Python xml.etree.ElementTreeとは?XMLを読み書きする方法

xml.etree.ElementTreeは、XMLを解析して木構造として扱う標準ライブラリです。追加インストールは要りません。

長いのでimport xml.etree.ElementTree as ETと短い名前を付けるのが慣例です。


題材のXML

<?xml version="1.0" encoding="utf-8"?>
<books>
  <book id="1" lang="ja">
    <title>Python入門</title>
    <price>2800</price>
  </book>
  <book id="2" lang="en">
    <title>Fluent Python</title>
    <price>5200</price>
  </book>
</books>

読み込んで走査する

import xml.etree.ElementTree as ET

root = ET.fromstring(xml_text)
print(root.tag, len(root))

for b in root.findall("book"):
    print(b.get("id"), b.findtext("title"), b.findtext("price"))

実行結果:

books 2
1 Python入門 2800
2 Fluent Python 5200
  • fromstring() … 文字列から読む。ファイルならET.parse('f.xml').getroot()
  • .tag … タグ名、.get('属性名') … 属性、.findtext('子') … 子要素のテキスト
  • len(要素) … 子要素の数

値はすべて文字列

price = root.find("book/price")
print(repr(price.text), type(price.text).__name__)

実行結果:

'2800' str

数値に見えても文字列です。計算に使うならint()やfloat()で変換します。


find と findall と、見つからないとき

print(root.find("book").get("id"))     # 最初の1件
print(len(root.findall("book")))      # 全件
print(root.find("magazine"))          # 無いとき

実行結果:

1
2
None

find()は見つからないと None を返します。例外は飛びません。


要素の真偽値という落とし穴

if 要素:で存在を判定してはいけません。

e = root.find("book")
print("子ありの要素:", bool(e))

empty = ET.fromstring("<a/>")
print("子なしの要素:", bool(empty))

実行結果:

子ありの要素: True
子なしの要素: False

要素の真偽値は「子要素があるか」で決まります。存在するのにFalseになり得るので、判定は必ずif e is not None:と書きます。


条件で絞り込む

簡易的なXPathが使えます。

print([t.text for t in root.findall(".//title")])
print([b.get("id") for b in root.findall("book[@lang='en']")])
print(root.findtext("book[2]/title"))

実行結果:

['Python入門', 'Fluent Python']
['2']
Fluent Python

.//は階層をまたいだ検索、[@属性='値']は属性での絞り込み、[2]は1から数えた順番です。0からではありません。

ただしXPathのすべてが使えるわけではありません。関数や軸の指定は対応していないので、複雑な条件は取り出してからPython側で絞ります。


書き出す

new = ET.Element("books")
b = ET.SubElement(new, "book", {"id": "3"})
ET.SubElement(b, "title").text = "新刊"

print(ET.tostring(new, encoding="unicode"))

実行結果:

<books><book id="3"><title>新刊</title></book></books>

encoding="unicode"を付けないとbytes が返ります。ファイルに書くならET.ElementTree(new).write('out.xml', encoding='utf-8', xml_declaration=True)を使います。

改行やインデントは入りません。読みやすく整えるにはET.indent()(Python 3.9以降)を使います。


壊れたXMLはエラーになる

try:
    ET.fromstring("<a><b></a>")
except ET.ParseError as e:
    print(type(e).__name__, "->", e)

実行結果:

ParseError -> mismatched tag: line 1, column 8

行と列を教えてくれるので、どこで壊れているかすぐ分かります。


名前空間がいちばんの難所

ns_xml = '<r xmlns="http://example.com/ns"><item>x</item></r>'
r = ET.fromstring(ns_xml)

print("タグ名:", r[0].tag)
print("素のitemで探す:", r.find("item"))
print("名前空間つき:", r.find("{http://example.com/ns}item").text)
print("辞書で渡す:", r.find("ns:item", {"ns": "http://example.com/ns"}).text)

実行結果:

タグ名: {http://example.com/ns}item
素のitemで探す: None
名前空間つき: x
辞書で渡す: x

xmlns が付いているXMLでは、タグ名の前に名前空間が埋め込まれます。find('item')がNoneを返すときは、まず.tagを印字して確認してください。


信頼できないXMLを読むとき

ElementTree は外部実体参照などの攻撃に対して安全とは言い切れません。公式ドキュメントにも注意書きがあります。外部から受け取ったXMLを扱うならdefusedxmlなどの対策済みライブラリを検討してください。


まとめ

  • fromstring()で読み、find/findall/findtextで取り出す
  • テキストは常にstr。数値は自分で変換する
  • 存在判定はis not None。if 要素:は子要素の有無になる
  • xmlnsがあるときはタグ名に名前空間が付く

関連記事

あわせて読むと理解が深まります。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら