YAML (YAML Ain’t Markup Language) は、設定ファイルやデータ交換フォーマットとして広く使われている、人間にとって読み書きしやすいデータシリアライズ形式です。特にPythonとの親和性が高く、多くのプロジェクトで活用されています。
このブログ記事では、PythonでYAMLを扱うための主要なライブラリであるPyYAML
とruamel.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を扱うためのライブラリはいくつかありますが、特に有名なのがPyYAML
とruamel.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
への移行が進みました。
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を扱うことは、設定管理やデータ交換において非常に便利です。主要なライブラリであるPyYAML
とruamel.yaml
は、それぞれ特徴があります。
PyYAML
: シンプルな読み書きには十分。ただし、safe_load
の使用を徹底すること。ruamel.yaml
: コメントや順序の保持が必要な場合、YAML 1.2準拠の動作が必要な場合に強力。Round Tripモードが特に便利。
どちらのライブラリを使うにしても、以下の点を意識することが重要です。
- セキュリティ: 常に
safe_load
相当の安全な関数を使う。 - エンコーディング: UTF-8を基本とし、適切に指定する。
- エラーハンドリング: ファイルI/Oやパースエラーに備える。
- YAMLバージョン: ツールや環境に合わせたバージョンを意識する。
これらの知識を活用して、PythonプロジェクトでYAMLを効果的に使いこなし、よりクリーンで管理しやすいコードを目指しましょう!🐍📄🚀