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

どちらを使うべきか?

機能PyYAMLruamel.yaml
YAMLバージョンサポート1.11.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を効果的に使いこなし、よりクリーンで管理しやすいコードを目指しましょう!