読者です 読者をやめる 読者になる 読者になる

Just $ A sandbox

プログラミングと計算機科学とかわいさ

lxmlチュートリアル翻訳してみた

PythonXML(HTML)を扱う高速で便利なライブラリがlxmlです。 非常に強力なメソッドが多数用意されているのですが、日本語の情報があまりないのが弱点です。なので今回、lxml.etree公式チュートリアルの一部を勝手に翻訳しました。

量が多いので全ては訳しきれていませんが*1、lxmlでおそらく一番よく使われるであろう、Elementクラスの部分を中心に訳を作りました。誰かのお役に立てれば幸いです。

また、翻訳にあたっては直訳よりも日本語として自然な訳を選んだ部分があります。正しくは原文を参照してください。ここの訳がおかしい、日本語が不自然、この文書も訳せ等ありましたらお知らせください。


lxml.etree チュートリアル

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']

要素の親戚(近所)へはnextprevious要素としてアクセスできます:

>>> 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ではstringconcatなどの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: &#234; - &#234;
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)
&#234;

タグの名前としてワイルドカード "*" を渡すと全ての要素ノード(そして要素のみ)を吐きます。 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'

バイト文字列の代わりにPythonunicode文字列へのシリアライズするとより扱いやすいです。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のパースを行なう様々な手段をサポートしています。主なパース函数はfromstringparseですが、どちらもデータソース(上に述べた文字列、ファイル…のこと)を第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リクエスト専用のurllib2requestといったライブラリが必要です。これらのライブラリは通常、responseが返ってきている間にパース可能なファイル系オブジェクトを結果として提供します。

パーサオブジェクト

Parser objects

インクリメンタルパース

Incremental parsing

イベントドリブンパース

Event-driven parsing

名前空間

Namespaces

E-ファクトリー

The E-factory

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エラーを返すことに注意して下さい。

*1:特にパーサに関する部分が不完全です

*2:原文: It is encapsulated by a leaf tag at the very bottom of the tree hierarchy. 直訳だと、木階層の一番下で、`葉'タグによってカプセルに入れられています