PythonでYAMLを読み書きする方法

この記事では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のデータ型
!!strstr型
!!intint型
!!floatfloat型
!!boolブール型
!!nullNone
!!timestampdatetime.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です。仕様で定義されているタグに多少の違いが見受けられますので、注意する必要がありそうです。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

・PM、SE、SIなどを20年以上経験
・ネットワークスペシャリスト、セキュリティスペシャリストなど複数の資格を保有

目次