iMind Developers Blog

iMind開発者ブログ

PythonのlxmlでXMLを扱う

概要

lxmlはlibxml2とlibxsltのPythonバインディング。XMLの生成、パース、XPath等、一般的な操作が一通りできる。

今回はXMLのパース、編集、保存等の基本的な処理を触ってみる。

バージョン情報

  • Python 3.6.5
  • lxml==4.2.4

サンプルデータ

valuesタグの配下にvalue1, value2, value3という要素があるXMLを用意する。

<root>
  <values>
    <value1 atr="x"></value1>
    <value2 att="x">2</value2>
    <value3 att="y">3</value3>
  </values>
</root>

当該XMLは data/lxml_exmaple/foo.xml というパスに保存されているものとする。

XMLファイルの読み込み

etree.parseでXMLファイルを読み込める。

from lxml import etree

xml_file = 'data/lxml_exmaple/foo.xml'
with open(xml_file) as f:
    tree = etree.parse(f)

print(etree.tostring(tree).decode())

tostringしてdecodeした実行結果

<root>
  <values>
    <value1 atr="x">1</value1>
    <value2 att="x">2</value2>
    <value3 att="y">3</value3>
  </values>
</root>

remove_blankの指定

パース時にblankを落として良い場合は、remove_blank_text=Trueを指定してXMLParserを作っておく。

parser = etree.XMLParser(remove_blank_text=True)
with open(xml_file) as f:
    tree = etree.parse(f, parser=parser)

print(etree.tostring(tree).decode())

tostringしてdecodeした実行結果。blankが除去されている。

<root><values><value1 atr="x">1</value1><value2 att="x">2</value2><value3 att="y">3</value3></values></root>

他にもコメントを除去してくれるremove_comments、XMLの形式的に多少の不正があってもパースしてくれるrecover、エンコード指定するencoding等、XMLParserクラスにはいろいろオプションが用意されている。

https://lxml.de/api/lxml.etree.XMLParser-class.html

個人的にはこのあたりをTrueにして使うことが多い。

parser = etree.XMLParser(remove_blank_text=True, recover=True, remove_comment=True)

tostring時の日本語の表示

日本語を含むXMLの場合。

<root><value>隣の客は青い</value></root>

そのままtostringするとエスケープされた状態で出力される。

print(etree.tostring(tree).decode())
<root><value>&#38563;&#12398;&#23458;&#12399;&#38738;&#12356;</value></root>

tostring時にencoding指定しておくと正しく表示される。

print(etree.tostring(tree, encoding="utf-8").decode())
<root><value>隣の客は青い</value></root>

要素をループでたどる

パース結果をiter()で回すと中のXMLの要素をたどることができる。

for elem in tree.iter():
    print('tag={}, attr={}, text={}'.format(elem.tag, elem.get('att'), elem.text))

実行結果

tag=root, attr=None, text=None
tag=values, attr=None, text=None
tag=value1, attr=None, text=1
tag=value2, attr=x, text=2
tag=value3, attr=y, text=3

root, value, value1, value2, value3と順に要素を取得できている。

findで指定要素の配下を取得する

valuesの下のvalue2を取得する。

elem = tree.find('values').find('value2')
print('tag={}, attr={}, text={}'.format(elem.tag, elem.get('att'), elem.text))
tag=value2, attr=x, text=2

values配下をiterで回す。この記述の場合、findで指定したvalues自身も結果に含まれる。

for elem in tree.find('values').iter():
    print('tag={}, attr={}, text={}'.format(elem.tag, elem.get('att'), elem.text))
tag=values, attr=None, text=None
tag=value1, attr=None, text=1
tag=value2, attr=x, text=2
tag=value3, attr=y, text=3

findした結果に対してiterを使わずにそのままループさせた場合は、childrenだけが返る。

for elem in tree.find('values'):
    print('tag={}, attr={}, text={}'.format(elem.tag, elem.get('att'), elem.text))
tag=value1, attr=None, text=1
tag=value2, attr=x, text=2
tag=value3, attr=y, text=3

XPathでの取得

要素に対してxpathを指定することもできる。

下記はvaluesの子要素を取得する例。

result = tree.xpath('/root/values/child::node()')
for elem in result:
    print('tag={}, attr={}, text={}'.format(elem.tag, elem.get('att'), elem.text))
tag=value1, attr=None, text=1
tag=value2, attr=x, text=2
tag=value3, attr=y, text=3

valuesをfindで取ってからその下の子要素を取得する。

result = tree.find('values').xpath('child::node()')
for elem in result:
    print('tag={}, attr={}, text={}'.format(elem.tag, elem.get('att'), elem.text))
tag=value1, attr=None, text=1
tag=value2, attr=x, text=2
tag=value3, attr=y, text=3

巨大なXMLを扱う場合

メモリに乗せられないようなサイズのXMLを扱う場合は、iterparseでSAXライクなパースが可能。

下記はstartとendのeventを指定してパースしている。

ite = etree.iterparse(xml_file, events=('start', 'end'), remove_blank_text=True)
for event, elem in ite:
    print(event, elem.tag, elem.text)

実行結果。各要素のstartとendが取れている。

start root None
start values None
start value1 1
end value1 1
start value2 2
end value2 2
start value3 3
end value3 3
end values None
end root None

取得するタグが決まっている場合は指定タグだけイベントを返すよう設定する。

ite = etree.iterparse(xml_file, events=('start', 'end'), tag=['value1', 'value2']), remove_blank_text=True)
for event, elem in ite:
    print(event, elem.tag, elem.text)
start value1 1
end value1 1
start value2 2
end value2 2

XMLPullParserというpull型のパーサーも用意されている。下記はファイルを10byteずつ読み込みparserにfeedしてイベントを読んでいった例。

parser = etree.XMLPullParser(events=('start', 'end'), remove_blank_text=True)
with open(xml_file, 'rb') as f:
    for chunk in iter(lambda: f.read(10), b''):
        print(b'** chunk='+chunk)
        parser.feed(chunk)
        for event, elem in parser.read_events():
            print(event, elem.tag, elem.text)

実行結果。途中で切れているXMLの断片がfeedされ、逐次イベントが発生している。

b'** chunk=<root>\n  <'
start root None
b'** chunk=values>\n  '
start values None
b'** chunk=  <value1 '
b'** chunk=atr="x">1<'
start value1 1
b'** chunk=/value1>\n '
end value1 1
b'** chunk=   <value2'
b'** chunk= att="x">2'
start value2 None
b'** chunk=</value2>\n'
end value2 2
b'** chunk=    <value'
b'** chunk=3 att="y">'
start value3 None
b'** chunk=3</value3>'
end value3 3
b'** chunk=\n  </value'
b'** chunk=s>\n</root>'
end values None
end root None
b'** chunk=\n\n'

面白い点としては、value1のstartは「start value1 1」とテキストが取れているのに対して、value2はchunkのタイミングが悪く「start value2 None」となっている。

XMLの編集

XPathで要素を取得し、テキストを編集してXMLを出力してみる。textに値を代入するだけで実行できる。

for value1 in tree.xpath('/root/values/value1'):
    value1.text = 'new text'
print(etree.tostring(tree).decode())

実行結果。value1のtextがnew textに変更されている。

<root><values><value1 atr="x">new text</value1><value2 att="x">2</value2><value3 att="y">3</value3></values></root>

属性の追加と変更。

# 属性の追加
value1.set('new_attr', 'new attr text')

# 属性の変更
value1.attrib['att'] = 'modify attr text'
<root><values><value1 atr="x" new_attr="new attr text" att="modify attr text">new text</value1><value2 att="x">2</value2><value3 att="y">3</value3></values></root>

要素の削除

要素の削除を行う場合は、削除したい要素のparentに対してremoveを呼び出す。

value2 = tree.find('values').find('value2')
value2.getparent().remove(value2)
print(etree.tostring(tree).decode())

実行結果。value2が削除されている。

<root><values><value1 atr="x">1</value1><value3 att="y">3</value3></values></root>

XMLの出力

write関数で指定ファイルを出力できる。pretty_print=Trueを指定すると整形済みのXMLが得られる。

tree.write('bar.xml', pretty_print=True)

改定履歴

Author: Masato Watanabe, Date: 2019-01-16, 記事投稿