Pythonライブラリ PyYAML 詳細解説:YAMLデータの読み書きから高度な使い方まで

設定ファイルやデータ交換に便利なYAMLをPythonで自在に操る

現代のソフトウェア開発において、設定ファイルの管理やプログラム間でのデータ交換は避けて通れない課題です。JSONやXMLなど様々なフォーマットが存在しますが、近年、その人間にとっての読みやすさ書きやすさからYAML (YAML Ain’t Markup Language) が注目を集めています。

Pythonエコシステムには、このYAMLを簡単に扱うための強力なライブラリ PyYAML が存在します。このブログ記事では、PyYAMLの基本的な使い方から、データ型の扱い、高度な機能、そして重要なセキュリティ上の注意点まで、詳細に解説していきます。PyYAMLをマスターして、Pythonでのデータ処理をより効率的かつ安全に行いましょう! 👍

1.1. YAMLとは?

YAMLは、構造化されたデータを表現するためのデータシリアライゼーション言語です。JSONと同様にキーと値のペア(マッピング)や順序付けられた値のリスト(シーケンス)を表現できますが、インデント(字下げ)を使って階層構造を示すため、JSONよりも括弧が少なく、人間にとって直感的に理解しやすいのが大きな特徴です。

例えば、以下は同じ情報をJSONとYAMLで表現した例です。

JSONの例

{
  "name": "Taro Yamada",
  "age": 30,
  "skills": [
    "Python",
    "YAML",
    "Web Development"
  ],
  "address": {
    "street": "1-2-3 Example St",
    "city": "Tokyo"
  }
}

YAMLの例

name: Taro Yamada
age: 30
skills:
  - Python
  - YAML
  - Web Development
address:
  street: 1-2-3 Example St
  city: Tokyo

YAMLは、設定ファイル(例: Docker Compose, Kubernetesマニフェスト, Ansible Playbook)、アプリケーション間のデータ転送、オブジェクトの永続化など、幅広い用途で利用されています。

1.2. PyYAMLとは?

PyYAMLは、PythonプログラムでYAMLデータを解析(読み込み)したり、生成(書き込み)したりするためのライブラリです。Pythonのネイティブなデータ型(辞書、リスト、文字列、数値など)とYAMLのデータ構造をシームレスに相互変換する機能を提供します。

PyYAMLは、YAML 1.1 仕様をサポートしており、Pythonコミュニティで広く利用されています。Unicodeサポート、pickleサポート(後述しますが注意が必要)、拡張APIなども備えています。

1.3. なぜPyYAMLを使うのか?

  • 可読性の高い設定ファイル: YAMLの読みやすさを活かし、複雑な設定も分かりやすく記述できます。
  • 簡単なデータ交換: Pythonオブジェクトを簡単にYAML形式に変換し、他のプログラムやシステムとデータを共有できます。
  • Pythonとの親和性: Pythonの辞書やリストと自然に連携できるため、コードがシンプルになります。

2.1. インストール

PyYAMLはPythonの標準ライブラリではないため、pipを使ってインストールする必要があります。ターミナルまたはコマンドプロンプトで以下のコマンドを実行してください。

pip install pyyaml

インストールが完了したら、Pythonスクリプト内で `import yaml` として利用できます。

💡 Tips: PyYAMLには、C言語で実装された高速なパーサーとエミッター (LibYAMLバインディング) も含まれています。これを利用するには、システムにLibYAMLライブラリがインストールされている必要があります。LibYAMLが利用可能な場合、PyYAMLは自動的にそれを使用しようとしますが、明示的に指定することも可能です(後述)。

2.2. 基本的なデータの読み込み (`yaml.safe_load`)

YAMLファイルやYAML形式の文字列からデータを読み込むには、`yaml.safe_load()` 関数を使用するのが最も安全で推奨される方法です。

例として、以下のような `config.yaml` ファイルがあるとします。

# config.yaml
database:
  host: localhost
  port: 5432
  user: admin
  enabled: true
api_keys:
  - key1_value_abc
  - key2_value_def

このファイルをPythonで読み込むコードは以下のようになります。

import yaml

# ファイルから読み込む場合
try:
    with open('config.yaml', 'r', encoding='utf-8') as file:
        config_data = yaml.safe_load(file)
        print("ファイルから読み込んだデータ:")
        print(config_data)
        print(f"データベースホスト: {config_data['database']['host']}")
except FileNotFoundError:
    print("エラー: config.yamlが見つかりません。")
except yaml.YAMLError as e:
    print(f"YAMLの解析エラー: {e}")

print("-" * 20)

# 文字列から読み込む場合
yaml_string = """
service:
  name: my-app
  version: 1.2.0
  replicas: 3
"""

try:
    service_data = yaml.safe_load(yaml_string)
    print("文字列から読み込んだデータ:")
    print(service_data)
    print(f"サービス名: {service_data['service']['name']}")
except yaml.YAMLError as e:
    print(f"YAMLの解析エラー: {e}")

`yaml.safe_load()` は、YAMLデータをPythonの辞書やリストなどの基本的なオブジェクトに変換します。

2.3. 基本的なデータの書き込み (`yaml.dump`)

Pythonのオブジェクト(辞書やリストなど)をYAML形式の文字列やファイルに書き込むには、`yaml.dump()` 関数を使用します。

import yaml

python_data = {
    'user': {
        'name': 'Hanako Sato',
        'age': 25,
        'email': 'hanako.sato@example.com',
        'active': True,
        'roles': ['editor', 'viewer']
    },
    'settings': {
        'theme': 'dark',
        'notifications': None # Noneはnullになります
    }
}

# 文字列として出力
try:
    yaml_output_string = yaml.dump(python_data, allow_unicode=True, sort_keys=False)
    print("YAML文字列出力:")
    print(yaml_output_string)
except yaml.YAMLError as e:
    print(f"YAMLの書き込みエラー: {e}")

print("-" * 20)

# ファイルに書き込む
try:
    with open('output.yaml', 'w', encoding='utf-8') as file:
        # default_flow_style=False にするとブロック形式(インデント形式)になります
        yaml.dump(python_data, file, default_flow_style=False, allow_unicode=True, sort_keys=False)
    print("output.yaml に書き込みました。")
except yaml.YAMLError as e:
    print(f"YAMLの書き込みエラー: {e}")

`dump()` 関数にはいくつかのオプションがあります。

  • `allow_unicode=True`: 日本語などのUnicode文字をそのまま出力します(デフォルトでは `\uXXXX` 形式にエスケープされることがあります)。
  • `sort_keys=False`: 辞書のキーをソートせずに出力します(デフォルトは `True` でキーがアルファベット順にソートされます)。
  • `default_flow_style=False`: デフォルトのフロースタイル(JSONのような `{}` や `[]` を使う形式)ではなく、ブロックスタイル(インデントを使う形式)で出力します。読みやすさのためには `False` を指定することが多いです。
  • `indent=N`: インデントのスペース数を指定します(デフォルトは2)。`default_flow_style=False` の場合に有効です。

2.4. `safe_load` vs `load`: 🚨 セキュリティ上の最重要注意点 🚨

PyYAMLには `yaml.load()` という関数も存在しますが、信頼できないソースからのYAMLデータを `yaml.load()` で処理することは絶対に避けてください。

`yaml.load()` の危険性

`yaml.load()` は、デフォルトのローダー (`FullLoader` またはそれ以前のバージョンのデフォルトローダー) を使用すると、YAMLドキュメント内で任意のPythonオブジェクトを構築したり、任意のPythonコードを実行したりできてしまう深刻な脆弱性を持っていました。これは、悪意のあるYAMLファイルを開くだけで、システムが乗っ取られる可能性があることを意味します。

過去に、この脆弱性を悪用した攻撃 (例: CVE-2017-18342, CVE-2020-1747, CVE-2020-14343 など) が実際に報告されています。これらの脆弱性は `yaml.load()` が内部でPythonの `pickle` モジュールと同様の機能を提供していたことに起因します。

PyYAML 5.1 以降では、`yaml.load()` を `Loader` 引数なしで呼び出すと警告が表示されるようになり、デフォルトのローダーが変更されましたが、依然として安全ではありません。

原則として、常に `yaml.safe_load()` を使用してください。 `yaml.safe_load()` は、YAMLドキュメントを基本的なPythonの型(文字列、数値、リスト、辞書、真偽値、None)にのみ変換し、任意のコード実行のリスクを排除します。
import yaml

# 安全な例
yaml_data_safe = "key: value"
data_safe = yaml.safe_load(yaml_data_safe)
print(f"安全なロード結果: {data_safe}")

# 危険な例 (絶対に実行しないでください!)
# yaml_data_unsafe = "!!python/object/apply:os.system ['echo vulnerable']"
# try:
#     # yaml.load を使うとコマンドが実行される可能性がある
#     data_unsafe = yaml.load(yaml_data_unsafe, Loader=yaml.UnsafeLoader) # または Loader=yaml.FullLoader や Loader引数なし (古いバージョン)
#     print(f"危険なロード結果: {data_unsafe}")
# except Exception as e:
#     print(f"危険なロード中にエラー: {e}")

# 推奨される方法
yaml_string = "items: [a, b, c]"
try:
    data = yaml.safe_load(yaml_string)
    print(f"推奨されるロード方法の結果: {data}")
except yaml.YAMLError as e:
    print(f"YAML解析エラー: {e}")

もし、どうしてもカスタムオブジェクトをデシリアライズする必要がある場合は、後述するカスタムコンストラクタを慎重に実装するか、信頼できるデータソースであることを十分に確認する必要があります。しかし、基本的には避けるべきです。

PyYAMLはYAMLの様々なデータ型をPythonの対応する型にマッピングします。

3.1. 基本的なデータ型

YAMLの基本的なスカラー値は、Pythonの基本的な型に変換されます。

YAMLの例 Pythonの型 説明
my_string: Hello World
another_string: "Quoted String"
multiline: |
  This is line 1.
  This is line 2.
str 文字列。引用符は通常不要ですが、特殊文字を含む場合や曖昧さを避けたい場合に使用します。| は改行を保持し、> は改行をスペースに置き換えます(最終行の改行は保持)。
integer_value: 123
negative_int: -45
int 整数。
float_value: 3.14159
scientific: 6.022e23
float 浮動小数点数。
is_active: true
is_enabled: false
use_yes: yes
use_no: no
turn_on: on
turn_off: off
bool 真偽値。true/false, yes/no, on/off が認識されます (YAML 1.1)。YAML 1.2 では true/false のみが標準ですが、PyYAML は互換性のために他も認識することがあります。
no_value: null
empty_value: ~
missing:
None Null値。null またはチルダ(~)、あるいはキーに対して値を指定しないことで表現されます。
timestamp: 2025-04-06T05:08:00Z
date: 2025-04-06
datetime.datetime / datetime.date PyYAMLはISO 8601形式の日時や日付を認識し、Pythonの `datetime` オブジェクトに変換します。
import yaml
from datetime import datetime, date

yaml_data = """
string_example: Hello YAML!
integer_example: 42
float_example: 2.71828
boolean_true: true
boolean_no: no
null_example: null
datetime_example: 2024-01-15T10:30:00Z
date_example: 2024-01-15
"""

data = yaml.safe_load(yaml_data)

print(f"String: {data['string_example']} (Type: {type(data['string_example'])})")
print(f"Integer: {data['integer_example']} (Type: {type(data['integer_example'])})")
print(f"Float: {data['float_example']} (Type: {type(data['float_example'])})")
print(f"Boolean (true): {data['boolean_true']} (Type: {type(data['boolean_true'])})")
print(f"Boolean (no): {data['boolean_no']} (Type: {type(data['boolean_no'])})")
print(f"Null: {data['null_example']} (Type: {type(data['null_example'])})")
print(f"Datetime: {data['datetime_example']} (Type: {type(data['datetime_example'])})")
print(f"Date: {data['date_example']} (Type: {type(data['date_example'])})")

3.2. リスト (シーケンス)

YAMLのシーケンスはPythonのリスト (`list`) に変換されます。ハイフン (`-`) を使って各要素を示します。フロースタイル(JSONライク)の角括弧 (`[]`) も使用可能です。

# ブロックスタイル
items:
  - apple
  - banana
  - cherry

# フロースタイル
colors: [red, green, blue]

# 混合スタイルも可能
mixed:
  - item1
  - [subitemA, subitemB]
  - key: value
import yaml

yaml_sequences = """
items:
  - apple
  - banana
  - cherry
colors: [red, green, blue]
mixed:
  - item1
  - [subitemA, subitemB]
  - {key: value} # マッピングも要素にできる
"""

data = yaml.safe_load(yaml_sequences)

print(f"Items: {data['items']} (Type: {type(data['items'])})")
print(f"Colors: {data['colors']} (Type: {type(data['colors'])})")
print(f"Mixed: {data['mixed']} (Type: {type(data['mixed'])})")
print(f"Mixed item 2: {data['mixed'][1]} (Type: {type(data['mixed'][1])})")
print(f"Mixed item 3: {data['mixed'][2]} (Type: {type(data['mixed'][2])})")

3.3. 辞書 (マッピング)

YAMLのマッピングはPythonの辞書 (`dict`) に変換されます。コロン (`:`) を使ってキーと値を区切ります。フロースタイルの中括弧 (`{}`) も使用可能です。

# ブロックスタイル
user:
  name: Jiro Suzuki
  age: 40
  city: Osaka

# フロースタイル
server: {host: 192.168.1.100, port: 8080}

# ネストしたマッピング
config:
  database:
    type: postgresql
    host: db.example.com
  cache:
    enabled: true
    type: redis
import yaml

yaml_mappings = """
user:
  name: Jiro Suzuki
  age: 40
  city: Osaka
server: {host: 192.168.1.100, port: 8080}
config:
  database:
    type: postgresql
    host: db.example.com
  cache:
    enabled: true
    type: redis
"""

data = yaml.safe_load(yaml_mappings)

print(f"User: {data['user']} (Type: {type(data['user'])})")
print(f"Server: {data['server']} (Type: {type(data['server'])})")
print(f"Config: {data['config']} (Type: {type(data['config'])})")
print(f"Database host: {data['config']['database']['host']}")

3.4. ネストされたデータ構造

YAMLでは、リストの中に辞書を入れたり、辞書の値としてリストを使ったり、自由にデータ構造をネストさせることができます。PyYAMLもこれを正しく解釈し、対応するPythonのネスト構造(リストのリスト、辞書のリスト、リストを含む辞書など)を生成します。

users:
  - id: 1
    name: Alice
    tags: [admin, developer]
    settings:
      theme: light
      notifications: true
  - id: 2
    name: Bob
    tags: [viewer]
    settings:
      theme: dark
      notifications: false
import yaml

yaml_nested = """
users:
  - id: 1
    name: Alice
    tags: [admin, developer]
    settings:
      theme: light
      notifications: true
  - id: 2
    name: Bob
    tags: [viewer]
    settings:
      theme: dark
      notifications: false
"""

data = yaml.safe_load(yaml_nested)

print(f"Users list: {data['users']}")
print(f"First user's name: {data['users'][0]['name']}")
print(f"First user's tags: {data['users'][0]['tags']}")
print(f"Second user's settings: {data['users'][1]['settings']}")

3.5. 複数ドキュメントの扱い

一つのYAMLファイル(または文字列)内に、`—` (ドキュメント開始マーカー) で区切って複数のYAMLドキュメントを含めることができます。オプションで `…` (ドキュメント終了マーカー) も使用できます。

これを読み込むには `yaml.safe_load_all()` 関数を使用します。この関数はジェネレータを返し、各ドキュメントに対応するPythonオブジェクトを順番に生成します。

# multi_doc.yaml
document: 1
data:
  - a
  - b
---
document: 2
type: config
settings:
  feature_x: enabled
...
---
document: 3
message: End of documents
import yaml

# ファイルから読み込む場合
try:
    with open('multi_doc.yaml', 'r', encoding='utf-8') as file:
        print("複数ドキュメントの読み込み (ファイル):")
        for idx, doc_data in enumerate(yaml.safe_load_all(file)):
            print(f"--- Document {idx + 1} ---")
            print(doc_data)
except FileNotFoundError:
    print("エラー: multi_doc.yamlが見つかりません。")
except yaml.YAMLError as e:
    print(f"YAMLの解析エラー: {e}")

print("-" * 20)

# 文字列から読み込む場合
multi_yaml_string = """
name: doc1
value: 10
---
name: doc2
items: [x, y]
---
name: doc3
valid: true
"""

try:
    print("複数ドキュメントの読み込み (文字列):")
    for idx, doc_data in enumerate(yaml.safe_load_all(multi_yaml_string)):
        print(f"--- Document {idx + 1} ---")
        print(doc_data)
except yaml.YAMLError as e:
    print(f"YAMLの解析エラー: {e}")

# 複数ドキュメントを書き込むには yaml.dump_all() を使う
docs_to_write = [
    {'id': 1, 'content': 'first'},
    {'id': 2, 'content': 'second'}
]

try:
    with open('multi_output.yaml', 'w', encoding='utf-8') as file:
        yaml.dump_all(docs_to_write, file, default_flow_style=False, allow_unicode=True)
    print("multi_output.yaml に複数ドキュメントを書き込みました。")
except yaml.YAMLError as e:
    print(f"YAMLの書き込みエラー: {e}")

3.6. タグ (`!tag`) とカスタムオブジェクト

YAMLには「タグ」という機能があり、ノード(値)がどのような種類であるかを明示的に示すことができます。標準タグ(例: `!!str`, `!!int`, `!!map`, `!!seq` など)は通常暗黙的に解決されますが、明示的に書くこともできます。

PyYAMLは、これを利用してPythonのカスタムオブジェクトをシリアライズ(YAML化)およびデシリアライズ(Pythonオブジェクト化)する機能を提供します。ただし、前述の通り、この機能は `yaml.load()` (または `UnsafeLoader`, `FullLoader`) を使う必要があり、セキュリティリスクを伴います

`yaml.safe_load()` でカスタムオブジェクトを扱いたい場合は、後述する `add_constructor` を使って、特定のタグに対する安全なデシリアライズ処理を自分で登録する必要があります。

以下は、`yaml.dump()` でカスタムオブジェクトをシリアライズする例です。(デシリアライズは `yaml.load()` が必要になるため、ここでは省略します)。

import yaml

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # オブジェクトの文字列表現 (デバッグ用)
    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

# Personオブジェクトを作成
person_obj = Person("Kenji Tanaka", 35)

# yaml.dumpでシリアライズ (カスタムタグが付与される)
# Dumperの指定が必要な場合がある
try:
    # PyYAML 5.1以降、カスタムオブジェクトのダンプにはデフォルトのDumperでは警告が出るか、
    # 場合によっては明示的な指定が必要。
    # ここでは yaml.Dumper を使う (ただし、load時に安全でない可能性を示唆)
    yaml_custom_obj = yaml.dump(person_obj, Dumper=yaml.Dumper, sort_keys=False)
    print("カスタムオブジェクトのシリアライズ結果:")
    print(yaml_custom_obj)
except yaml.YAMLError as e:
    print(f"YAML書き込みエラー: {e}")

# 出力例 (PyYAMLのバージョンや環境によりタグの形式が異なる場合があります):
# !!python/object:__main__.Person
# age: 35
# name: Kenji Tanaka

# 注意: 上記のYAMLを yaml.safe_load() で読み込もうとするとエラーになります。
# yaml.load(..., Loader=yaml.UnsafeLoader) などが必要ですが、非推奨です。

# 安全にカスタムオブジェクトを扱う代替案:
# 1. オブジェクトを辞書に変換してから dump する
person_dict = {'name': person_obj.name, 'age': person_obj.age, '_type': 'Person'}
yaml_dict_output = yaml.dump(person_dict, default_flow_style=False, allow_unicode=True, sort_keys=False)
print("\n辞書に変換してからダンプ:")
print(yaml_dict_output)

# 2. 読み込み時に、特定のタグを持つデータを安全に再構築する処理を自作する (add_constructor)
# (詳細は「高度な使い方」で説明)

⚠️ カスタムオブジェクトの注意点

`!!python/object` などのタグを使ったカスタムオブジェクトのシリアライズ/デシリアライズは非常に強力ですが、安易に使うべきではありません。特に、外部から受け取ったYAMLデータをデシリアライズする場合は、`yaml.safe_load()` を使い、必要であれば安全なカスタムコンストラクタを実装してください。

4.1. `dump` 関数の詳細オプション

`yaml.dump()` 関数は、出力形式を細かく制御するための多くのキーワード引数を受け付けます。主なものをいくつか紹介します。

オプション 説明
stream 出力先のファイルオブジェクトやストリーム。指定しない場合は文字列として返されます。 with open('out.yaml', 'w') as f: yaml.dump(data, stream=f)
Dumper 使用する Dumper クラスを指定します。デフォルトは `yaml.Dumper` ですが、`yaml.SafeDumper` を使うとカスタムオブジェクトタグなど、安全でない可能性のある機能が無効化されます。C拡張が利用可能な場合は `yaml.CDumper`, `yaml.CSafeDumper` が高速です。 yaml.dump(data, Dumper=yaml.SafeDumper)
default_flow_style コレクション(リストや辞書)のデフォルトのスタイル。`False` (デフォルト)でブロックスタイル、`True` でフロースタイルになります。個々のコレクションでスタイルが異なる場合、PyYAMLは自動で判断しますが、明示的に指定すると統一できます。 yaml.dump(data, default_flow_style=False)
default_style スカラー値(文字列など)のデフォルトの引用符スタイル。`’` (シングルクォート), `”` (ダブルクォート), `|` (リテラル), `>` (フォールド)などを指定できます。`None` (デフォルト)の場合はPyYAMLが適切に判断します。 yaml.dump({'text': long_text}, default_style='|')
canonical `True` にすると、正規の(canonical)YAML形式で出力します。より冗長になりますが、機械処理には適している場合があります。 yaml.dump(data, canonical=True)
indent ブロックスタイルでのインデントのスペース数。デフォルトは2です。 yaml.dump(data, indent=4, default_flow_style=False)
width 一行の推奨最大幅。PyYAMLはこの幅を超えないように、フロースタイルのコレクションや長い文字列を折り返そうとします。デフォルトは80です。 yaml.dump(long_list, width=120)
allow_unicode `True` にすると、非ASCII文字をUnicode文字としてそのまま出力します。デフォルトは `False` で、`\uXXXX` 形式にエスケープされます。 yaml.dump({'名前': '山田'}, allow_unicode=True)
encoding `stream` にバイナリモードで書き込む際のエンコーディングを指定します。デフォルトは `None` (UTF-8が使われることが多い)。 with open('out.yaml', 'wb') as f: yaml.dump(data, stream=f, encoding='utf-8')
explicit_start `True` にすると、ドキュメントの先頭に `—` を出力します。 yaml.dump(data, explicit_start=True)
explicit_end `True` にすると、ドキュメントの末尾に `…` を出力します。 yaml.dump(data, explicit_end=True)
version 出力するYAMLバージョンを指定します。タプルで `(1, 1)` や `(1, 2)` のように指定します。デフォルトは `None`。 yaml.dump(data, version=(1, 2))
tags カスタムタグのマッピングを指定します。 `tags={‘!person’: my_person_tag}` (やや高度)
sort_keys `True` (デフォルト)にすると、マッピング(辞書)のキーをアルファベット順にソートして出力します。`False` にすると、Python 3.7+ での挿入順(またはそれ以前のバージョンの不定順)で出力します。 yaml.dump(data, sort_keys=False)
import yaml
from datetime import date

data_to_dump = {
    'project': 'PyYAML Example',
    'version': 1.2,
    'release_date': date(2025, 4, 6),
    'maintainers': [
        {'name': 'Alice', 'email': 'alice@example.com'},
        {'name': 'Bob', 'email': 'bob@example.com'}
    ],
    'description': "これはPyYAMLのdumpオプションを示すための\n複数行にわたる説明文です。",
    'settings': {'debug': False, 'log_level': 'INFO'}
}

print("--- デフォルト設定 ---")
print(yaml.dump(data_to_dump))

print("\n--- カスタム設定 (ブロック、インデント4、キーソートなし、Unicode許可) ---")
print(yaml.dump(data_to_dump,
                default_flow_style=False,
                indent=4,
                sort_keys=False,
                allow_unicode=True))

print("\n--- さらにカスタム設定 (上記 + 開始/終了マーカー、幅指定) ---")
print(yaml.dump(data_to_dump,
                default_flow_style=False,
                indent=4,
                sort_keys=False,
                allow_unicode=True,
                explicit_start=True,
                explicit_end=True,
                width=60)) # 幅を狭くしてみる

4.2. `Loader` と `Dumper` のカスタマイズ

PyYAMLでは、`Loader` クラスと `Dumper` クラスを継承して、YAMLの解析や生成の挙動をカスタマイズできます。これにより、特定のタグを持つデータを特定のPythonオブジェクトに変換したり(カスタムコンストラクタ)、特定のPythonオブジェクトを特定のYAML形式で出力したり(カスタムリプレゼンタ)することが可能になります。

カスタムコンストラクタ (安全なデシリアライズ)

`yaml.safe_load()` を使いつつ、特定のタグ(例: `!Point`)を持つYAMLデータをカスタムクラスのインスタンス(例: `Point`クラス)に安全に変換する方法です。

import yaml

# カスタムクラス
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

# カスタムコンストラクタ関数
# loader: 現在のLoaderインスタンス
# node: 解析中のYAMLノード (ここでは !Point タグを持つマッピングノード)
def point_constructor(loader, node):
    # マッピングノードの内容をPythonの辞書として取得
    value = loader.construct_mapping(node, deep=True)
    # 辞書の値を使ってPointオブジェクトを生成
    return Point(value['x'], value['y'])

# SafeLoader にカスタムコンストラクタを登録
yaml.add_constructor('!Point', point_constructor, Loader=yaml.SafeLoader)

# カスタムタグを含むYAMLデータ
yaml_with_tag = """
start: !Point {x: 10, y: 20}
end: !Point
  x: 100
  y: 200
other_data: [1, 2, 3]
"""

# safe_load で読み込む
try:
    data = yaml.safe_load(yaml_with_tag)
    print("カスタムコンストラクタによるデシリアライズ結果:")
    print(data)
    print(f"Start point object: {data['start']}, Type: {type(data['start'])}")
    print(f"End point object: {data['end']}, Type: {type(data['end'])}")
except yaml.YAMLError as e:
    print(f"YAML解析エラー: {e}")

このように `add_constructor` を使うことで、`safe_load` の安全性を保ちながら、特定の構造を持つデータを目的のPythonオブジェクトにマッピングできます。

カスタムリプレゼンタ (カスタムシリアライズ)

特定のPythonオブジェクト(例: `Point`クラス)を特定のYAML形式(例: `!Point`タグ付きマッピング)で出力する方法です。

import yaml

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Point(x={self.x}, y={self.y})"

# カスタムリプレゼンタ関数
# dumper: 現在のDumperインスタンス
# data: シリアライズ対象のPythonオブジェクト (ここでは Point インスタンス)
def point_representer(dumper, data):
    # Pointオブジェクトを {x: ..., y: ...} の形式のマッピングノードとして表現
    mapping = {'x': data.x, 'y': data.y}
    # !Point タグを付けてマッピングノードを返す
    return dumper.represent_mapping('!Point', mapping)

# Dumper (または SafeDumper) にカスタムリプレゼンタを登録
yaml.add_representer(Point, point_representer, Dumper=yaml.SafeDumper) # SafeDumperでも登録可能

# Pointオブジェクトを作成
point1 = Point(5, -5)
point2 = Point(0, 0)

data_to_dump = {
    'figure': 'line',
    'points': [point1, point2]
}

# SafeDumper を使って dump する
try:
    yaml_output = yaml.dump(data_to_dump, Dumper=yaml.SafeDumper, default_flow_style=False, sort_keys=False)
    print("カスタムリプレゼンタによるシリアライズ結果:")
    print(yaml_output)
except yaml.YAMLError as e:
    print(f"YAML書き込みエラー: {e}")

# 出力例:
# figure: line
# points:
# - !Point
#   x: 5
#   y: -5
# - !Point
#   x: 0
#   y: 0

# 上記で生成したYAMLは、前の例のカスタムコンストラクタを登録したSafeLoaderで読み込めます
try:
    reloaded_data = yaml.safe_load(yaml_output)
    print("\n再読み込みしたデータ:")
    print(reloaded_data)
    print(f"Reloaded point1: {reloaded_data['points'][0]}, Type: {type(reloaded_data['points'][0])}")
except yaml.YAMLError as e:
    print(f"YAML解析エラー: {e}")

`add_representer` を使うことで、PythonオブジェクトがYAMLにどのように変換されるかを細かく制御できます。

4.3. エイリアスとアンカー (`&`, `*`)

YAMLには、ドキュメント内で繰り返し現れるノード(値やコレクション)を効率的に記述するための「アンカー (`&`)」と「エイリアス (`*`)」という機能があります。

  • アンカー (`&anchor_name`): あるノードに名前(アンカー名)を付けます。
  • エイリアス (`*anchor_name`): アンカー名を使って、そのノードへの参照を作成します。

これにより、同じデータを何度も書く手間が省け、ファイルサイズも削減できます。また、データの一貫性を保つのにも役立ちます。PyYAMLはアンカーとエイリアスを正しく解釈し、参照されている同じPythonオブジェクトを構築します。

# anchors.yaml
defaults: &defaults # アンカー &defaults を定義
  adapter: postgresql
  host: localhost

development:
  database:
    <<: *defaults # エイリアス *defaults を使って defaults の内容を展開・マージ
    database: myapp_dev

test:
  database:
    <<: *defaults # 同じく *defaults を参照
    database: myapp_test

user_data:
  common_address: &addr # アンカー &addr を定義
    street: 123 Main St
    city: Anytown
  user1:
    name: Alice
    address: *addr # エイリアス *addr を参照
  user2:
    name: Bob
    address: *addr # 同じく *addr を参照

上記のYAMLを `yaml.safe_load()` で読み込むと、`development` と `test` の `database` は `defaults` の内容を継承し、`user1` と `user2` の `address` は同一のPython辞書オブジェクトを参照します。

import yaml

try:
    with open('anchors.yaml', 'r', encoding='utf-8') as file:
        data = yaml.safe_load(file)
        print("アンカーとエイリアスを含むYAMLの読み込み結果:")
        # print(data) # 全体を出力すると少し長いのでコメントアウト

        print("\nDevelopment Database:")
        print(data['development']['database'])

        print("\nTest Database:")
        print(data['test']['database'])

        print("\nUser1 Address:")
        print(data['user_data']['user1']['address'])

        print("\nUser2 Address:")
        print(data['user_data']['user2']['address'])

        # user1とuser2のアドレスが同じオブジェクトか確認
        is_same_object = data['user_data']['user1']['address'] is data['user_data']['user2']['address']
        print(f"\nUser1とUser2のアドレスは同じオブジェクトか? -> {is_same_object}")

        # マージの確認 (<<: *defaults)
        # development.database は defaults のキーを含み、databaseキーが上書きされている
        print(f"\nDevelopment DB host: {data['development']['database']['host']}") # defaultsから継承
        print(f"Development DB name: {data['development']['database']['database']}") # developmentで定義

except FileNotFoundError:
    print("エラー: anchors.yamlが見つかりません。")
except yaml.YAMLError as e:
    print(f"YAMLの解析エラー: {e}")

# PyYAMLでアンカーとエイリアスを出力する
# 同じオブジェクトを複数回dumpすると、PyYAMLが自動的にアンカーとエイリアスを使うことがある
common_settings = {'timeout': 30, 'retry': 3}
config = {
    'service1': common_settings,
    'service2': common_settings, # 同じオブジェクトを参照
    'service3': {'timeout': 60, 'retry': 5} # 別のオブジェクト
}

print("\nアンカー/エイリアスを含むYAMLの出力:")
print(yaml.dump(config, default_flow_style=False, sort_keys=False))
# 出力例:
# service1: &id001
#   retry: 3
#   timeout: 30
# service2: *id001
# service3:
#   retry: 5
#   timeout: 60
# (アンカー名は PyYAML が自動生成します)

💡 マージキー (`<<`): 上の例の `<<: *defaults` は、YAMLのマージキーと呼ばれる特殊なキーです。エイリアスで指定されたマッピングの内容を現在のマッピングにマージ(取り込み)します。もしキーが重複する場合は、現在のマッピングの値が優先されます。

5.1. `yaml.load` の危険性と `safe_load` の徹底

繰り返しになりますが、これが最も重要な注意点です。信頼できない入力に対して `yaml.load()` を使用しないでください。 常に `yaml.safe_load()` を使用することを習慣づけてください。これにより、任意のコード実行の脆弱性を防ぐことができます。

もし既存のコードで `yaml.load()` が使われている場合は、`yaml.safe_load()` に置き換えるか、少なくとも `Loader=yaml.SafeLoader` を明示的に指定するように修正することを強く推奨します。

import yaml

# 非推奨: Loader指定なし (PyYAML 5.1以降は警告が出る)
# data = yaml.load(stream)

# 非推奨: 安全でないLoaderの使用
# data = yaml.load(stream, Loader=yaml.FullLoader) # CVE-2020-1747, CVE-2020-14343 の可能性
# data = yaml.load(stream, Loader=yaml.UnsafeLoader)
# data = yaml.load(stream, Loader=yaml.Loader) # 古いバージョンのデフォルト、非常に危険

# 推奨: safe_load を使う
data_safe1 = yaml.safe_load(stream)

# 推奨: load に SafeLoader を明示的に指定する
data_safe2 = yaml.load(stream, Loader=yaml.SafeLoader)

5.2. バージョンによる違い

PyYAMLも他のライブラリと同様に、バージョンアップによって挙動が変わることがあります。

  • PyYAML 5.1 (2019年頃): `yaml.load()` のデフォルトローダーが変更され、`Loader`引数なしでの呼び出し時に警告 (YAMLLoadWarning) が出るようになりました。`FullLoader` や `UnsafeLoader` が導入されました。
  • PyYAML 5.4 (2021年頃): `FullLoader` に存在した脆弱性 (CVE-2020-1747, CVE-2020-14343) が修正されました。
  • PyYAML 6.0 (2021年頃): Python 2.7 および 3.5 のサポートが終了しました。`load()` で `Loader` 引数が必須になりました。

特に古いバージョンのPyYAMLを使用している場合は、セキュリティ上のリスクがある可能性があるため、最新版へのアップデートを検討してください。プロジェクトの依存関係としてバージョンを固定している場合は、使用しているバージョンに既知の脆弱性がないか確認することが重要です。

5.3. エラーハンドリング (`yaml.YAMLError`)

YAMLデータの読み書き時には、様々なエラーが発生する可能性があります(例: ファイルが見つからない、YAMLの構文が間違っている、不正なデータ型が含まれている)。これらのエラーを適切に処理するために、`try…except` ブロックを使用し、`yaml.YAMLError` (およびそのサブクラス) をキャッチするようにしましょう。

import yaml

invalid_yaml = """
key1: value1
key2: [item1, item2
key3: value3 # ここで構文エラー (括弧が閉じていない)
"""

try:
    data = yaml.safe_load(invalid_yaml)
    print("データ:", data)
# except FileNotFoundError as e: # ファイル読み込みの場合
#    print(f"ファイルエラー: {e}")
except yaml.YAMLError as e:
    print(f"YAML解析エラーが発生しました:")
    if hasattr(e, 'problem_mark'):
        mark = e.problem_mark
        print(f"  場所: 行 {mark.line + 1}, 列 {mark.column + 1}")
    if hasattr(e, 'problem'):
        print(f"  問題: {e.problem}")
    if hasattr(e, 'context'):
        print(f"  コンテキスト: {e.context}")
except Exception as e:
    print(f"予期せぬエラー: {e}")

`YAMLError` オブジェクトは、エラーが発生した場所(行番号、列番号)などの詳細情報を持っていることがあるため、デバッグに役立ちます。

5.4. パフォーマンスに関する考慮事項

非常に大きなYAMLファイルを扱う場合、パフォーマンスが問題になることがあります。

  • LibYAMLの使用: 可能であれば、C言語実装のLibYAMLバインディングを使用すると、純粋なPython実装よりも大幅に高速になります。通常、PyYAMLはインストール時に自動で検出しようとしますが、`yaml.CLoader` / `yaml.CSafeLoader` / `yaml.CDumper` / `yaml.CSafeDumper` を明示的に指定することで確実に使用できます。
  • `load_all`/`dump_all`: 巨大な単一ドキュメントではなく、複数の小さなドキュメントに分割できる場合は、`load_all` で逐次処理することを検討してください。メモリ使用量を抑えられます。
  • 代替フォーマットの検討: 極端にパフォーマンスが要求される場合や、バイナリデータを含む場合は、JSON、MessagePack、Protocol Buffersなど、他のシリアライゼーションフォーマットの方が適している可能性があります。
import yaml

# CLoader/CDumperが利用可能か確認して使用する例
try:
    from yaml import CLoader as Loader, CDumper as Dumper, CSafeLoader as SafeLoader, CSafeDumper as SafeDumper
    print("LibYAMLバインディング (C拡張) を使用します。")
except ImportError:
    from yaml import Loader, Dumper, SafeLoader, SafeDumper
    print("純粋なPython実装を使用します。")

# 使用例
try:
    with open('config.yaml', 'r', encoding='utf-8') as file:
        # 安全なCローダーを使用
        config_data = yaml.load(file, Loader=SafeLoader)

    python_data = {'key': 'value'}
    with open('output_c.yaml', 'w', encoding='utf-8') as file:
        # 安全なCダンパーを使用
        yaml.dump(python_data, file, Dumper=SafeDumper, default_flow_style=False)

except FileNotFoundError:
    print("ファイルエラー")
except yaml.YAMLError as e:
    print(f"YAMLエラー: {e}")
except NameError:
    print("Loader/Dumperのインポートに失敗しました。PyYAMLが正しくインストールされていない可能性があります。")

5.5. 代替ライブラリとの比較 (ruamel.yaml など)

PyYAMLは広く使われていますが、いくつかの代替ライブラリも存在します。代表的なものに ruamel.yaml があります。

特徴 PyYAML ruamel.yaml
YAMLバージョン 主に 1.1 をサポート (一部 1.2 の要素も認識) YAML 1.2 を完全にサポート
コメントの保持 基本的にコメントは破棄される `round_trip_load`/`round_trip_dump` を使うことで、コメントや書式を保持したまま読み書きが可能
キーの順序 `dump`時に `sort_keys=False` で挿入順を保持 (Python 3.7+)。読み込み時はデフォルトで辞書になるため順序は保証されない(Python 3.7+ では挿入順)。 デフォルトでキーの順序を保持する (内部で `CommentedMap` を使用)
API 比較的シンプル (`load`, `dump`, `safe_load`, `safe_dump` など) PyYAMLに似ているが、より多機能で洗練されたAPI (`YAML().load()`, `YAML().dump()`, `round_trip_load`, `round_trip_dump` など)
開発状況 安定しているが、`ruamel.yaml` ほど頻繁なアップデートはない より活発に開発されており、新機能の追加や改善が継続的に行われている
安全性 `safe_load` の使用が強く推奨される。`load` には脆弱性の歴史がある。 デフォルトの `load` でも警告が出る。安全な `safe_load` が推奨される点は同様。API設計は安全性をより意識している傾向。

どちらを選ぶか?

  • 単純なデータの読み書きや、既存の多くのツールとの互換性を重視する場合は、PyYAML で十分なことが多いでしょう。
  • YAMLファイルのコメントや書式、キーの順序を保持したまま編集・更新したい場合や、最新のYAML 1.2 仕様に準拠したい場合は、ruamel.yaml が非常に強力な選択肢となります。

他にも `StrictYAML` のように、より厳密なスキーマ検証や安全性を重視したライブラリも存在します。用途に応じて適切なライブラリを選択することが重要です。

このブログ記事では、PythonでYAMLを扱うための標準的なライブラリであるPyYAMLについて、基本的な使い方からデータ型の扱い、高度な機能、そして最も重要なセキュリティ上の注意点まで詳しく解説しました。

キーポイントのおさらい:

  • YAMLは人間が読み書きしやすいデータフォーマット。
  • PyYAMLはPythonでYAMLを扱うためのライブラリ。
  • インストールは `pip install pyyaml`。
  • 読み込みは `yaml.safe_load()` を使う (`yaml.load()` は危険!)。
  • 書き込みは `yaml.dump()` を使う (オプションで出力形式を制御可能)。
  • リスト、辞書、基本的なデータ型、ネスト構造を扱える。
  • 複数ドキュメントは `safe_load_all()` / `dump_all()`。
  • アンカー (`&`) とエイリアス (`*`) で繰り返しを避けられる。
  • カスタムコンストラクタ/リプレゼンタで挙動を拡張できる (安全な実装を心がける)。
  • エラーハンドリング (`yaml.YAMLError`) を行う。
  • パフォーマンスが必要な場合はLibYAMLや代替ライブラリ (ruamel.yaml) を検討する。

PyYAMLを正しく理解し、安全に利用することで、設定ファイルの管理やデータ交換がより効率的かつ堅牢になります。ぜひ、あなたのPythonプロジェクトでPyYAMLを活用してみてください! 😊🐍