この記事ではPythonでYAMLのデータを読み込んでだり、PythonのデータをYAMLファイルへ書き出したりする方法について説明します。
もし、YAMLのことをあまり知らなければ、次の記事で詳しく解説していますので、そちらもご確認ください。
PythonでYAMLを読み書きするには、YAMLの処理系が必要です。この記事では処理系としてPyYAMLを利用します。
PyYAMLのインストール
PyYAMLを使用するには初めにインストールする必要があります。PyYAMLをインストールするには次のコマンドを実行します。
$ pip install pyyaml
YAMLを読み込む
PyYAMLでYAMLを読み込むにはyaml.load()関数を使います。
簡単なYAML文書を読み込んでみましょう。次のようなYAMLファイルを用意します。ファイル名はsample1.ymlとします。文字エンコーディングはUTF-8で保存します。
- one
- two
- three
最初にPyYAMLパッケージのインポートが必要です。次のようにインポートしましょう。
>>> import yaml
yaml.load()関数の1つ目の引数は、ファイルオブジェクトを受け入れます。また、PyYAML 5.1からはLoader引数にローダーを指定する必要があります。ローダーについてはこの後すぐに説明します。
>>> with open('sample1.yml') as fin:
... yaml.load(fin, Loader=yaml.FullLoader)
...
['one', 'two', 'three']
1つ目の引数に文字列を渡すこともできます。
>>> yaml.load('''
... - 1
... - 2
... - 3
... ''', Loader=yaml.FullLoader)
[1, 2, 3]
ローダー
PyYAML 5.1からローダーを指定しないyaml.load()関数の呼び出しは非推奨となり、警告が出るようになりました。
>>> with open('sample1.yml') as yml:
... yaml.load(yml)
...
<stdin>:2: YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe. Please read https://msg.pyyaml.org/load for full details.
['one', 'two', 'three']
信頼できないYAML文書をyaml.load()関数でロードすることは元々安全ではありませんでした。そのうよなYAML文書を安全にロードするためにPyYAMLではsafe_load()関数が提供されていました。安全ではないことはドキュメント化され、yaml.load()関数の使用はコード作成者の責任でした。これは意図してそのように設計されたものです。
しかし、yaml.load()関数が安全ではないことが脆弱性との指摘を受け、ローダーを指定しないyaml.load()関数が非推奨なりました。
警告が出ないようにするには、yaml.load()関数に明示的にローダーを指定します。指定できるローダーには次のものがあります。
- FullLoader
-
YAML言語を完全にロードできますが、任意のコードの実行は回避するローダーです。PyYAML 5.1以降、ローダーを明示的に指定しなかったときに呼びされる(ただし警告が出る)デフォルのローダーです。
- BaseLoader
-
基本的なYAML言語のみロードできるローダーです。すべてのスカラーは文字列としてロードされます。
- SafeLoader
-
YAML言語のサブセットを安全にロードするローダーです。信頼できないYAML文書をロードする時に推奨されるローダーです。
- UnsafeLoader
-
以前のローダーで、このローダーで信頼できないYAML文書をロードするのは安全ではありません。
yaml.load()関数にローダーを指定する代わりに、それぞれのローダーを使うショートカットのメソッドを使うこともできます。
- yaml.full_load()
- yaml.safe_load()
- yaml.unsafe_load()
複数のYAML文書を読み込む
1つのファイルや文字列に複数のYAML文書を含む場合、yaml.load_all()メソッドを使いすべての文書を読み込むことができます。
複数のYAML文書を含むYAMLファイルの例を以下に示します。
--- # 文書1
- one
- two
--- # 文書2
- 111
- 222
上述のYAMLファイルをyaml.load_all()ですべて読み込みます。yaml.load_all()の戻り値は、個々のYAML文書を返すジェネレーターオブジェクトです。
>>> with open('sample2.yml') as yml:
... docs = yaml.load_all(yml, Loader=yaml.FullLoader)
... for doc in docs:
... doc
...
['one', 'two']
[111, 222]
YAML文書を書き出す
PythonのオブジェクトをYAML文書に書き出すにはyaml.dump()メソッドを使います。yaml.dump()メソッドは、Pythonのオブジェクトを受け入れYAML文書を返します。
>>> print(yaml.dump([1, 2, 3]))
- 1
- 2
- 3
YAML文書をファイルへ出力するには、2番目の引数にファイルオブジェクトを指定します。
>>> with open('sample3.yml', 'w') as fout:
... yaml.dump([1, 2, 3], fout)
1つのファイルに複数のYAML文書を出力するには、yaml.dump_all()メソッドを使います。yaml.dump_all()はリストやジェネレータオブジェクトを受け入れます。
>>> with open('sample4.yml', 'w') as fout:
... yaml.dump_all([[1, 2], ['one', 'two']], fout)
出力されるファイルの内容は次の通りです。
- 1
- 2
---
- one
- two
dump()メソッドのオプション引数
yaml.dump()メソッドはYAML文書のフォーマットを指定するオプションのさまざまな引数があります。
explicit_start引数はディレクティブ終了マーカーを、explicit_end引数は文書終了マーカーの出力を制御します。
>>> print(yaml.dump([1, 2], explicit_start=True))
---
- 1
- 2
>>> print(yaml.dump([1, 2], explicit_end=True))
- 1
- 2
...
フロースタイルで出力するか、ブロックスタイルで出力するか制御することもできます。それにはdefault_flow_style引数を使います。Trueであればフロースタイル、Falseはブロックスタイル、NoneはPyYAMLがスタイルを調整した結果を出力します。
>>> data1 = [{'one': 1, 'two': 2}, {'three': 3, 'four': 4}]
>>> print(yaml.dump(data1, default_flow_style=True))
[{one: 1, two: 2}, {four: 4, three: 3}]
>>> print(yaml.dump(data1, default_flow_style=False))
- one: 1
two: 2
- four: 4
three: 3
>>> print(yaml.dump(data1, default_flow_style=None))
- {one: 1, two: 2}
- {four: 4, three: 3}
スカラーのスタイルも制御できます。いくつか例を示します。
>>> data2 = ["one\ntwo", "three\nfour\n"]
>>> print(yaml.dump(data2, default_style='"'))
- "one\ntwo"
- "three\nfour\n"
>>> print(yaml.dump(data2, default_style="|"))
- |-
one
two
- |
three
four
>>> print(yaml.dump(data2, default_style=">"))
- >-
one
two
- >
three
four
その他の引数を指定した場合の出力例を以下に示します。
>>> data3 = {'one':[1, 2], 'two':["3", "4", [5, 6]]}
>>> print(yaml.dump(data3, indent=4))
one:
- 1
- 2
two:
- '3'
- '4'
- - 5
- 6
>>> print(yaml.dump(data3, canonical=True))
---
!!map {
? !!str "one"
: !!seq [
!!int "1",
!!int "2",
],
? !!str "two"
: !!seq [
!!str "3",
!!str "4",
!!seq [
!!int "5",
!!int "6",
],
],
}
データ型の解決
YAML文書に記述されたデータは、PyYAMLによって読み込まれPythonのデータ型にマッピングされます。PythonのデータをYAML文書に書き出すときは反対の処理が行われます。
タグによるデータ型の指定
YAMLのデータにタグが明示的に指定されている場合、Pythonのどのデータ型にマッピングされるかは明らかです。よく使用するタグとPythonのデータ型の対応関係を見てみましょう。
最初はコレクションのタグとPythonのデータ型の対応関係からです。
タグ | Pythonのデータ型 |
---|---|
!!seq | リスト |
!!map | 辞書 |
!!set | 集合 |
次の例はPythonのオブジェクトをPyYAMLにタグ付きで出力させています。タグとPythonのデータ型の対応を確認してみてください。
>>> print(yaml.dump([1, 2], canonical=True))
---
!!seq [
!!int "1",
!!int "2",
]
>>> print(yaml.dump({'one': 1, 'two': 2}, canonical=True))
---
!!map {
? !!str "one"
: !!int "1",
? !!str "two"
: !!int "2",
}
>>> print(yaml.dump({1, 2}, canonical=True))
---
!!set {
? !!int "1"
: !!null "null",
? !!int "2"
: !!null "null",
}
スカラーで利用できるタグとPythonのデータ型の対応関係は次の通りです。
タグ | Pythonのデータ型 |
---|---|
!!str | str型 |
!!int | int型 |
!!float | float型 |
!!bool | ブール型 |
!!null | None |
!!timestamp | datetime.datetime型 |
同様にPyYAMLにタグ付きで出力させます。
>>> s = [
... 'str',
... 123,
... 3.14,
... [True, False],
... None,
... datetime.datetime.now()
... ]
>>> print(yaml.dump(s, canonical=True))
---
!!seq [
!!str "str",
!!int "123",
!!float "3.14",
!!seq [
!!bool "true",
!!bool "false",
],
!!null "null",
!!timestamp "2023-05-07 22:55:00.657419",
]
この他にPyYAML(Python)固有のタグもあります、PyYAMLで使用できるタグの完全な一覧は「PyYAMLのドキュメント」を参照ください。
タグなしデータのデータ型解決
YAML文書でタグが明示的に指定されていないデータは、どのデータ型にマッチするか順番にチェックされ、最初に一致したデータ型にマッピングされます。
どの順番でどのデータ型をチェックするかはYAML処理系に依存します。PyYAMLのドキュメントでは明確な答えは確認できませんでしたが、次に幾つかの例を示します。
str: foo
int: -12
float: 3.14
list: [1, 2, 3]
dict: {one: 1, two: 2}
none: [~, null]
bool: [true, false, on, off]
=>
{'str': 'foo',
'int': -12,
'float': 3.14,
'list': [1, 2, 3],
'dict': {'one': 1, 'two': 2},
'none': [None, None],
'bool': [True, False, True, False]}
例外
YAML文書の解析中にエラーに遭遇すると、YAMLErrorまたはそのサブクラスのオブジェクトを送出します。
次の例はYAMLError(またはそのサブクラス)をキャッチしてエラーのあった位置を表示します。
>>> try:
... yaml.load("[[", Loader=yaml.FullLoader)
... except yaml.YAMLError as exc:
... if hasattr(exc, 'problem_mark'):
... mark = exc.problem_mark
... print(f'Error pos: ({mark.line+1}:{mark.column+1})')
...
Error pos: (1:3)
まとめ
PyYAMLのドキュメントはYAML 1.1を参照している箇所があるので、YAML 1.1に準拠しているのかもしれません。最新はYAML 1.2です。仕様で定義されているタグに多少の違いが見受けられますので、注意する必要がありそうです。