【PyYAML VS ruamel.yaml】PythonからYAMLファイルを触ってみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Python で YAML ファイル操作する必要があり、2種類のライブラリ

を触る機会があったので、簡単な使い方を記録しておきます。

実行環境

  • Python3.6
  • OS : Amazon Linux
  • LibYAML : 0.1.6
  • PyYAML : 3.12
  • ruamel.yaml : 0.15.0

tl;dr

  • ともに YAML 1.2 に対応。
  • PyYAML は古くから存在し、シンプルな API で基本機能に特化。ドキュメントの多くが Python2 時代のまま。
  • ruamel.yaml は PyYAML のフォークでこまめにアップデートされている。入出力結果ができるだけ同じになるように設計されている。

LibYAML のインストール

PyYAML/ruamel.yaml ともに LibYAML と連携可能です。ライブラリインストール時に LibYAML がインストール済みであれば、利用可能になります。特別な理由がない限りはインストールしておきましょう。

$ sudo yum install -y libyaml-devel

PyYAML の使い方

インストール

$ python -m venv test
$ source test/bin/activate
$ pip install -U pyyaml

ファイル入出力

YAML ファイルの入出力をやってみます

import yaml

with open('in.yml') as stream:
    data = yaml.load(stream)

with open('out.yml', 'w') as stream:
    yaml.dump(data, stream=stream))

フロー vs ブロック

出力内容からわかるように、PyYAML はデフォルトではフロースタイルを採用しています。 ブロックスタイルで出力するには dump 時に default_flow_style=False オプションを渡します。

>>> print(yaml.dump(yaml.load('a: b')))
{a: b}

>>> print(yaml.dump(yaml.load('a: b'), default_flow_style=False))
a: b

ユニコードの扱い

日本語などの非アスキー文字を dump 時にエンコードされないようにするには、allow_unicode=True オプションを渡します。

>>> print(yaml.dump(yaml.load('a: あ')))
{a: "\u3042"}

>>> print(yaml.dump(yaml.load('a: あ'), allow_unicode=True))
{a: あ}

数字のみで構成される文字列

数字のみで構成される文字列の場合、

  • 0 で始まらなければ、整数
  • 0 で始まる場合、octet 評価できれば octet、出来なければ文字列 として扱われます
>>> yaml.load('a: 123')
{'a': 123}
>>> yaml.load('a: 012') # octet
{'a': 10}
>>> yaml.load('a: 09') #
{'a': '09'}

null の扱い

null は常に null で出力されます。

>>> yaml.dump(yaml.load('a: [null, ~]'))
'a: [null, null]\n'

コメントの扱い

ロード時にコメントは破棄されます。

>>> data = yaml.load('''
... # comment
... a: b
... c: d #comment''')
>>> yaml.dump(data)
'{a: b, c: d}\n'

ディレクティブのセパレーター

ディレクティブごとにセパレーター(---)を挿入する場合、explicit_start=True オプションを渡します。

>>> yaml.dump('a: b', explicit_start=True)
"--- 'a: b'\n"

順序の維持について

PyYAML は入出力の前後で順序が維持されることを保障しません。 順序を維持する PR が存在します。

Allow to turn off sorting keys in Dumper #143 · yaml/pyyaml · GitHub

ruamel.yaml の使い方

PyYAML だけでは要件を実現出来ない箇所があったため、かわりに見つけたのが ruamel.yaml です。

インストール

$ python -m venv test
$ source test/bin/activate
$ pip install -U ruamel.yaml

ファイル入出力

YAML ファイルの入出力をやってみます

import ruamel
import ruamel.yaml

yaml = ruamel.yaml.YAML()

with open('in.yml') as stream:
    data = yaml.load(stream)

with open('out.yml', 'w') as stream:
    yaml.dump(data, stream=stream)

PyYAML とほぼ同じです。

以下の様にファイル出力することも出来ます。

from pathlib import Path
stream = Path('out.yml')
yaml.dump(data, stream=stream)

フロー vs ブロック

ruamel.yaml はデフォルトでブロックスタイルを採用しています。

フロースタイルにするには yaml.default_flow_style を True にします。

>>> yaml.default_flow_style
False
>>> yaml.dump(yaml.load('a: b'), sys.stdout)
a: b

ユニコードの扱い

特別な対応は不要です。

>>> yaml.dump(yaml.load('a: あ'), sys.stdout)
a: あ

数字のみで構成される文字列

数字のみで構成される文字列の場合、不思議な仕様です。

  • 0 で始まらなければ、整数
  • 0 で始まる場合、octet 評価できれば先頭の0を除いた整数として評価、出来なければ文字列として扱われます
>>> yaml.load('a: 123')
CommentedMap([('a', 123)])
>>> yaml.load('a: 012')
CommentedMap([('a', 12)]) # ← 10 進の 10 ではない
>>> yaml.load('a: 09')
CommentedMap([('a', '09')])

少しモヤモヤしますね。

null の扱い

null は ~/null では無く空文字で出力されます。

>>> yaml.dump({'a': None}, sys.stdout)
a:

ここもモヤモヤしますね。 YAML 1.2 の null の定義を引用します

Represents the lack of a value. This is typically bound to a native null-like value (e.g., undef in Perl, None in Python). Note that a null is different from an empty string.

なお null の出力はカスタマイズ出来ます。

例えば null を ~ で出力したい場合、次のようにします。

>>> def custom_none(self, data):
....    return self.represent_scalar(u'tag:yaml.org,2002:null', u'~')
>>> ruamel.yaml.RoundTripRepresenter.add_representer(type(None), custom_none)
>>> yaml.dump({'a': None}, sys.stdout)
a: ~

関連 https://bitbucket.org/ruamel/yaml/issues/169/roundtripdumper-dumps-null-values

コメントの扱い

コメントは保持されます。

>>> data = yaml.load('''
... # comment
... a: b
... c: d #comment''')
>>> yaml.dump(data, sys.stdout)
# comment
a: b
c: d #comment

ディレクティブのセパレーター

ディレクティブごとにセパレーター(---)を挿入する場合、explicit_start=True にします。

>>> yaml.explicit_start
>>> yaml.explicit_start = True
>>> yaml.dump({'a': 1}, sys.stdout)
---
a: 1

順序の維持について

入出力により、順序が維持されることを保障します。 新規アイテムは末尾に追加されます。

>>> data = yaml.load('''
... a: b
... c: d''')
>>> data
CommentedMap([('a', 'b'), ('c', 'd')])
>>> data['e'] = 'f' # アイテムを追加
>>> yaml.dump(data, sys.stdout)
a: b
c: d
e: f

まとめ

PyYAML とそのフォークである ruamel.yaml の使い方を紹介しました。

PyYAML が YAML の仕様を重視したライブラリであるのに対して、ruamel.yaml は YAML 処理を重視したライブラリと言う印象を持ちました。

用途に合わせて使い分けて下さい。

参考

  • PyYAML https://bitbucket.org/xi/pyyaml
  • ruamel.yaml http://yaml.readthedocs.io/en/latest/
  • LibYAML http://pyyaml.org/wiki/LibYAML