1.2 データフォーマットのPythonデータ構造へのパース

1.1 では XML・JSON・YAML が「何か」「なぜ違うか」を見た。ここからは、それらを Python で実際に読み書きする話に進む。受け取った XML・JSON・YAML は、最初はただの「長い文字列」である。そのままでは中身を1つずつ取り出せない。だから、まず Python が扱える「辞書」や「リスト」といったデータ構造に変換する。この変換が パース(解析) であり、本ページの中心テーマだ。

なぜ文字列のままでは扱えないのか

文字列のままでは「特定の値だけを取り出す」ことができないからである。だからパースを挟んで、Python のデータ構造に直す。

たとえば API から、次のような JSON が「1本の文字列」として返ってきたとする。

'{"hostname": "router1", "enabled": true}'

このとき hostname の値「router1」だけが欲しくても、文字列のままでは取り出せない。できるのは「何文字目から何文字目を切り出す」といった力技だけで、これは形が少し変わるだけで簡単に壊れる。引用符の有無や空白の入り方が違えば、たちまち動かなくなる。

そこでパースする。パースとは、フォーマットの文法に沿って文字列を読み解き、対応する Python のデータ構造を組み立てる処理である。先ほどの文字列をパースすると、次の Python の辞書(キーで値を引けるデータ構造)になる。

{"hostname": "router1", "enabled": True}

こうなれば data["hostname"] と書くだけで “router1” を取り出せる。data["enabled"]True(真偽値)として得られ、そのまま if 文の条件に使える。文字列の切り貼りではなく、データ構造として扱えるようになる——これがパースを挟む意味である。

flowchart LR
    A["受信した文字列
(JSON や YAML の書式)"] -->|パース| B["Python のデータ構造
辞書・リスト・数値など"] B -->|シリアライズ| A B --> C["キーを指定して
値を取り出す"]

書き出すときはこの逆をたどる。Python のデータ構造を、送信用・保存用の文字列に戻す。1.1 で見たとおり、この向きを シリアライズ(直列化) と呼ぶ。パースとシリアライズは、同じ橋を逆向きに渡る一組の操作である。

3形式は Python のどの型に対応するか

3形式の基本的な部品は、Python の基本的な型にほぼ素直に対応する。「キーと値の集まり」は辞書に、「値の並び」はリストに、文字列・数値・真偽値・空っぽの値もそれぞれ対応する型になる。まず対応表を見てから、ズレやすい点を補足する。

データの中身JSONYAMLPython
キーと値の集まりオブジェクト {}マッピング辞書 dict
値の並び配列 []シーケンスリスト list
文字列"router1"router1str
整数・小数10001000int / float
真偽値true / falsetrue / falseTrue / False
空っぽの値nullnull / ~None

JSON と YAML は、この対応がとても素直である。理由は 1.1 で触れたとおりで、両者がもともと「オブジェクト(辞書)」と「配列(リスト)」というプログラミング言語に共通の構造を土台にしているからだ。パースすれば、ほぼ表のとおりに辞書とリストの入れ子が得られる。

ズレやすいのは XML である。XML には「辞書」「リスト」という概念が最初からない。あるのは入れ子になったタグと属性だけだ。そのため XML をパースしても、いきなり Python の辞書やリストにはならない。代わりに「要素ツリー」(タグの親子関係を表す木構造)というオブジェクトが得られ、そこから必要な値を1つずつ取り出していく。同じ「パース」でも、JSON/YAML と XML では到達するゴールの形が違う、と意識しておくとよい。

Note

型でつまずきやすいのが真偽値と空の値だ。JSON の true は、パースすると Python では先頭が大文字の True になる。同様に nullNone(値が無いことを表す Python の特別な値)になる。文字列の "true"(引用符つき)とは別物である点に注意したい。引用符があれば、それはただの文字列であってパースしても真偽値にはならない。

読み込みと書き出しの最小コード

ここからは、同じデータを各形式から Python に読み込み、また書き出す最小の例を並べる。題材は 1.1 と同じネットワーク機器の情報を簡略化したものだ。まず JSON、次に YAML、最後に XML の順で、対応の素直なものから見ていく。

JSON(標準ライブラリだけで完結)

import json

text = '{"hostname": "router1", "enabled": true, "interfaces": ["Gi0/0", "Gi0/1"]}'

# 読み込み: 文字列 → Python のデータ構造
data = json.loads(text)
print(data["hostname"])      # router1
print(data["interfaces"][0]) # Gi0/0

# 書き出し: Python のデータ構造 → 文字列
out = json.dumps(data)

json.loads の末尾の s は string(文字列)の意味で、「文字列から読む」関数だ。逆の json.dumps は「文字列へ書き出す」。読み込めば、あとは普通の辞書・リストとして data["hostname"] のように扱える。

YAML(外部ライブラリ PyYAML が必要)

import yaml   # 事前に pip install PyYAML が必要

text = """
hostname: router1
enabled: true
interfaces:
  - Gi0/0
  - Gi0/1
"""

# 読み込み
data = yaml.safe_load(text)
print(data["hostname"])      # router1

# 書き出し
out = yaml.safe_dump(data)

使い方は JSON とよく似ている。違いは、import yaml の前に外部ライブラリ PyYAML をインストールしておく必要がある点だ。また読み込みには load ではなく safe_load を使う。理由は後述するが、「安全なほう」を選ぶ習慣をつけておくとよい。

XML(構造が辞書に直結しない)

import xml.etree.ElementTree as ET   # 標準ライブラリ

text = """
<device>
  <hostname>router1</hostname>
  <enabled>true</enabled>
</device>
"""

# 読み込み: 文字列 → 要素ツリー
root = ET.fromstring(text)
print(root.find("hostname").text)   # router1

JSON や YAML のように data["hostname"] とは書けないことに注目してほしい。root.find("hostname") でタグを探し、その .text で中身の文字列を取り出す、という手順になる。さらに <enabled>true</enabled> の中身は文字列の "true" であって、真偽値の True にはならない。1.1 で触れたとおり XML のタグの中身は基本的にすべて文字列なので、真偽値や数値として使いたければ自分で変換する必要がある。この「ひと手間」が、XML と JSON/YAML の扱いやすさの差として表れる。

なぜ json は標準で、YAML/XML は扱いが分かれるのか

理由は単純で、Python に最初から付いてくる(標準ライブラリ)かどうかが違うからである。json モジュールは標準ライブラリなので import json だけで使える。PyYAML は標準ではないので pip install で別途入れる必要がある。XML は標準ライブラリで読めるが、得られる形が辞書ではないぶん扱いに手間がかかる。

なぜ YAML は標準に入っていないのか。大きいのは、JSON が Web API の共通言語として桁違いに広く使われ、どの言語にも標準搭載される「事実上の必需品」になったからだ。一方 YAML は主に設定ファイル向けで、用途がやや絞られる。さらに YAML は仕様が豊かで、それを完全に解釈する処理は重い。こうした事情から、Python は軽い JSON だけを標準に取り込み、YAML は外部ライブラリに委ねている。

XML が「標準で読めるのに辞書にならない」のも、根は 1.1 で見た成り立ちにある。XML は属性や名前空間を持つ「文書」を表す形式で、単純なキーと値の対応に収まりきらない。だから無理に辞書へ押し込めず、タグの親子関係をそのまま表す要素ツリーとして渡す設計になっている。

Tip

YAML の読み込みで yaml.load ではなく yaml.safe_load を使うのは、安全のためである。古い load は、設定ファイルの記述から Python のオブジェクトを生成できてしまい、信頼できないファイルを読むと意図しないコードが動く恐れがある。safe_load は辞書・リスト・文字列・数値といった素直なデータ型だけに読み込みを限定する。外部から受け取ったデータを読むときは、必ず safe_load を選ぶ。

まとめ:パースは「文字列」と「データ構造」をつなぐ橋

要点はシンプルだ。受け取った文字列はパースして Python のデータ構造(多くは辞書とリストの入れ子)に直してから扱い、送るときはシリアライズして文字列に戻す。JSON は標準の json で素直に往復でき、YAML は外部の PyYAML で同じように扱え、XML は要素ツリーを介してひと手間かけて値を取り出す。フォーマットごとに窓口(ライブラリ)と到達する形は違っても、「文字列 ⇄ データ構造」という橋を渡る、という構図は共通である。


本ページはCisco DevNet Associate(200-901) Exam Topics v1.1を学習範囲の根拠として参照。文章・図表はすべて新規作成。