PythonistaのためのYAML徹底解説:PyYAMLとruamel.yamlを使いこなす

YAML (YAML Ain’t Markup Language) は、設定ファイルやデータ交換フォーマットとして広く使われている、人間にとって読み書きしやすいデータシリアライズ形式です。特にPythonとの親和性が高く、多くのプロジェクトで活用されています。 このブログ記事では、PythonでYAMLを扱うための主要なライブラリであるPyYAMLruamel.yamlについて、基本的な使い方から高度なテクニック、そして注意点まで詳しく解説します。 📄

1. YAMLとは?なぜPythonで使うのか?

YAMLは、JSONやXMLと同様に構造化されたデータを表現するためのフォーマットですが、インデント(字下げ)によって階層構造を示す点が大きな特徴です。これにより、括弧やタグが少なく、非常にシンプルで読みやすい形式となっています。

Pythonのコードブロックもインデントで構造を示すため、YAMLの形式と非常に似ており、Pythonプログラマーにとっては直感的に理解しやすいでしょう。設定ファイルとしてYAMLを使うことで、複雑な設定もすっきりと記述でき、管理が容易になります。また、異なるプログラミング言語間でのデータ交換にも利用されます。

YAMLの主な特徴:

  • ✅ 人間にとって読み書きしやすい
  • ✅ インデントによる階層表現
  • ✅ コメントが書ける(# で始まる行)
  • ✅ リスト(シーケンス)や辞書(マッピング)を表現可能
  • ✅ 豊富なデータ型(文字列、数値、真偽値、null、日付など)
  • ✅ 1つのファイルに複数のドキュメントを含めることができる(--- で区切る)

PythonでYAMLを扱うことで、これらのメリットを活かし、設定管理やデータ処理を効率化できます。

2. Pythonの主要YAMLライブラリ:PyYAML vs ruamel.yaml

PythonでYAMLを扱うためのライブラリはいくつかありますが、特に有名なのがPyYAMLruamel.yamlです。

PyYAML (pyyaml)

古くから存在する、最も広く使われているYAMLライブラリです。YAML 1.1仕様をサポートしており、基本的なYAMLファイルの読み書き機能を提供します。シンプルで使いやすいAPIが特徴ですが、いくつかの注意点もあります(後述)。

インストール:

pip install pyyaml

ruamel.yaml

PyYAMLのフォーク(派生版)として開発されたライブラリで、YAML 1.2仕様をサポートしています。PyYAMLのいくつかの問題を改善しており、特に以下の点で優れています。

  • ✅ コメントの保持: YAMLファイル内のコメントを読み書き時に保持できます。
  • ✅ 順序の保持: 辞書(マッピング)のキーの順序を保持します。
  • ✅ よりYAML 1.2仕様に準拠した動作

設定ファイルをプログラムで編集し、元のフォーマット(コメントや順序)を極力維持したい場合に非常に強力です。

インストール:

pip install ruamel.yaml

どちらを使うべきか?

機能 PyYAML ruamel.yaml
YAMLバージョンサポート 1.1 1.2
基本的な読み書き ✅ 可能 ✅ 可能
コメント保持 ❌ 不可 ✅ 可能 (typ='rt' (round-trip) モード)
キーの順序保持 ❌ 保証されない ✅ 可能 (typ='rt' モード)
活発な開発 △ (安定版) ✅ (継続的に更新)
依存関係 (オプションでLibYAML) (オプションでruamel.yaml.clib)

シンプルな読み書きだけであればPyYAMLでも十分ですが、設定ファイルの編集や、元の構造を保持したい場合はruamel.yamlの利用を強く推奨します。この記事では、主にPyYAMLの基本的な使い方を解説しつつ、ruamel.yamlの利点についても触れていきます。

3. PyYAMLの基本的な使い方

3.1 YAMLファイルの読み込み (load / safe_load)

YAMLファイルをPythonオブジェクト(主に辞書やリスト)に変換するには、yaml.load()またはyaml.safe_load()関数を使用します。

⚠️ セキュリティ警告:load()の代わりにsafe_load()を使う

yaml.load()関数は、信頼できないソースからのYAMLデータを読み込む際に非常に危険です。これは、YAMLの仕様上、任意のPythonオブジェクトを構築したり、任意のコードを実行したりすることが可能だからです。過去にこの機能が悪用された脆弱性 (例: CVE-2017-18342, CVE-2020-14343など) が報告されています。

必ずyaml.safe_load()を使用してください。 これは、安全なYAMLのサブセットのみを読み込み、危険な機能(任意のコード実行など)を無効にします。PyYAML 5.1以降では、load()を使う際にLoader引数の指定が必須となり、指定しない場合は警告が表示されますが、それでもsafe_load()を使うのが最も安全です。

例:config.yaml

# 設定ファイル例
database:
  host: localhost
  port: 5432
  user: admin
  password: "secure_password" # これは例です。実際のパスワードは安全に管理してください。

api_settings:
  url: "https://api.example.com/v1"
  timeout: 30 # seconds
  retry_attempts: 3

feature_flags:
  new_dashboard: true
  experimental_feature: false
  user_groups:
    - alpha
    - beta

Pythonコード (読み込み):

import yaml
from pathlib import Path

# YAMLファイルのパス
yaml_file_path = Path('config.yaml')

try:
    with open(yaml_file_path, 'r', encoding='utf-8') as f:
        # 安全なsafe_loadを使用
        config_data = yaml.safe_load(f)

    # 読み込んだデータを表示・利用
    print("--- 設定データ全体 ---")
    print(config_data)

    print("\n--- 個別の値へのアクセス ---")
    db_host = config_data['database']['host']
    api_timeout = config_data['api_settings']['timeout']
    user_groups = config_data['feature_flags']['user_groups']

    print(f"データベースホスト: {db_host}")
    print(f"APIタイムアウト: {api_timeout}")
    print(f"ユーザーグループ: {user_groups}")

except FileNotFoundError:
    print(f"エラー: ファイル '{yaml_file_path}' が見つかりません。")
except yaml.YAMLError as e:
    print(f"エラー: YAMLファイルの解析中にエラーが発生しました。 - {e}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

safe_load()はYAMLの内容を解析し、対応するPythonのデータ構造(この場合はネストした辞書とリスト)を返します。キーを使って値にアクセスできます。ファイルの文字コードはencoding='utf-8'のように指定するのが一般的です。

文字列からの読み込み

safe_load()はファイルオブジェクトだけでなく、YAML形式の文字列も直接読み込めます。

import yaml

yaml_string = """
- item1
- item2
- sublist:
    - subitem A
    - subitem B
"""

data_from_string = yaml.safe_load(yaml_string)
print(data_from_string)

3.2 PythonオブジェクトのYAMLファイルへの書き込み (dump)

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

Pythonコード (書き込み):

import yaml
from pathlib import Path

# 書き込むデータ (Pythonの辞書)
output_data = {
    'server': {
        'ip_address': '192.168.1.100',
        'port': 8080,
        'enabled': True,
        'services': ['web', 'database', 'cache']
    },
    'metadata': {
        'version': '1.2.3',
        'description': 'サーバー設定データ' # 日本語を含む例
    }
}

# 出力ファイルパス
output_yaml_path = Path('output.yaml')

try:
    with open(output_yaml_path, 'w', encoding='utf-8') as f:
        # dump関数で書き込み
        yaml.dump(
            output_data,
            f,
            allow_unicode=True,     # 日本語などの非ASCII文字をそのまま出力
            default_flow_style=False, # ブロックスタイル (インデント形式) を強制
            sort_keys=False         # キーの順序を保持しようと試みる (PyYAMLでは保証されない)
        )
    print(f"データを '{output_yaml_path}' に書き込みました。")

except Exception as e:
    print(f"ファイルの書き込み中にエラーが発生しました: {e}")

dump()の主なオプション:

  • stream: 書き込み先のファイルオブジェクト。省略すると文字列として結果を返す。
  • allow_unicode=True: Unicode文字(日本語など)を\uXXXXのようなエスケープシーケンスではなく、そのまま出力します。通常はTrueを指定するのが良いでしょう。
  • default_flow_style=False: リストや辞書をフロースタイル(例: [item1, item2], {key: value})ではなく、ブロックスタイル(インデントを使った形式)で出力します。可読性のためにFalseが推奨されることが多いです。PyYAMLはデフォルトで、ネストされたコレクションがない場合はフロースタイルを選択することがあります。
  • sort_keys=False: 辞書のキーをソートせずに出力しようと試みます。ただし、PyYAMLはPython 3.6以前の辞書のように順序を保証しないため、意図した順序にならない場合があります。順序保持が必要な場合はruamel.yamlを使用します。
  • indent=N: インデントのスペース数を指定します(デフォルトは2)。

3.3 データ型:スカラー、シーケンス、マッピング

PyYAMLはYAMLの基本的なデータ構造をPythonの型にマッピングします。

  • スカラー (Scalar): 文字列、数値 (整数、浮動小数点数)、真偽値 (True, False)、None (YAMLのnull~に対応)
    • 注意: YES/NO, ON/OFFなども真偽値として解釈されることがあります(”The Norway Problem”として知られる)。文字列として扱いたい場合はクォーテーションで囲む必要があります(例: 'NO')。
  • シーケンス (Sequence): 順序付けられた要素のリスト。Pythonのlistに対応します。
    • ブロックスタイル: - item
    • フロースタイル: [item1, item2]
  • マッピング (Mapping): キーと値のペアのコレクション。Pythonのdictに対応します。
    • ブロックスタイル: key: value
    • フロースタイル: {key1: value1, key2: value2}
import yaml

yaml_data_types = """
string_example: Hello, YAML!
quoted_string: "Special characters: & * #" # クォートで囲むと特殊文字も扱いやすい
integer_example: 123
float_example: 3.14159
boolean_true: true
boolean_false: No # これは False になるので注意!文字列なら 'No' と書く
null_example: null # または ~
list_example:
  - apple
  - banana
  - orange
dictionary_example:
  name: Alice
  age: 30
  city: Tokyo
nested_structure:
  - key: item1
    value: 100
  - key: item2
    value: [a, b, c]
"""

parsed_data = yaml.safe_load(yaml_data_types)
print(parsed_data)

# データ型の確認
print(f"boolean_falseの型: {type(parsed_data['boolean_false'])}") # 
print(f"list_exampleの型: {type(parsed_data['list_example'])}")   # 
print(f"dictionary_exampleの型: {type(parsed_data['dictionary_example'])}") # 
print(f"null_exampleの型: {type(parsed_data['null_example'])}")     # 

3.4 複数ドキュメントの扱い

YAMLファイルは---区切りで複数のドキュメントを含むことができます。PyYAMLでこれらを読み込むにはyaml.safe_load_all()を使用します。これはジェネレータを返すため、ループで各ドキュメントを処理します。

例:multi_doc.yaml

# ドキュメント1
document: 1
type: config
---
# ドキュメント2
document: 2
type: data
payload:
  - id: 1
    value: foo
  - id: 2
    value: bar
---
# ドキュメント3: シンプルなリスト
- item_a
- item_b

Pythonコード:

import yaml
from pathlib import Path

multi_doc_path = Path('multi_doc.yaml')

try:
    with open(multi_doc_path, 'r', encoding='utf-8') as f:
        documents = yaml.safe_load_all(f)
        print("--- 複数ドキュメントの読み込み ---")
        for i, doc in enumerate(documents):
            print(f"\nドキュメント {i+1}:")
            print(doc)

except FileNotFoundError:
    print(f"エラー: ファイル '{multi_doc_path}' が見つかりません。")
except yaml.YAMLError as e:
    print(f"エラー: YAMLファイルの解析中にエラーが発生しました。 - {e}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

同様に、複数のPythonオブジェクトをyaml.dump_all()で1つのファイルに書き込めます。

4. ruamel.yamlによる高度な操作

ruamel.yamlは、PyYAMLの基本的な機能に加え、より高度な操作やYAML 1.2準拠の機能を提供します。特に「Round Trip」モード (typ='rt') が強力です。

4.1 コメントと順序の保持 (Round Trip)

設定ファイルを読み込み、一部を変更して書き戻す際に、元のコメントやキーの順序が失われると非常に不便です。ruamel.yamlのRound Tripモードはこれを解決します。

from ruamel.yaml import YAML
from pathlib import Path
import sys

yaml_file_with_comments = Path('config_with_comments.yaml')

# コメント付きのYAMLファイルを作成 (例)
yaml_content = """
# データベース設定
database:
  host: old_host.example.com # ホスト名 (変更予定)
  port: 5432

# APIエンドポイント
api:
  url: https://api.example.com
"""
yaml_file_with_comments.write_text(yaml_content, encoding='utf-8')


# ruamel.yamlのYAMLインスタンスを作成 (Round Tripモード)
yaml = YAML() # typ='rt'がデフォルト
yaml.preserve_quotes = True # クォートを保持
yaml.indent(mapping=2, sequence=4, offset=2) # インデント設定 (例)

try:
    # ファイルを読み込む (コメントや順序が保持される)
    with open(yaml_file_with_comments, 'r', encoding='utf-8') as f:
        data = yaml.load(f)

    # データを変更
    data['database']['host'] = 'new_host.internal.local'
    data['api']['timeout'] = 60 # 新しいキーを追加

    # 変更後のデータを書き戻す (コメントや順序は維持される)
    print("--- 変更後のYAML (標準出力へ) ---")
    yaml.dump(data, sys.stdout)

    # ファイルに書き戻す場合
    output_path = Path('config_updated.yaml')
    with open(output_path, 'w', encoding='utf-8') as f:
         yaml.dump(data, f)
    print(f"\n\n変更を '{output_path}' に書き込みました。")


except Exception as e:
    print(f"エラーが発生しました: {e}")

このコードを実行すると、config_updated.yamlには元のコメントが残り、databaseセクションのhostが更新され、apiセクションにtimeoutが追加されたYAMLが出力されます。キーの順序も維持されます。これはPyYAMLでは実現困難な機能です。

4.2 YAML 1.2 対応と細かい挙動の違い

ruamel.yamlはYAML 1.2をサポートしているため、PyYAML (YAML 1.1) とは一部の解釈が異なる場合があります。

  • 8進数: YAML 1.1では012は8進数の10と解釈されますが、YAML 1.2では単なる数値の12です。ruamel.yaml (pure Pythonモード) は1.2の挙動に従います (0o12のように明示的に書くのが1.2流)。
  • 真偽値: YAML 1.2ではtrue/falseのみが標準の真偽値です (Yes/No, On/Offは非標準)。
  • Unicode: より厳密なUnicodeサポート。

基本的な使い方では大きな違いはありませんが、細かい仕様に依存するケースではruamel.yamlの方がより標準に準拠していると言えます。

5. 注意点とベストプラクティス

5.1 ⚠️ セキュリティ: safe_load を常に使う

繰り返しになりますが、最も重要な注意点です。外部から受け取る可能性のあるYAMLデータ(設定ファイル、APIレスポンスなど)を読み込む際は、必ず yaml.safe_load() (PyYAML) または YAML(typ='safe') (ruamel.yaml) を使用してください。

load() (特に古いPyYAMLやLoader指定なし) は、悪意のあるコード実行 (例: CVE-2020-14343) に繋がる深刻なセキュリティリスクがあります。2017年頃からこの問題は広く認識されるようになり、多くのプロジェクトでsafe_loadへの移行が進みました。

信頼できないYAMLデータに対してload()を使用することは絶対に避けてください。

5.2 エンコーディング

YAMLファイルはテキストファイルであり、文字エンコーディングの問題が発生することがあります。特に日本語を含む場合、UTF-8で保存・読み書きするのが一般的です。

# 読み込み時
with open('config.yaml', 'r', encoding='utf-8') as f:
    data = yaml.safe_load(f)

# 書き込み時
with open('output.yaml', 'w', encoding='utf-8') as f:
    yaml.dump(data, f, allow_unicode=True)

ファイルを扱う際は、常にencoding='utf-8'を指定することを推奨します。もし他のエンコーディング(Shift_JISなど)のファイルを読む必要がある場合は、適切に指定してください。不明な場合は、複数のエンコーディングを試すエラーハンドリングが必要になることもあります。

5.3 YAMLのバージョン

前述の通り、PyYAMLは主にYAML 1.1、ruamel.yamlはYAML 1.2をサポートします。両バージョン間には細かい非互換性があります。使用するツールやライブラリがどのバージョンを期待しているかを確認することが重要です。例えば、AWS CloudFormationはYAML 1.1をサポートしています。

5.4 エラーハンドリング

ファイルの読み書きやYAMLの解析中には様々なエラーが発生する可能性があります。

  • FileNotFoundError: 指定されたファイルが存在しない。
  • yaml.YAMLError: YAMLの構文エラー(インデントミス、不正な形式など)。
    • yaml.parser.ParserError, yaml.scanner.ScannerError など、より具体的なエラークラスもあります。
  • UnicodeDecodeError: 指定されたエンコーディングでファイルを読み取れない。
  • PermissionError: ファイルへのアクセス権がない。

適切なtry...exceptブロックを使って、これらのエラーを捕捉し、ユーザーに分かりやすいメッセージを表示したり、代替処理を行ったりするようにしましょう。

import yaml
from pathlib import Path

file_path = Path('potentially_invalid.yaml')

try:
    with open(file_path, 'r', encoding='utf-8') as f:
        data = yaml.safe_load(f)
    # 成功した場合の処理
    print("YAMLの読み込みに成功しました。")
    # print(data)

except FileNotFoundError:
    print(f"エラー: ファイル '{file_path}' が見つかりません。")
except yaml.YAMLError as e:
    # YAMLErrorは構文エラーなどを含む基底クラス
    print(f"エラー: YAMLファイルの形式が正しくありません。")
    # エラーの詳細情報をログに出力するなど
    print(f"詳細: {e}")
    # 必要であれば、エラーが発生した行番号などを取得することも可能
    if hasattr(e, 'problem_mark'):
        mark = e.problem_mark
        print(f"エラー箇所 (おおよそ): 行 {mark.line + 1}, 列 {mark.column + 1}")
except UnicodeDecodeError:
    print(f"エラー: ファイル '{file_path}' の文字コードがUTF-8ではない可能性があります。")
except PermissionError:
     print(f"エラー: ファイル '{file_path}' へのアクセス権がありません。")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

5.5 クォートの扱い

YAMLでは多くの場合、文字列をクォート(' or ")で囲む必要はありません。しかし、以下のような場合はクォートが必要です。

  • 値が特殊文字 (:, {, }, [, ], ,, &, *, #, ?, |, -, <, >, =, !, %, @, `) で始まる場合。
  • 値が数値や真偽値 (true, false, 123, 1.23, Yes, No など) と解釈される可能性があり、それを文字列として扱いたい場合 (例: '123', 'No')。
  • 値の前後に空白を含めたい場合。

シングルクォート(')内では、シングルクォート自体は '' とエスケープします。ダブルクォート(")内では、バックスラッシュ(\)によるエスケープシーケンス(\n, \", \\ など)が利用可能です。どちらを使うかは状況に応じて選択します。

6. まとめ ✨

PythonでYAMLを扱うことは、設定管理やデータ交換において非常に便利です。主要なライブラリであるPyYAMLruamel.yamlは、それぞれ特徴があります。

  • PyYAML: シンプルな読み書きには十分。ただし、safe_loadの使用を徹底すること。
  • ruamel.yaml: コメントや順序の保持が必要な場合、YAML 1.2準拠の動作が必要な場合に強力。Round Tripモードが特に便利。

どちらのライブラリを使うにしても、以下の点を意識することが重要です。

  1. セキュリティ: 常にsafe_load相当の安全な関数を使う。
  2. エンコーディング: UTF-8を基本とし、適切に指定する。
  3. エラーハンドリング: ファイルI/Oやパースエラーに備える。
  4. YAMLバージョン: ツールや環境に合わせたバージョンを意識する。

これらの知識を活用して、PythonプロジェクトでYAMLを効果的に使いこなし、よりクリーンで管理しやすいコードを目指しましょう!🐍📄🚀