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があるときはタグ名に名前空間が付く
関連記事
あわせて読むと理解が深まります。





