lxmlチュートリアル翻訳してみた
PythonでXML(HTML)を扱う高速で便利なライブラリがlxmlです。 非常に強力なメソッドが多数用意されているのですが、日本語の情報があまりないのが弱点です。なので今回、lxml.etree公式チュートリアルの一部を勝手に翻訳しました。
量が多いので全ては訳しきれていませんが*1、lxmlでおそらく一番よく使われるであろう、Elementクラスの部分を中心に訳を作りました。誰かのお役に立てれば幸いです。
また、翻訳にあたっては直訳よりも日本語として自然な訳を選んだ部分があります。正しくは原文を参照してください。ここの訳がおかしい、日本語が不自然、この文書も訳せ等ありましたらお知らせください。
lxml.etreeチュートリアル
これはlxml.etreeを用いたXML処理に関するチュートリアルです。これはElementTree APIのメインコンセプト、およびプログラマとしての生活を楽にするいくつかのシンプルな改善を簡単に見渡すためのものです。 APIの完全なリファレンスについては、生成されたAPIドキュメントをご覧ください。
Contents
lxml.etreeをインポートする一般的な方法は次のとおりです。
>>> from lxml import etree
参照
Elementクラス
ElementはElementTree APIに対する主なコンテナオブジェクトです。ほとんどのXMLツリーは、このクラスを介してアクセスされます。要素はElementファクトリを通して簡単に作成することができます:
>>> root = etree.Element("root")
要素はXMLツリー構造の中にまとめられています。子要素を生成して親要素へ追加するためには、append
メソッドが使えます:
>>> root.append( etree.Element("child1") )
しかし、SubElementファクトリーという、上のことをするよりもずっと簡潔で効率のよい方法があることはよく知られているでしょう。SubElementファクトリーはElementファクトリーと同じ引数を受け取りますが、加えて第1引数として親要素を渡さなければいけません:
>>> child2 = etree.SubElement(root, "child2") >>> child3 = etree.SubElement(root, "child3")
これが確かにXMLであることを確認するためには、作成したツリーをシリアライズします:
>>> print(etree.tostring(root, pretty_print=True)) <root> <child1/> <child2/> <child3/> </root>
要素はリスト
簡単かつ直接これらのサブ要素にアクセスするために、普通のPythonのリストの振る舞いに限りなく近くなるよう要素は定義されています:
>>> child = root[0] >>> print(child.tag) child1 >>> print(len(root)) 3 >>> root.index(root[1]) # lxml.etreeだけ 1 >>> children = list(root) >>> for child in root: ... print(child.tag) child1 child2 child3 >>> root.insert(0, etree.Element("child0")) >>> start = root[:1] >>> end = root[-1:] >>> print(start[0].tag) child0 >>> print(end[0].tag) child3
ElementTree 1.3とlxml 2.0以前は、Elementが子要素を持つかどうかを真偽値で判定できました。すなわち、子要素のリストが空なら、
if root: # 今は廃止されました print("The root element has children")
しかし子要素を持っているかどうかというよりは、存在する「もの」は真と判定されるだろう、Elementは存在する「もの」である(から真と判定される)だろうと考えてしまうという理由で、これは廃止されました。つまりElementが上のような形のどんなif文であっても偽と判定されるのは多くのユーザーを驚かせるでしょうという理由です。その代わり、より明確でエラーの出にくいlen(element)
を使いましょう。
>>> print(etree.iselement(root)) # 何らかのElementであるかどうかを調べる True >>> if len(root): # 子要素をもつかどうかを調べる ... print("The root element has children") The root element has children
lxml(2.0以降)のElementの振る舞いが、リストの振る舞いやオリジナルのElementTree(version 1.3またはPython2.7/3.2以前)の振る舞いとはズレるという別の重要な問題もあります:
>>> for child in root: ... print(child.tag) child0 child1 child2 child3 >>> root[0] = root[-1] # lxml.etreeの要素を代入 >>> for child in root: ... print(child.tag) child3 child1 child2
この例では、最後の要素はコピーされるのではなく別の位置に動いてしまっています。つまり、別の位置に代入したときに以前の位置からは自動的に削除されるのです。リストでは、オブジェクトは同時に別の位置に存在でき、上の場合は単にアイテムの参照を最初の位置にコピーするだけでどちらも完全に同じものとして保持されます:
>>> l = [0, 1, 2, 3] >>> l[0] = l[-1] >>> l [3, 1, 2, 3]
オリジナルのElementTreeでは1つのElementオブジェクトはどのツリーのどの場所にも存在できるので、リストのときと同じコピー操作が発生することに注意して下さい。意図していたとしてもいなかったとしても、このようなElementに関する変更がツリーに現れる全ての場所のElementに適用されることは明らかな欠点です。
この相違点の上に述べた方(Elementオブジェクトの操作の方)によって、Elementがlxml.etree内では常に1つの親要素しかもたないので、getparent
メソッドを使うことができます。これはオリジナルのElementTreeでは実装されていません。
>>> root is root[0].getparent() # lxml.etreeだけ True
lxml.etreeで要素を別の位置へコピーしたいときは、Pythonの標準ライブラリであるcopyモジュールを用いて独立な深いコピーを作成することを考えて下さい:
>>> from copy import deepcopy >>> element = etree.Element("neu") >>> element.append( deepcopy(root[1]) ) >>> print(element[0].tag) child1 >>> print([ c.tag for c in root ]) ['child3', 'child1', 'child2']
要素の親戚(近所)へはnext
やprevious
要素としてアクセスできます:
>>> root[0] is root[1].getprevious() # lxml.etreeだけ True >>> root[1] is root[0].getnext() # lxml.etreeだけ True
要素は属性を辞書としてもつ
XML要素は属性をサポートしています。Elementファクトリー内で直接作成することができます:
>>> root = etree.Element("root", interesting="totally") >>> etree.tostring(root) b'<root interesting="totally"/>'
属性は順序の関係ない名前と値のペアに過ぎないので、要素の辞書のようなインターフェイスを用いると非常に便利です:
>>> print(root.get("interesting")) totally >>> print(root.get("hello")) None >>> root.set("hello", "Huhu") >>> print(root.get("hello")) Huhu >>> etree.tostring(root) b'<root interesting="totally" hello="Huhu"/>' >>> sorted(root.keys()) ['hello', 'interesting'] >>> for name, value in sorted(root.items()): ... print('%s = %r' % (name, value)) hello = 'Huhu' interesting = 'totally'
アイテムを検索したいときや、「本当」の辞書のようなオブジェクトを得たい理由(何かに渡すなど)があるときは、attrib
プロパティを使いましょう:
>>> attributes = root.attrib >>> print(attributes["interesting"]) totally >>> print(attributes.get("no-such-attribute")) None >>> attributes["hello"] = "Guten Tag" >>> print(attributes["hello"]) Guten Tag >>> print(root.get("hello")) Guten Tag
attrib
はElement自身によって提供されている辞書のようなオブジェクトであることに注意して下さい。これはつまりElementを変更するとattribに反映され、逆にattribを変更するとElementにそれが反映されるということです。また、Elementのattribが使われる限りXMLツリーがメモリに残り続けることも意味しています。XMLツリーに依存しない独立した属性を得たい時は辞書へコピーしてください:
>>> d = dict(root.attrib) >>> sorted(d.items()) [('hello', 'Guten Tag'), ('interesting', 'totally')]
要素はテキストをもつ
要素は(プレーンな/XMLのタグとは無関係な)テキストをもっています:
>>> root = etree.Element("root") >>> root.text = "TEXT" >>> print(root.text) TEXT >>> etree.tostring(root) b'<root>TEXT</root>'
たくさんの(データ中心の)XML文書においては、テキストが見られるのはここだけです。ツリー階層の一番下に保持されています*2。
しかし、XMLが(X)HTMLのようなタグづけされたテキスト文書として使われるとき、テキストは別のタグの間、ツリーの途中にも現れるはずです:
<html><body>Hello<br/>World</body></html>
ここでは、<br/>
タグがテキストに囲まれています。これはドキュメントスタイルのXMLやコンテンツが混ざったXMLでしばしばあることです。Elementはtail
プロパティを利用してこれをサポートしています。このプロパティは直接要素に続くテキストをXMLツリーの次の要素のところまで保持しています:
>>> html = etree.Element("html") >>> body = etree.SubElement(html, "body") >>> body.text = "TEXT" >>> etree.tostring(html) b'<html><body>TEXT</body></html>' >>> br = etree.SubElement(body, "br") >>> etree.tostring(html) b'<html><body>TEXT<br/></body></html>' >>> br.tail = "TAIL" >>> etree.tostring(html) b'<html><body>TEXT<br/>TAIL</body></html>'
XML文書のどんなテキストコンテンツを表現するのにも.text
と.tail
の2つのプロパティがあれば十分です。これによって、ElementTree APIはElementクラスに加えて、(古典的なDOM APIで見られるような)よく邪魔になるテキスト用の特別なノードを必要としません。
しかし、tailテキストが邪魔になる場合もあります。例えば、ツリーの中から要素をシリアライズするとき、(子要素のtailテキストが欲しいのであったとしても)結果にtailテキストが必ずしも必要なわけではないでしょう。こういった目的のため、tostring
函数はwith_tail
をキーワード引数としてもちます:
>>> etree.tostring(br) b'<br/>TAIL' >>> etree.tostring(br, with_tail=False) # lxml.etreeだけ b'<br/>'
テキストだけを読みたい時、つまり途中に挟まったどんなタグも必要ないときは、全てのテキストとtail属性を正しい順番で再帰的につなげてやらなくてはいけません。再びtostring
函数が役に立ちます。今回はmethod
キーワードを使いましょう:
>>> etree.tostring(html, method="text") b'TEXTTAIL'
テキスト検索のためのXPath
ツリーのテキストを吐くための別の方法がXPathです。XPathによって、異なるテキストのかたまりをリストへ変換することもできます:
>>> print(html.xpath("string()")) # lxml.etree only! TEXTTAIL >>> print(html.xpath("//text()")) # lxml.etree only! ['TEXT', 'TAIL']
これをよく使うのなら函数にラップしましょう:
>>> build_text_list = etree.XPath("//text()") # lxml.etree only! >>> print(build_text_list(html)) ['TEXT', 'TAIL']
XPathによって返される結果の文字列は特別な「賢い」オブジェクトで、元のオブジェクトのことを知っていることに注意して下さい。getparent
メソッドを使ってElementのときと同じように、元のオブジェクトの情報を得ることができます:
>>> texts = build_text_list(html) >>> print(texts[0]) TEXT >>> parent = texts[0].getparent() >>> print(parent.tag) body >>> print(texts[1]) TAIL >>> print(texts[1].getparent().tag) br You can also find out if it's normal text content or tail text: >>> print(texts[0].is_text) True >>> print(texts[1].is_text) False >>> print(texts[1].is_tail) True
text
函数の結果に対してはこれは期待通りに動く一方、lxmlではstring
やconcat
などのXPath函数によって得られた文字列値の元のオブジェクトは得られない:
>>> stringify = etree.XPath("string()") >>> print(stringify(html)) TEXTTAIL >>> print(stringify(html).getparent()) None
ツリーイテレーション
再帰的にツリーを通過して各要素に対して何かをしたい上のような場合、ツリーイテレーションが非常に便利な解決法です。Elementはこのためのツリーイテレーションを提供します。これは文書の順番、つまりツリーをXMLにシリアライズしたときにタグが現れる順番に要素を出力してくれます:
>>> root = etree.Element("root") >>> etree.SubElement(root, "child").text = "Child 1" >>> etree.SubElement(root, "child").text = "Child 2" >>> etree.SubElement(root, "another").text = "Child 3" >>> print(etree.tostring(root, pretty_print=True)) <root> <child>Child 1</child> <child>Child 2</child> <another>Child 3</another> </root> >>> for element in root.iter(): ... print("%s - %s" % (element.tag, element.text)) root - None child - Child 1 child - Child 2 another - Child 3
もしも1つのタグにだけ興味があるときは、名前をiter
に渡すことでフィルタリングすることができます。lxml 3.0以降では、イテレーション中に複数のタグを取得するために複数のタグを渡すこともできます:
>>> for element in root.iter("child"): ... print("%s - %s" % (element.tag, element.text)) child - Child 1 child - Child 2 >>> for element in root.iter("another", "child"): ... print("%s - %s" % (element.tag, element.text)) child - Child 1 child - Child 2 another - Child 3
デフォルトでは、イテレーションはツリーのXML処理命令((XML文書における<? hogehoge ?>
みたいなやつ))、コメント、エンティティインスタンスを含む全てのノードを吐きます。Elementオブジェクトだけが返ってくることが分かっている場合、Elementファクトリーにtag
パラメータを渡すこともできます:
>>> root.append(etree.Entity("#234")) >>> root.append(etree.Comment("some comment")) >>> for element in root.iter(): ... if isinstance(element.tag, basestring): ... print("%s - %s" % (element.tag, element.text)) ... else: ... print("SPECIAL: %s - %s" % (element, element.text)) root - None child - Child 1 child - Child 2 another - Child 3 SPECIAL: ê - ê SPECIAL: <!--some comment--> - some comment >>> for element in root.iter(tag=etree.Element): ... print("%s - %s" % (element.tag, element.text)) root - None child - Child 1 child - Child 2 another - Child 3 >>> for element in root.iter(tag=etree.Entity): ... print(element.text) ê
タグの名前としてワイルドカード "*" を渡すと全ての要素ノード(そして要素のみ)を吐きます。 lxml.etreeにおいて、要素はツリーのあらゆる方向へのより複雑なイテレーションを提供します。つまり、子要素へ、親要素(やもっと上位の要素)へ、近所へのアクセスができます。
シリアライゼーション
シリアライゼーションには、文字列を返すtostring
函数やファイル・ファイル系オブジェクト・(FTP PUTあるいはHTTP POSTを介した)URLへの書き込みを行うElementTree.write
メソッドがよく使われます。どちらの呼び出しも、整形された出力をするためのpretty_print
とプレーンASCIIではない特定の出力エンコーディングを指定するためのencoding
など、同じキーワード引数を受け付けます:
>>> root = etree.XML('<root><a><b/></a></root>') >>> etree.tostring(root) b'<root><a><b/></a></root>' >>> print(etree.tostring(root, xml_declaration=True)) <?xml version='1.0' encoding='ASCII'?> <root><a><b/></a></root> >>> print(etree.tostring(root, encoding='iso-8859-1')) <?xml version='1.0' encoding='iso-8859-1'?> <root><a><b/></a></root> >>> print(etree.tostring(root, pretty_print=True)) <root> <a> <b/> </a> </root>
pretty printでは最後に改行が追加されることに注意して下さい。
(ElementTree 1.3同様)lxml 2.0以降では、シリアライゼーション函数はXMLシリアライゼーション以外も可能です。method
キーワードを指定することでHTMLへのシリアライズやテキストを吐き出すこともできます:
>>> root = etree.XML( ... '<html><head/><body><p>Hello<br/>World</p></body></html>') >>> etree.tostring(root) # default: method = 'xml' b'<html><head/><body><p>Hello<br/>World</p></body></html>' >>> etree.tostring(root, method='xml') # same as above b'<html><head/><body><p>Hello<br/>World</p></body></html>' >>> etree.tostring(root, method='html') b'<html><head></head><body><p>Hello<br>World</p></body></html>' >>> print(etree.tostring(root, method='html', pretty_print=True)) <html> <head></head> <body><p>Hello<br>World</p></body> </html> >>> etree.tostring(root, method='text') b'HelloWorld'
XMLシリアライゼーションでは、プレーンテキストのデフォルトエンコーディングはASCIIです:
>>> br = next(root.iter('br')) # get first result of iteration >>> br.tail = u'W\xf6rld' >>> etree.tostring(root, method='text') # doctest: +ELLIPSIS Traceback (most recent call last): ... UnicodeEncodeError: 'ascii' codec can't encode character u'\xf6' ... >>> etree.tostring(root, method='text', encoding="UTF-8") b'HelloW\xc3\xb6rld'
バイト文字列の代わりにPythonのunicode文字列へのシリアライズするとより扱いやすいです。unicode型をencoding
へ渡すだけです:
>>> etree.tostring(root, encoding=unicode, method='text') u'HelloW\xf6rld'
参考として、W3CによるUnicode文字列のセットと文字エンコーディングに関する記事もあります。
ElementTreeクラス
ElementTreeは主に、ルートにノードを持つツリーに関するドキュメントラッパーです。これはシリアライゼーションと一般的なドキュメントを扱うためのメソッド群を提供します:
>>> root = etree.XML('''\ ... <?xml version="1.0"?> ... <!DOCTYPE root SYSTEM "test" [ <!ENTITY tasty "parsnips"> ]> ... <root> ... <a>&tasty;</a> ... </root> ... ''') >>> tree = etree.ElementTree(root) >>> print(tree.docinfo.xml_version) 1.0 >>> print(tree.docinfo.doctype) <!DOCTYPE root SYSTEM "test">
ElementTreeはファイルやファイル系オブジェクト(下のパースの節を参照)をパースするためのparse
函数を呼んだときの戻り値でもあります。
重要な違いの一つとして、ElementTreeクラスはElementクラスとは違って完全なドキュメントとしてシリアライズします。これは、文書内のDOCTYPE宣言や他のDTDと同様にXML処理命令やコメントをトップレベルで処理することを含んでいます。
>>> print(etree.tostring(tree)) # lxml 1.3.4 and later <!DOCTYPE root SYSTEM "test" [ <!ENTITY tasty "parsnips"> ]> <root> <a>parsnips</a> </root>
文字列やファイルのパース
lxml.etreeは文字列、ファイル、URL(http/ftp)、ファイル系オブジェクトといったあらゆる重要な形式からXMLのパースを行なう様々な手段をサポートしています。主なパース函数はfromstring
とparse
ですが、どちらもデータソース(上に述べた文字列、ファイル…のこと)を第1引数に渡して呼びます。デフォルトでは標準パーサを用いますが、第2引数としていつでも異なるパーサを渡すことができます。
fromstring
函数
>>> some_xml_data = "<root>data</root>" >>> root = etree.fromstring(some_xml_data) >>> print(root.tag) root >>> etree.tostring(root) b'<root>data</root>'
XML
函数
XML
函数はfromstring
函数のように振る舞いますが、XMLリテラルをデータソースに書き込むためによく使われます。
>>> root = etree.XML("<root>data</root>") >>> print(root.tag) root >>> etree.tostring(root) b'<root>data</root>'
また、HTMLリテラルに対応したHTML
函数もあります。
parse
函数
ファイル系オブジェクトといったものの例のように、以下のコードは外部ファイルの代わりに文字列から読むためBytesIO
クラスを使っています。このクラスはPython2.6以降のioモジュールにあります。より古いバージョンのPythonでは、StringIOモジュールのStringIO
クラスを使わなければいけません。しかし実際、明らかにこれらを一緒に使うことは避け、上の文字列パース函数を使うべきでしょう。
>>> some_file_like_object = BytesIO("<root>data</root>") >>> tree = etree.parse(some_file_like_object) >>> etree.tostring(tree) b'<root>data</root>'
parse
は文字列パース函数のときのようなElementオブジェクトではなくElementTreeオブジェクトを返すことに注意して下さい。
>>> root = tree.getroot() >>> print(root.tag) root >>> etree.tostring(root) b'<root>data</root>'
この違いの裏にはparse
がファイルから完全な文書を返すのに対し文字列パース函数は普通は断片的なXMLをパースするのに使われるというわけがあります。
parse
函数は以下のデータソース全てをサポートしています:
- openファイルオブジェクト(バイナリモードで開いていることを確認して下さい)
- 呼ばれる度にバイト文字列を返す
.read(byte_count)
メソッドを持ったファイル系オブジェクト - ファイル名文字列
- HTTPまたはFTPのURL文字列
ファイル名やURLを渡すのは、オープンファイルやファイル系オブジェクトを渡すよりも速いことが多いことに注意して下さい。しかし、libxml2
のHTTP/FTPクライアントはかなり単純なので、HTTP認証などはURLリクエスト専用のurllib2
やrequest
といったライブラリが必要です。これらのライブラリは通常、responseが返ってきている間にパース可能なファイル系オブジェクトを結果として提供します。
パーサオブジェクト
インクリメンタルパース
イベントドリブンパース
名前空間
E-ファクトリー
ElementPath
ElementTreeライブラリはシンプルなXPathに近い、ElementPathと呼ばれるパス言語を備えています。最大の違いは、ElementPathの表記では{namespace}
タグ記法が使えることでしょう。しかし、値の比較や函数といった高度な機能は使えません。
完全なXPathの実装に加えて、lxml.etreeは(ほとんど)同じ実装を使っているとはいえElementTreeと同じ方法でElementPath言語のサポートもしています。APIはElementとElementTreeで、以下の4つのメソッドを提供しています:
iterfind
はパス条件にマッチする全ての要素をイテレートしますfindall
はマッチした要素のリストを返しますfind
は最初にマッチしたものを効率良く返しますfindtext
は最初にマッチしたものの.text
の内容を返します
以下に例をいくつか示します。
>>> root = etree.XML("<root><a x='123'>aText<b/><c/><b/></a></root>")
要素の子要素を探す:
>>> print(root.find("b")) None >>> print(root.find("a").tag) a
ツリー内の全ての要素を探す:
>>> print(root.find(".//b").tag) b >>> [ b.tag for b in root.iterfind(".//b") ] ['b', 'b']
特定の属性を持つ要素を探す:
>>> print(root.findall(".//a[@x]")[0].tag) a >>> print(root.findall(".//a[@y]")) []
.iter
メソッドはツリー内の特定のタグを名前を使って探すだけで、パスによってではありません。これはつまり以下のコマンドは成功する場合は等価になります:
>>> print(root.find(".//b").tag) b >>> print(next(root.iterfind(".//b")).tag) b >>> print(next(root.iter("b")).tag) b
.find
メソッドは何にもマッチするものがなければNone
を返すだけだが、他の2つの例はStopIteration
エラーを返すことに注意して下さい。