1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

初めに

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
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?