概要
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>隣の客は青い</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, 記事投稿