閑古錐:YAMLファイルをProperties形式(風)で表示する(Python)


初めに

YAMLファイルは構造化されていて書きやすい反面、項目を探すのに苦労することがある。
たとえば、

sample.yml
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形式では、

sample.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の構造を表現した構造になっており、辞書型、リスト型、アトリビュートを持つオブジェクト、アトリビュートを持たないオブジェクトに部類される。アトリビュートを持たないオブジェクト以外は、ノードをもち階層を表現していて、ノードにはさらにオブジェクトが含まれる。この階層構造をたどると、最終的にはアトリビュートを持たないオブジェクトにたどり着く。アトリビュートを持たないオブジェクトは値であり、これまでたどったノード名とともに出力する。

ソースコード

yaml2properties.ph
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()

テスト

テストデータ

sample.yml
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