初めに
YAMLファイルは構造化されていて書きやすい反面、項目を探すのに苦労することがある。
たとえば、
service:
apply-date: 2021-06-19
application:
apply-date: 2021-06-20
というファイルがあって、applicationのapply-dateの値を探したいとき、grepで検索すると、
$ grep apply-date sample.yml
apply-date: 2021-06-19
apply-date: 2021-06-20
という結果になって、ほしい情報が得られないことがある。
一方、YAMLを使うまえによく使っていたproperties形式では、
service.apply-date=2021-06-19
application.apply-date=2021-06-20
となっており、上記のような検索をするには便利である。
そこで、YAMLファイルを読み込んでProperties形式(風)に出力するプログラムを探してみたが、見つからなかったので、自作することにした。当方、Pythonでのプログラムつくりは経験がなかったので、勉強を兼ねてPythonで組んでみることにしようと思う。
以下のような仕様で作成する。
- YAMLファイルの読み込みはPyYAMLを使う
- 複数ファイル扱えるようにし、出力にファイル名を付加する、しないを指定できるようにする
- 検索が主な目的なので、文字列とか数値などの区別はしない
- シーケンスのindexを付ける、付けないを指定できるようにする(順番を無視したい場合を考慮する)
- 値に改行が含まれる場合は、改行を"\n"という文字列に置き換える
- 値に「#'"」などが含まれていても、そのまま出力する
- 値に漢字が含まれていてもそのまま出力する(unicode変換はしない)
- 標準入力を対象にしない
- 実行環境はPython 3.7.3(他は検証しない)
プログラムについて
中心となるのは、メソッドy2pである。targetは、YAMLファイルをパースして得られたオブジェクトで、YAMLの構造を表現した構造になっており、辞書型、リスト型、アトリビュートを持つオブジェクト、アトリビュートを持たないオブジェクトに部類される。アトリビュートを持たないオブジェクト以外は、ノードをもち階層を表現していて、ノードにはさらにオブジェクトが含まれる。この階層構造をたどると、最終的にはアトリビュートを持たないオブジェクトにたどり着く。アトリビュートを持たないオブジェクトは値であり、これまでたどったノード名とともに出力する。
ソースコード
import argparse
import io
import os.path
import re
import sys
import yaml
# yamlファイルを読み込んでproperties形式で出力する
class Yaml2Properties:
# ファイル名と引数オプションを使って生成する
def __init__(self, fileName, args):
self.fileName = fileName
self.args = args
# このオブジェクトを生成して値として返すファクトリメソッド
@classmethod
def of(cls, fileName, args):
obj = Yaml2Properties(fileName, args)
return obj
# targetに含まれるオプジェクトを解析する
# オブジェクトが含まれる場合は再帰的に呼び出す
def y2p(self, path, target, separator):
try:
# 辞書型
if isinstance(target, dict):
for key, value in target.items():
self.y2p(path + separator + str(key), value, separator)
# None型
elif target is None:
self.print_line(path, '')
# list型
elif isinstance(target, list):
for i, item in enumerate(target):
if(self.args.noindex):
index = ''
else:
index = str(i)
self.y2p(path + '[' + index + ']', item, separator)
# アトリビュートを持つ
elif hasattr(target, '__dict__'):
for key, value in target.__dict__.values():
self.y2p(path + separator + str(key), value, separator)
# アトリビュートを持たない、すなわち値
else:
self.print_line(path, target)
# 再帰の呼び出し元で受けたRuntimeErrorは呼び出し先で発生させたものなので無視する
except RuntimeError as e:
raise RuntimeError(e)
except Exception as e:
raise RuntimeError(
'unknown object type:' +
str(e) +
'\n,at:' +
path +
'\n,target:' +
str(target))
# pathとtarget(値)を出力する
def print_line(self, path, target):
# ファイル名を出力しない
if self.args.nofilename:
f = ''
# ファイル名を出力する
else:
f = self.fileName + ':'
# pathから先頭の'.'を取り除く
p = re.sub('^\\.', '', path)
# targetに含まれる改行を置き換える
t = str(target).replace('\n', '\\n')
print(f + p + '=' + t)
# 変換開始
def convert(self):
# ファイルが存在するかどうかチェック
if not os.path.isfile(self.fileName):
raise RuntimeError('file not found:' + self.fileName)
# ファイルを開く
with open(self.fileName, 'r', encoding='utf-8') as f:
# PyYAMLを使ってyamlファイルをオブジェクトに変換する
data = yaml.load(f, Loader=yaml.SafeLoader)
# オブジェクトの構造を解析して、properties形式で出力する
self.y2p('', data, '.')
# main
def main():
# 標準出力の文字コードを設定
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
# 引数の処理
parser = argparse.ArgumentParser(description='Yaml to Properties')
parser.add_argument('fileName', nargs='+', help='ex:yaml-file.yml')
parser.add_argument(
'-f',
'--nofilename',
action='store_true',
help='no file name')
parser.add_argument(
'-i',
'--noindex',
action='store_true',
help='no index of array')
args = parser.parse_args()
# 与えられたファイル名すべてを処理する
for fileName in args.fileName:
try:
Yaml2Properties.of(fileName, args).convert()
except Exception as e:
print('error:' + str(e))
main()
テスト
テストデータ
simple:
string: sample
integer: 123
float: 4.56
true: true
false: false
date: 2015-7-27
blank:
quoted:
integer: "789"
true: "true"
complex:
- name: test1
age: 10
- name: test2
age: 20
- name: test3
age: 30
実行結果
$ python --version
Python 3.7.3
$ python yaml2properties.py sample.yml
sample.yml:simple.string=sample
sample.yml:simple.integer=123
sample.yml:simple.float=4.56
sample.yml:simple.True=True
sample.yml:simple.False=False
sample.yml:simple.date=2015-7-27
sample.yml:simple.blank=
sample.yml:simple.quoted.integer=789
sample.yml:simple.quoted.True=true
sample.yml:complex[0].name=test1
sample.yml:complex[0].age=10
sample.yml:complex[1].name=test2
sample.yml:complex[1].age=20
sample.yml:complex[2].name=test3
sample.yml:complex[2].age=30