PythonでXMLを処理する — 【完全解説】ElementTreeモジュールの使い方

Pythonの標準ライブラリには、XML文書の解析や作成を簡易に行うことができるxml.etree.ElementTreeモジュールが含まれています。ここではそのモジュールを使ってXMLを操作するさまざまな方法を、例を交えながらわかりやすく解説したいと思います。

具体的には次の内容を解説します。

この記事でわかること
  • ファイルなどのさまざまなリソースからXML文書を読み込んでパースする
  • XML文書ツリーの要素を順に走査したり、特定の要素を検索する
  • XPathを使って要素を検索する
  • XML文書の編集したり、構築したりする。
  • XML文書をファイルなどへ出力する

ただし、このモジュールはXMLの機能のほんの一部しかサポートしていません。上述のような処理で十分ならとても有用ですが、それ以上のことを実施した場合はlxmlのようなライブラリが必要になるでしょう。

目次

この記事で使用するXML文書

この記事ではxml.etree.ElementTreeのドキュメントで使われている次のXML文書を使って主に解説します。これをcountry_data.xmlというファイル名で保存してください。

<?xml version="1.0"?>
<data>
    <country name="Liechtenstein">
        <rank>1</rank>
        <year>2008</year>
        <gdppc>141100</gdppc>
        <neighbor name="Austria" direction="E"/>
        <neighbor name="Switzerland" direction="W"/>
    </country>
    <country name="Singapore">
        <rank>4</rank>
        <year>2011</year>
        <gdppc>59900</gdppc>
        <neighbor name="Malaysia" direction="N"/>
    </country>
    <country name="Panama">
        <rank>68</rank>
        <year>2011</year>
        <gdppc>13600</gdppc>
        <neighbor name="Costa Rica" direction="W"/>
        <neighbor name="Colombia" direction="E"/>
    </country>
</data>

xml.etree.ElementTree ― The ElementTree XML API | python.org

また、この後の例を実行するためにElementTreeモジュールを次のようにインポートしておきます。

>>> import xml.etree.ElementTree as ET

ElementTreeの主要なクラス

このモジュールにはいくつかのクラスが定義されていますが、主に使うのは次の2つのクラスです。XMLの解析(読み込み)、要素の参照、内部のツリー構造をシリアライズするといった基本的な処理は、これらのクラスとモジュールで定義された関数を使って行います。

ElementTreeクラス

モジュール内部でXML文書はツリー構造で表現されます。このクラスはそのツリー全体を表します。

Elementクラス

ツリーのノード(要素)を表すクラスです。

XML文書を読み込む

XML文書は、ファイルや文字列といったソースから読み込むことができます。読み込まれた文書はパースされツリー構造が構築されます。

XML文書をパースするときにパーサーはXMLのコメント、処理命令、ドキュメントタイプ宣言は読み飛ばします。そのため構築されたツリーにはこれらの要素は含まれません。しかし、このモジュールを使ってコメントや処理命令を含むツリーを構築して出力することは可能です。

ファイルからXML文書を読み込む

ファイルに保存されたXML文書を読み込むには、xml.etree.ElementTreeモジュールのparse()関数を使います。引数にファイル名を指定します。この関数は読み込んだXML文書をパースしてElementTreeクラスのインスタンスを返します。

>>> tree = ET.parse('country_data.xml')
>>> root = tree.getroot()

getroot()メソッドはツリーのルート要素を返します。

文字列からXML文書を読み込む

文字列のXML文書(あるいはその断片)を読み込むには、xml.etree.ElementTreeモジュールのfromstring()関数を使います。これはルート要素を表すElementクラスのインスタンスを返します。country_data_as_stringはXML文書の文字列を保持する変数です。

root = ET.fromstring(country_data_as_string)

parse()関数はElementTreeオブジェクトを返しますが、fromstring()関数はElementオブジェクトを返します。これはparse()関数が、主に完全なXML文書を読み込むために使われるのに対し、fromstring()関数はXML文書の断片を読み込むために多く使われるからです。

XML()関数はfromstring()関数と機能的には同じですが、XML文書の文字列リテラルをコードに埋め込むときに使います。

root = ET.XML("""<?xml version="1.0"?>
<data>
    <country name="Liechtenstein">
        <rank>1</rank>
        ... (以下、省略)
)

XML文書をファイルに保存する

ElementTreeクラスが表すツリーは、write()メソッドを使ってファイルに書き出すことができます。1番目の引数にファイル名を指定します。オプションでencoding引数に使用するエンコーディングを指定できます。省略した場合はUS-ASCIIエンコーディングを使用します。

>>> tree.write('write_sampl.xml', encoding='utf-8')

オプションのxml_declaration引数にTrueを指定すると、出力にXML宣言が追加されます。この引数のデフォルトはNoneで、エンコーディングがUS-ASCII、UTF-8、Unicode以外の場合にXML宣言が出力します。Falseは常に出力しないことを指示します。

>>> tree.write('write_sampl.xml', encoding='utf-8', xml_declaration=True)

書き出されるXML宣言は次のようになります。

<?xml version='1.0' encoding='utf-8'?>
<data>
    <country name="Liechtenstein">
        <rank>1</rank>
        ...

XML文書をシリアライズする

ツリーを文字列にシリアライズするには、xml.etree.ElementTreeモジュールのtostring()関数を使います。1つ目の引数にシリアライズするElementオブジェクトを渡します。オプションのencoding引数は使用するエンコーディングで、encoding引数を省略した場合はUS-ASCIIエンコーディングが使われます。

>>> serialized = ET.tostring(root, encoding='utf-8')
>>> serialized
b'<data>\n    <country name="Liechtenstein">\n  ...

Unicode文字列にシリアライズしたい場合は、encoding引数にunicodeを指定します。

>>> unicode = ET.tostring(root, encoding='unicode')
>>> print(unicode)
<data>
    <country name="Liechtenstein">
        <rank>1</rank>
        ...

XML宣言を追加して書き出すには、xml_declaration引数にTrueを指定します。この引数の意味はElementTreeクラスのwrite()メソッドと同じです。

標準出力へツリーを出力する

ツリーを確認するために、tostring()関数を使ってシリアライズしてから標準出力に出力しても良いですが、もっと簡単な方法があります。xml.etree.ElementTreeモジュールのdump()関数の引数にElementTreeオブジェクトまたはElementオブジェクトを渡して呼び出せばオブジェクトが表すツリーを標準出力へ出力します。

>>> ET.dump(tree)
>>> ET.dump(root)

dump()関数はデバッグ以外での使用は推奨されていません。

Elementオブジェクトの属性

Elementオブジェクトには、次の表の属性を持っています。

属性説明
tag要素のタグ名(要素名)
attrib要素の属性を保持する辞書
text要素の開始タグと終了タグの間、子要素がある場合は開始タグと最初の子要素との間のテキスト
tail要素の終了タグから次のタグまでの間のテキスト

例えばタグ名は、tag属性で参照できます。

>>> root.tag
'data'

その他の属性については、この後必要なところで詳しく説明します。

要素を参照する

XML文書を読み込むと内部的にツリー構造が構築されます。このツリーのノード(要素)は、さまざまな方法で参照することができます。例えばツリーの要素を順に辿ったり、特定の要素を検索することができます。ここではそれら目的の要素を参照する方法について見ていきます。

ツリーのルート要素を取得する

ElementTreeオブジェクトが表すツリーのルート要素はgetroot()メソッドで取得できます。これはルート要素を表すElementオブジェクトを返します。

root = tree.getroot()

Elementオブジェクトをリストのように使う

Elementオブジェクトはリストのように振る舞います。例えば次のようにインデックスを指定して子要素を参照することができます。インデックスは0から始まります。これはXML文書の出現順で要素の最初の子要素です。

>>> elem = root[0]
>>> print(f'{elem.tag} - {elem.attrib}')
country - {'name': 'Liechtenstein'}

孫ノードを参照するには次のようにインデックスを指定します。

>>> grandchild = root[0][1]
>>> grandchild.text
'2008'

リスト同様にElementオブジェクトはイテラブル(イテレート可能)です。そのためforを使ってイテレートすることができます。これはイテレートされるたびに子要素(Elementオブジェクト)を文書の出現順に返します。

>>> for child in root:
...     print(f'{child.tag} - {child.attrib}')
... 
country - {'name': 'Liechtenstein'}
country - {'name': 'Singapore'}
country - {'name': 'Panama'}

子要素の数が知りたいときはlen()関数を使います。これや要素の子要素の数を返します。

>>> len(root)
3

len()関数は要素が子要素を持っているかテストするときにも使われます。

>>> if not len(grandchild):
...     print('Element has no child.')
... 
Element has no child.

要素を再帰的にイテレートする

Elementクラスのiter()メソッドを使えば、現在の要素をルートとするサブツリーを再帰的にイテレートすることもできます。このメソッドはツリー内の要素を文書内の出現順(深さ優先順)にイテレートするイテレーターを返します。引数を省略したり、Noneや「*」を指定した場合、すべての要素をイテレートするイテレータを返します。

>>> for elem in root.iter():
...     print(f'{elem.tag} - {elem.attrib}')
... 
data - {}
country - {'name': 'Liechtenstein'}
rank - {}
year - {}
gdppc - {}
neighbor - {'name': 'Austria', 'direction': 'E'}
neighbor - {'name': 'Switzerland', 'direction': 'W'}
country - {'name': 'Singapore'}
   ...

ElementTreeクラスもiter()メソッドを持っています。これはルート要素を起点とするイテレータを返します。それ以外はElementクラスのメソッドと同じです。

>>> for elem in tree.iter():
...     print(f'{elem.tag}, {elem.attrib}')

特定の要素だけイテレートする

特定の要素だけをイテレートしたい場合は、iter()メソッドの引数にタグ名(要素名)を指定します。そうすると、イテレータは指定されたタグ名の要素だけ返します。

>>> for elem in root.iter('rank'):
...     print(elem.text)
... 
1
4
68

XPathを使った要素の参照

xml.etree.ElementTreeモジュールは限定的ですがXPathのパス式をサポートしています。パス式を使うことにより、特定の要素をもっと柔軟に参照することが可能になります。

パス式が使用できるメソッド

ElementクラスとElementTreeクラスには、引数にパス式をとる次の同名のメソッドがそれぞれにあります。Elementオブジェクトのメソッドは、オブジェクトが表す要素をルートとするサブツリーを対象としますが、ElementTreeクラスのメソッドはルート要素を起点するツリーを対象にする点が異なります。

  • find()メソッド
  • findall()メソッド
  • findtext()メソッド
  • iterfind()メソッド

最初にメソッドについて説明して、その後にパス式を解説します。

find()メソッドで要素を検索する

Elementクラスのfind()メソッドは、現在の要素をルートとするサブツリーから引数のパス式にマッチする要素を検索します。このメソッドは見つかった最初の要素を返します。要素が見つからなかったときはNoneを返します。

>>> target = root.find('country')
>>> print(f'{target.tag} - {target.attrib}')
country - {'name': 'Liechtenstein'}

ElementTreeクラスのfind()メソッドは、ルート要素を起点として検索します。それ以外は同じです。

findall()メソッドで要素を検索する

find()メソッドは見つかった最初の要素を返しますが、findall()メソッドは見つかったすべての要素をリストで返します。要素が見つからなかったときは空のリストを返します。

>>> target = root.findall('country')
>>> for elem in target:
...     print(f'{elem.tag} - {elem.attrib}')
... 
country - {'name': 'Liechtenstein'}
country - {'name': 'Singapore'}
country - {'name': 'Panama'}

ElementTreeクラスのfindall()メソッドは、ルート要素を起点として検索します。それ以外は同じです。

findtext()メソッドでテキストを検索する

Elementクラスのfindtext()メソッドは、現在の要素をルートとするサブツリーから引数のパス式にマッチする要素を検索し、最初に見つかった要素のテキストを返します。要素が見つからなかったときはNoneを返します。

>>> text = root.findtext('./country/rank')
>>> print(text)
1

ElementTreeクラスのfindtext()メソッドは、ルート要素を起点として検索します。それ以外は同じです。

iterfind()メソッドで要素をイテレートする

Elementクラスのiterfind()メソッドは、現在の要素をルートとするサブツリーから、引数のパス式にマッチする要素をXML文書内の出現順でイテレートするイテレータを返します。

>>> target = root.iterfind('country')
>>> for elem in target:
...     print(f'{elem.tag} - {elem.attrib}')
... 
country - {'name': 'Liechtenstein'}
country - {'name': 'Singapore'}
country - {'name': 'Panama'}

ElementTreeクラスのiterfind()メソッドは、ルート要素を起点として検索します。それ以外は同じです。

パス式の概要

このモジュールのパス式のサポートは限定的です。それなのでXPathのパス式の完全な説明はできませんが、このモジュールで使う上で必要な知識をここで説明します。

パス式は次のようにステップを並べて、XML文書中のあるノードから特定のノードまでの位置を示すものです。各ステップはスラッシュ(/)またはスラッシュを重ねたもの(//)で連結されます。

/ステップ/ステップ/ステップ

先頭のスラッシュ(/)はルートノードを表します(ルート要素も含むツリー全体のルートで、ルート要素とは異なる)。先頭が「/」または「//」で始まるパス式は、ルートノードから探索を始めるので絶対パスと呼ばれます。それ以外はある位置から相対的に目的のノードまでの位置を示すので相対パスと呼ばれます。

ステップの連結に使われる「//」は、現在のノードおよびすべての子孫ノードを選択することを意味します。例えば次のようなパス式を例にします。

./ステップ1//ステップ2

このときの「//」は、次のように展開されると考えることができます。

./ステップ1/<現在ノードおよびすべての子孫ノードを選択>/ステップ2

「.」は現在のノードを意味します。次にその子ノードからステップ1でノードが絞られます。次に「ステップ1で絞られたノードとそのノードのすべての子孫ノード」が選択されます。最後に先ほど選択されたノードの子ノードからステップ2でさらにノードが絞り込まれます。

find()、findall()、iterfind()メソッドの引数に絶対パスのパス式は使えないようです。これはElementTreeオブジェクトがツリーのルートノードにアクセスできる方法を提供していない、あるいは最上位ノードがルート要素として実装されているなどが考えられます。例えば、次のようにツリーの現在のノードを検索してもルート要素が返されます。

>>> tree.find('.').tag
'data'

とりあえず、これらのメソッドの引数には相対パスを指定するようにしましょう。

各ステップは次のようにノードテストと述語からなります。オプションの述語は大括弧で囲まれます。ノードテストで対象を選択し、述語はさらに絞り込むために使われます。

ノードテスト[述語]

本物のパス式では、ステップは「軸::ノードテスト[述語]」の3つの部分からなります。軸は省略可能で省略すると子要素を選択することを意味します。

このモジュールで使えるノードテストには次のものがあります。

ノードテスト意味
名前名前を持つすべての子要素を選択する。名前空間内の要素は 「{名前空間}要素名」のように記述する。
*コメントや処理命令を含むすべての子要素を選択する。「{名前空間}*」は、指定された名前空間内のすべての要素を選択し、「{*}名前」は、任意の名前空間内の名前という要素を選択し、「{}*」は、名前空間内にない要素のみを選択する。
.現在のノードを選択する。
..親ノードを選択する。パスが現在のノードの親要素へ進もうとした場合はNoneを返す。

次にノードテストの例を示します。

# パス式は現在の要素の子要素country、そのまた子要素であるneighborをすべて選択する
>>> node = root.find('./country/neighbor')
>>> print(f'{node.tag} - {node.attrib}')
neighbor - {'name': 'Austria', 'direction': 'E'}

# 上の例と同じ。
>>> node = root.find('country/neighbor')
>>> print(f'{node.tag} - {node.attrib}')
neighbor - {'name': 'Austria', 'direction': 'E'}

# パス式は現在の要素の子要素countryをすべて選択する。
>>> nodes = root.findall('./country')
>>> for elem in nodes:
...     print(f'{elem.tag} - {elem.attrib}')
... 
country - {'name': 'Liechtenstein'}
country - {'name': 'Singapore'}
country - {'name': 'Panama'}

# 結果は上記と同じ。ただし検索範囲が異なる。パス式は現在の要素のすべての子孫要素の
# パス式は現在の要素のすべての子孫要素の内の要素countryをすべて選択する。
>>> nodes = root.findall('.//country')
>>> for elem in nodes:
...     print(f'{elem.tag} - {elem.attrib}')
... 
country - {'name': 'Liechtenstein'}
country - {'name': 'Singapore'}
country - {'name': 'Panama'}

このモジュールで使える述語には次のものがあります。述語を記述することにより要素をさらに絞り込むことができます。

操作意味
[@属性名]属性名を持つ全ての要素を選択する。
[@属性名=’属性値’]属性名が属性値を持つすべての要素を選択する。
[@属性名!=’属性値’]属性名が属性値を持たないすべての要素を選択する。
[タグ名]タグ名の子要素を持つすべての要素を選択する。
[.=’テキスト’]ノードテストで選択されたノードで、コンテンツに指定されたテキストを持つすべての要素を選択する。
[.!=’テキスト’]ノードテストで選択されたノードで、コンテンツに指定されたテキストを持たないすべての要素を選択する。
[タグ名=’テキスト’]ノードテストで選択されたノードの内、指定されたテキストを持つタグ名ノードを子に持つ、すべてのノードを選択する。
[タグ名!=’テキスト’]ノードテストで選択されたノードの内、指定されたテキストを持たないタグ名ノードを子に持つ、すべてのノードを選択する。
[位置]位置にあるすべての要素を選択する。位置は整数で、先頭が1で末尾は式 last()になります。は「 last()-1」のように末尾からの位置で指定することもできます。

次にいくつか述語の例を示します。

# パス式は現在の要素のすべての子孫要素の内、name属性を持つものをすべて選択する。
>>> nodes = root.findall('.//*[@name]')
>>> for elem in nodes:
...     print(f'{elem.tag} - {elem.attrib}')
... 
country - {'name': 'Liechtenstein'}
neighbor - {'name': 'Austria', 'direction': 'E'}
neighbor - {'name': 'Switzerland', 'direction': 'W'}
country - {'name': 'Singapore'}
    ...

# パス式はyear要素でテキストに「2011」を持つ要素を選択する
>>> nodes = root.findall('./country/year[.="2011"]')
>>> for elem in nodes:
...     print(f'{elem.tag} - {elem.text}')
... 
year - 2011
year - 2011

# パス式はテキストに「2011」を持つyear要素を子に持つcountry要素をすべて選択する。
>>> nodes = root.findall('./country[year="2011"]')
>>> for elem in nodes:
...     print(f'{elem.tag} - {elem.attrib}')
... 
country - {'name': 'Singapore'}
country - {'name': 'Panama'}

# パス式は1番目のneighbor要素を選択する
>>> nodes = root.findall('./country/neighbor[1]')
>>> for elem in nodes:
...     print(f'{elem.tag} - {elem.attrib}')
... 
neighbor - {'name': 'Austria', 'direction': 'E'}
neighbor - {'name': 'Malaysia', 'direction': 'N'}
neighbor - {'name': 'Costa Rica', 'direction': 'W'}

要素の属性を参照する

Elementオブジェクトのattrib属性は、要素(ノード)の属性を辞書として保持しています。

>>> elem = tree.find('.//neighbor')
>>> elem.tag
'neighbor'
>>> elem.attrib
{'name': 'Austria', 'direction': 'E'}

ある属性の属性値を参照するには、Elementオブジェクトのget()メソッドに属性名を渡します。keys()メソッドは属性名のリストを返し、items()メソッドは要素の属性を (名前, 値)のタプルをリストとして返します。

>>> elem.get('name')
'Austria'
>>> elem.keys()
['name', 'direction']
>>> elem.items()
[('name', 'Austria'), ('direction', 'E')]

要素のテキストを参照する

要素のテキストはtext属性あるいはtail属性を参照することによりアクセスできます。

次のようにXML文書の断片を読み込んで確認してみましょう。

>>> foo = ET.XML('<foo>TEXT<bar />TAIL</foo>')
>>> bar = foo[0]
>>> ET.dump(foo)
<foo>TEXT<bar />TAIL</foo>
>>> ET.dump(bar)
<bar />TAIL

text属性は要素の開始タグと終了タグの間、あるいは開始タグと最初の子要素との間のテキストです。tail属性は要素自身の終了タグから次のタグまでの間にテキストになります。

>>> foo.text
'TEXT'
>>> bar.tail
'TAIL'

テキストをイテレートする

Elementクラスのitertext()メソッドは、要素をルートとするサブツリーのテキストを文書の中での出現順でイテレートするイテレーターを返します。

>>> list(root.itertext())
['\n    ', '\n        ', '1', '\n        ', '2008', '\n        ', '141100', '\n        ', '\n        ', '\n    ', '\n    ', '\n        ', '4', '\n        ', '2011', '\n        ', '59900', '\n        ', '\n    ', '\n    ', '\n        ', '68', '\n        ', '2011', '\n        ', '13600', '\n        ', '\n        ', '\n    ', '\n']

テキストには文書を整形するためのインデントや改行も含まれます。

XML文書を編集する

読み込んだXML文書を編集したり、XML文書を一から構築することもできます。ここではXML文書を編集する方法を説明します。

要素を作成する

xml.etree.ElementTreeモジュールのElement()関数は新しい要素(Elementオブジェクト)を作成します。1つ目の引数にタグ名を渡します。オプションで2番目の引数に属性の辞書を渡すこともできます。

>>> a = ET.Element('a', {'x': 'foo', 'y': 'bar'})
>>> ET.dump(a)
<a x="foo" y="bar" />

子要素を追加する

xml.etree.ElementTreeモジュールのSubElement()関数は子要素を新しく作成し、要素の子として追加します。第1引数に親要素(のElementオブジェクト)、第2引数にタグ名を渡して呼び出します。オプションで第3引数に属性の辞書を渡すこともできます。この関数は新しく作成した子要素のElementオブジェクトを返します。

>>> b = ET.SubElement(a, 'b', {'z': 'baz'})
>>> ET.dump(a)
<a x="foo" y="bar"><b z="baz" /></a>

Elementクラスのappend()メソッドを使って子要素を追加することもできます。このとき要素は末尾に追加されます。

>>> c = ET.Element('c')
>>> a.append(c)
>>> ET.dump(a)
<a x="foo" y="bar"><b z="baz" /><c /></a>

末尾以外の場所に子要素を追加したい場合はElementクラスのinsert()メソッドを使います。第1引数には追加する場所を示すインデックス、第2引数にElementオブジェクトを指定します。インデックスは0から始まり、0を指定すると先頭の子要素として追加されます。

>>> d = ET.Element('d')
>>> a.insert(0, d)
>>> ET.dump(a)
<a x="foo" y="bar"><d /><b z="baz" /><c /></a>

子要素を削除する

子要素を削除するにはElementクラスのremove()メソッドを使います。引数には削除したい子要素のElementオブジェクトを渡します。

>>> a.remove(d)
>>> ET.dump(a)
<a x="foo" y="bar"><b z="baz" /><c /></a>

属性を追加する

既存の要素に属性を追加するにはElementクラスのset()メソッドを使います。引数には属性のキーと値を指定します。

>>> b.set('key', 'val')
>>> ET.dump(a)
<a x="foo" y="bar"><b z="baz" key="val" /><c /></a>

ツリーをインデントする

作成したツリーはそのままでは見づらいでしょう。xml.etree.ElementTreeモジュールのindent()関数を使うと、インデントして見やすく整形することができます。オプションのspace引数にはインデントレベルごとに挿入される文字列を指定します。省略した場合はデフォルトの2文字のスペースが使われます。

>>> ET.indent(a, space='    ')
>>> ET.dump(a)
<a x="foo" y="bar">
    <b z="baz" key="val" />
    <c />
</a>

コメントや処理命令を追加する

コメントはxml.etree.ElementTreeモジュールのComment()関数を使って作成できます。作成されたコメントはElementクラスのインスタンスですので、append()やinsert()メソッドを使って要素に追加することができます。

>>> comment = ET.Comment('This is comment.')
>>> a.insert(0, comment)
>>> ET.indent(a, space='    ')
>>> ET.dump(a)
<a x="foo" y="bar">
    <!--This is comment.-->
    <b z="baz" key="val" />
    <c />
</a>

処理命令はxml.etree.ElementTreeモジュールのProcessingInstruction()関数を使って作成します。処理命令もElementクラスのインスタンスですので、append()やinsert()メソッドを使って要素に追加できます。

>>> pi = ET.ProcessingInstruction('target', 'some text')
>>> a.insert(1, pi)
>>> ET.indent(a, space='    ')
>>> ET.dump(a)
<a x="foo" y="bar">
    <!--This is comment.-->
    <?target some text?>
    <b z="baz" key="val" />
    <c />
</a>

テキストを追加・修正する

要素のコンテンツにテキストを追加したり、既存のテキストを変更したい場合は、Elementオブジェクトのtext属性やtail属性に文字列を代入します。

先ほどの例にもう一度、d要素を追加します。ただし、今度はc要素の子として追加します。

>>> c.append(d)
>>> ET.dump(a)
<a x="foo" y="bar">
    <!--This is comment.-->
    <?target some text?>
    <b z="baz" key="val" />
    <c><d /></c>
</a>

それでは要素のコンテンツとしてテキストを追加してみましょう。text属性は要素の開始タグと終了タグの間、子要素がある場合は開始タグと最初の子要素との間のテキストです。tail属性は要素の終了タグと次のタグまでの間にテキストです。それらに文字列を代入することでテキストを追加しています。

>>> c.text = 'TEXT'
>>> d.tail = 'TAIL'
>>> ET.dump(a)
<a x="foo" y="bar">
    <!--This is comment.-->
    <?target some text?>
    <b z="baz" key="val" />
    <c>TEXT<d />TAIL</c>
</a>

テキストを削除する

テキストを削除するには、text属性またはtail属性にNoneを代入します。

>>> d.tail = None
>>> ET.dump(a)
<a x="foo" y="bar">
    <!--This is comment.-->
    <?target some text?>
    <b z="baz" key="val" />
    <c>TEXT<d /></c>
</a>

ElementオブジェクトからElementTreeオブジェクトを作成する

作成したツリー構造をファイルに出力するにはElementTreeオブジェクトが必要です。ある要素(Elementオブジェクト)をルート要素とするElementTreeオブジェクトは、xml.etree.ElementTreeモジュールのElementTree()関数を使って作成できます。引数にルートとなる要素を指定して呼び出すと、その要素をルート要素とするElementTreeオブジェクトを返します。

>>> newtree = ET.ElementTree(a)

名前空間のあるXMLの解析

XML文書が名前空間を持つ場合、名前空間に属するXMLの「要素」と「属性」は通常は「名前空間プレフィックス:名前」という形式で識別されます。ただし、デフォルトの名前空間に属する場合は、名前空間プレフィックス(と:)はありません。これらは、このモジュールに読み込まれると「{URI}名前」という形式に展開されます。

説明のために次のようなXML文書を読み込みます。

>>> ns_tree = ET.XML("""<?xml version='1.0'?>
... <entry xmlns:os="http://example.com/os"
...           xmlns="http://example.com/ty">
...     <employee>
...         <name sex="mail" site="minato">
...             Suzuki Ichiro
...         </name>
...         <name sex="mail" site="shibuya">
...             Sato Taro
...         </name>
...     </employee>
...     <os:employee>
...         <os:name os:sex="mail" os:site="sakai">
...             Suzuki Ichiro
...         </os:name>
...         <os:name os:sex="femail" os:site="kadoma">
...             Yamada Hanako
...         </os:name>
...     </os:employee>
... </entry>""")

名前空間内の要素や属性の名前は、モジュール内では「{URI}名前」という形式で参照します。そのため関数の引数に指定する場合、この形式で指定する必要があります。

>>> elem = ns_tree.find('.//{http://example.com/os}name')
>>> elem.tag
'{http://example.com/os}name'
>>> elem.get('{http://example.com/os}site')
'sakai'

プレフィックスの辞書を作成して関数の2番目の引数に指定することによって、「{URI}名前」の形式の代わりに「プレフィックス(辞書のキー):名前」の形式を使うこともできます。辞書はプレフィックス(任意の文字列)と名前空間URIのペアです。URIを「”」に対応づけるとプレフィックスなしの名前を使うことができます。

>>> prefix_dict = {'os': 'http://example.com/os', '': 'http://example.com/ty'}
>>> elem = ns_tree.find('.//os:name', prefix_dict)
>>> print(f'{elem.tag} - {elem.attrib}')
{http://example.com/os}name - {'{http://example.com/os}sex': 'mail', '{http://example.com/os}site': 'sakai'}
>>> elem = ns_tree.find('.//name', prefix_dict)
>>> print(f'{elem.tag} - {elem.attrib}')
{http://example.com/ty}name - {'sex': 'mail', 'site': 'minato'}

最後の出力で、属性がURIで修飾されていないことに注意してください。この例のXML文書にはデフォルトの名前空間が宣言(xmlns=”http://example.com/ty”)されています。プレフィックスで修飾されていない要素はデフォルトの名前空間が適用されますが、修飾されていない属性には適用されません。

ところで名前空間を持つXML文書を読み込むと、モジュール内では「{URI}名前」という形式に展開されることは前述しました。これをシリアライズして標準出力へ出力したり、ファイルへ書き込むときには次のような名前空間プレフィックスに置き換えられます。

>>> ET.dump(ns_tree)
<ns0:entry xmlns:ns0="http://example.com/ty" xmlns:ns1="http://example.com/os">
    <ns0:employee>
        <ns0:name sex="mail" site="minato">
            Suzuki Ichiro
        </ns0:name>
        <ns0:name sex="mail" site="shibuya">
            Sato Taro
        </ns0:name>
    </ns0:employee>
    <ns1:employee>
        <ns1:name ns1:sex="mail" ns1:site="sakai">
            Suzuki Ichiro
        </ns1:name>
        <ns1:name ns1:sex="femail" ns1:site="kadoma">
            Yamada Hanako
        </ns1:name>
    </ns1:employee>
</ns0:entry>

シリアライズされるときに名前空間プレフィックスを制御したい場合は、xml.etree.ElementTreeモジュールのregister_namespace()関数を使って、URIに対応する名前空間プレフィックスを登録します。そうすると名前空間にある要素や属性は、シリアライズされるときにこのプレフィックスが付与されます。URIをデフォルトの名前空間に対応づける場合は空文字列(”)を渡します。

>>> ET.register_namespace('os', 'http://example.com/os')
>>> ET.register_namespace('', 'http://example.com/ty')
>>> ET.dump(ns_tree)
<entry xmlns="http://example.com/ty" xmlns:os="http://example.com/os">
    <employee>
        <name sex="mail" site="minato">
            Suzuki Ichiro
        </name>
        <name sex="mail" site="shibuya">
            Sato Taro
        </name>
    </employee>
    <os:employee>
        <os:name os:sex="mail" os:site="sakai">
            Suzuki Ichiro
        </os:name>
        <os:name os:sex="femail" os:site="kadoma">
            Yamada Hanako
        </os:name>
    </os:employee>
</entry>

まとめ

xml.etree.ElementTreeモジュールは、XML文書の読み書き、要素からのデータの取り出しといったことが簡単にできます。しかし、XML宣言、コメント、処理命令、文書型宣言(DTD)などのXML要素についてのサポートは十分ではありません。XMLを完全に解析して利用する場合はlxmlなどの他のライブラリを使用する必要があります。

よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

・PM、SE、SIなどを20年以上経験
・ネットワークスペシャリスト、セキュリティスペシャリストなど複数の資格を保有

目次