xml
の一般的な用例として、
ブログやフォーラム、その他の頻繁に更新されるウェブサイトの最新記事をリストアップするのに使われる「フィード」がある。
有名なブログ用ソフトウェアのほとんどは、
フィードを生成して、新しい記事やスレッドや投稿が公開されるたびにフィードの内容を更新する機能を備えている。
皆さんがそのブログのフィードを「購読」すれば、
そのブログの更新を追うことができ、
Google Readerのような専用の「フィードアグリゲータ」を使うことで多数のブログを追いかけることもできる。
この章で扱っていくxmlデータがある。 このデータはフィード — 具体的に言うとAtomフィード というもの。
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title>
<subtitle>currently between addictions</subtitle>
<id>tag:diveintomark.org,2001-07-29:/</id>
<updated>2009-03-27T21:56:07Z</updated>
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/>
<link rel='self' type='application/atom+xml' href='http://diveintomark.org/feed/'/>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
<updated>2009-03-27T21:56:07Z</updated>
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/>
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Accessibility is a harsh mistress</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress'/>
<id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>
<updated>2009-03-22T01:05:37Z</updated>
<published>2009-03-21T20:09:28Z</published>
<category scheme='http://diveintomark.org' term='accessibility'/>
<summary type='html'>The accessibility orthodoxy does not permit people to
question the value of features that are rarely useful and rarely used.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
</author>
<title>A gentle introduction to video encoding, part 1: container formats</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats'/>
<id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>
<updated>2009-01-11T19:39:22Z</updated>
<published>2008-12-18T15:54:22Z</published>
<category scheme='http://diveintomark.org' term='asf'/>
<category scheme='http://diveintomark.org' term='avi'/>
<category scheme='http://diveintomark.org' term='encoding'/>
<category scheme='http://diveintomark.org' term='flv'/>
<category scheme='http://diveintomark.org' term='GIVE'/>
<category scheme='http://diveintomark.org' term='mp4'/>
<category scheme='http://diveintomark.org' term='ogg'/>
<category scheme='http://diveintomark.org' term='video'/>
<summary type='html'>These notes will eventually become part of a
tech talk on video encoding.</summary>
</entry>
</feed>
xml
は、階層構造をもつデータを記述するための汎用的な手法。
xml文書 (document) は1つ以上の要素 (element)を含んでおり、
これらの要素は開始タグと終了タグによって区切られる。
以下は、完全なxml文書だ:
<foo> 1
</foo> 2
foo要素の開始タグになる。
foo要素の開始タグに対応する終了タグ。
文章や数学やコードにおいて括弧を釣り合わせるのと同じように、すべての開始タグは対応する終了タグによって閉じられなければならない。
要素はどんな深さにまでネストされていても良い。 foo要素の内部にあるbar要素は、fooの子 (child) ないし下位要素 (subelement) と呼ばれる。
<foo>
<bar></bar>
</foo>
あらゆるxml文書において、一番初めの要素はルート要素 (root element) と呼ばれる。
xml
はルート要素を1つだけ持つことができる。
以下は2つのルート要素を持っているので、xml文書ではない。
<foo></foo>
<bar></bar>
要素は属性 (attributes
) を持つことができる。
属性というのは名前と値のペアだ。
属性は、開始タグのなかで、空白で区切られて列挙される。
同じ属性名を1つの要素の中で繰り返し使うことは許されず、属性値はクォート文字で囲む必要がある。
クォートは、シングルクォートとダブルクォートのどちらでもかまわない。
<foo lang='en'> # 1
<bar id='papayawhip' lang="fr"></bar> # 2
</foo>
foo要素は、langという名前の1つの属性を持っている。このlang属性の値はenになる
bar要素は、id
および lang
という名前の2つの属性を持っている。
lang属性の値はfrになっているが、 これがfoo要素のlang属性と衝突することは絶対にない。 個々の要素の属性は、その要素に固有のもの。
要素が2つ以上の属性を持っているとき、それらの属性の並び順は意味を持たない。 要素の属性というのは、Pythonの辞書のような順序づけされていないキーと値の集合だととらえられる。 なお、個々の要素の上に定義できる属性の数に制限はない。
要素はテキスト内容 (text content) を持つことができる。
<foo lang='en'>
<bar lang='fr'>PapayaWhip</bar>
</foo>
テキストも子も持たない要素は空要素 (empty element) になる。
<foo></foo>
空要素を書くための省略表現がある。 /を開始タグに付けることで、 終了タグを完全に省略することができる。 先ほどのxml文書は、代わりに次のように書くことができる:
<foo/>
Pythonの関数を異なるモジュールに定義できるのと同様に、
xml要素も異なる名前空間 (namespace)に定義することができる。
名前空間は普通はURLのような見た目をしている。
デフォルトの名前空間を定義するには、xmlns
宣言を使用する。
名前空間の宣言は属性に似ているが、異なる目的を持っている。
<feed xmlns='http://www.w3.org/2005/Atom'> 1
<title>dive into mark</title> 2
</feed>
http://www.w3.org/2005/Atom
名前空間の中にある。http://www.w3.org/2005/Atom
名前空間の中にある。名前空間の宣言は、それが定義された要素に加えて、すべての子要素にも影響する。
xmlns:prefix
宣言を使うと、
宣言した名前空間を接頭辞 (prefix)に関連づけることができる。
その場合、その名前空間にある各要素は、明示的に接頭辞を付けて宣言しなければならなくなる。
<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'> 1
<atom:title>dive into mark</atom:title> 2
</atom:feed>
http://www.w3.org/2005/Atom
名前空間の中にある。http://www.w3.org/2005/Atom
名前空間の中にある。xmlパーサにとっては、先に示した2つのxml文書は同一。
名前空間 + 要素名 = xmlの同一性。
接頭辞は名前空間を参照するためだけに存在しているので、
接頭辞の名前 (atom:
) 自体は意味を持たない。
名前空間が一致し、要素名が一致し、属性の有無と値が一致し、
各々の要素のテキスト内容が一致していれば、xml文書は同一。
xml文書の最初の行(つまりルート要素の前)には文字コード情報を入れることができる。
<?xml version='1.0' encoding='utf-8'?>
頻繁に更新されるコンテンツを持ったウェブサイトなら何でもいい。 このようなサイトとしては例えば、CNN.comがある。 このサイトは、
それぞれの記事もまた、タイトルと、最初に投稿された日時(公開後に訂正されたり誤字が修正された場合は最終更新日時も)と、固有のURLを持っている。
Atom
形式は、このすべての情報を標準形式で記録するように設計されている。
ブログとCNN.comは、デザイン・対象領域・読者の点で大きく異なっているが、
両者とも同じような基本構造を持っている。
CNN.comはタイトルを持ち、ブログもタイトルを持つ。
CNN.comは記事を公開し、ブログも記事を公開する。
トップレベルにあるのはルート要素だ。
この要素(http://www.w3.org/2005/Atom
名前空間のfeed要素)はどのAtomフィードでも同じになる。
<feed xmlns='http://www.w3.org/2005/Atom' 1
xml:lang='en'> 2
http://www.w3.org/2005/Atom
はAtomの名前空間だ。xml:lang
属性を加えることができる。この属性は、その要素とその子要素の言語を宣言する。
この例では、xml:lang
属性はルート要素で一度だけ宣言されているので、
フィード全体が英語で書かれていることがわかる。
Atom
フィードの中にはフィード自身に関する情報もいくつか存在している。
これらは、ルートレベルのfeed
要素の子供として宣言されている。
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title> 1
<subtitle>currently between addictions</subtitle> 2
<id>tag:diveintomark.org,2001-07-29:/</id> 3
<updated>2009-03-27T21:56:07Z</updated> 4
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/> 5
この識別子はネット上においてユニークでなければならない。
これの作り方については「RFC 4151」に書いてある。
4. このフィードが最後に更新されたのは2009年3月27日の21:56 GMT。
通常、これは最新記事の最終変更日時と等しい。
5. このlink要素はテキスト内容を持っていないが、
3つの属性rel, type, hrefを持っている。relの値はこのリンクの種類を伝えている。rel='alternate'というのは、
これがこのフィードの代替表現へのリンクだということを意味している。
type='text/html'
属性は、これがhtmlページへのリンクだということを意味している。
そして、リンクのターゲットはhref
属性で与えられている。
このサイトは、 “dive into mark“ という名前のサイトのフィードであり、
そのサイトは http://diveintomark.org/
で利用でき、
そのサイトの最終更新日は2009年3月27日だということがわかる。
一部のxml文書では要素の順序が意味を持つことがあるが、
「Atom
フィード」では要素の順序は意味を持たない。
フィード自体に関するメタデータの後は、最新記事のリストになっている。 一つ一つの記事は次のように表されている:
<entry>
<author> 1
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title> 2
<link rel='alternate' type='text/html' 3
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id> 4
<updated>2009-03-27T21:56:07Z</updated> 5
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/> 6
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds 7
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry>
author
要素は、誰がこの記事を書いたのかを伝えている。そいつはMarkという名前で、http://diveintomark.org/
に行けばぶらついている彼に会うことができる(このURLはフィードのメタデータにある代替リンクと同じだが、かならずしも同じである必要はない。
実際、多くのウェブログは、自分のウェブサイトを持つ執筆者が何人か集まって書いている)。
title
要素は記事のタイトル “Dive into history, 2009 edition” を表している。
フィードレベルの代替リンクと同様に、このlink要素は記事のhtml版のアドレスを示している。
フィードと同様に、エントリには固有の識別子(id) が必要。
エントリは2つの日付を持っている:
エントリは任意の数のカテゴリを持つことができる。
この記事はdiveintopython、docbook、htmlに分類されている。
summary
要素は記事の短い要約を与える(ここには示されていないが、フィードに記事の全文を含めたい場合のためのcontent要素も存在する)。このsummary要素にはAtom特有のtype='html'属性が含まれており、
これはこの要約がプレーンテキストではなくhtmlで書かれていることを示している。
なぜなら、この要約には、html
特有の実体参照(—と…)が含まれているから。
これらの記号は、そのままの形ではなく、
“—” と “……” に変換した上で表示しなくてはならない。
Pythonでxml
文書をパースする方法は何種類かある。
これまで伝統的に用いられてきたdom
パーサとsax
パーサも使えるが、
ここではElementTree
と呼ばれる別のライブラリを使う。
examples/feed.xml
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into mark</title>
<subtitle>currently between addictions</subtitle>
<id>tag:diveintomark.org,2001-07-29:/</id>
<updated>2009-03-27T21:56:07Z</updated>
<link rel='alternate' type='text/html' href='http://diveintomark.org/'/>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Dive into history, 2009 edition</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition'/>
<id>tag:diveintomark.org,2009-03-27:/archives/20090327172042</id>
<updated>2009-03-27T21:56:07Z</updated>
<published>2009-03-27T17:20:42Z</published>
<category scheme='http://diveintomark.org' term='diveintopython'/>
<category scheme='http://diveintomark.org' term='docbook'/>
<category scheme='http://diveintomark.org' term='html'/>
<summary type='html'>Putting an entire chapter on one page sounds
bloated, but consider this &mdash; my longest chapter so far
would be 75 printed pages, and it loads in under 5 seconds&hellip;
On dialup.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
<uri>http://diveintomark.org/</uri>
</author>
<title>Accessibility is a harsh mistress</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress'/>
<id>tag:diveintomark.org,2009-03-21:/archives/20090321200928</id>
<updated>2009-03-22T01:05:37Z</updated>
<published>2009-03-21T20:09:28Z</published>
<category scheme='http://diveintomark.org' term='accessibility'/>
<summary type='html'>The accessibility orthodoxy does not permit people to
question the value of features that are rarely useful and rarely used.</summary>
</entry>
<entry>
<author>
<name>Mark</name>
</author>
<title>A gentle introduction to video encoding, part 1: container formats</title>
<link rel='alternate' type='text/html'
href='http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats'/>
<id>tag:diveintomark.org,2008-12-18:/archives/20081218155422</id>
<updated>2009-01-11T19:39:22Z</updated>
<published>2008-12-18T15:54:22Z</published>
<category scheme='http://diveintomark.org' term='asf'/>
<category scheme='http://diveintomark.org' term='avi'/>
<category scheme='http://diveintomark.org' term='encoding'/>
<category scheme='http://diveintomark.org' term='flv'/>
<category scheme='http://diveintomark.org' term='GIVE'/>
<category scheme='http://diveintomark.org' term='mp4'/>
<category scheme='http://diveintomark.org' term='ogg'/>
<category scheme='http://diveintomark.org' term='video'/>
<summary type='html'>These notes will eventually become part of a
tech talk on video encoding.</summary>
</entry>
</feed>
ElementTree
ライブラリはPython標準ライブラリの一部であり、
xml.etree.ElementTree
に含まれている。
import xml.etree.ElementTree as etree
ElementTree
ライブラリのメインのエントリポイントは parse()
関数であり、
この関数はファイル名、もしくはファイルライクオブジェクトを引数に取る。
この関数は文書全体を一度にパースするが、
メモリが限られている場合は、xml文書をインクリメンタルにパースする方法も用意されている。
tree = etree.parse('examples/feed.xml')
tree
<xml.etree.ElementTree.ElementTree at 0x1c1ab399ef0>
parse()
関数は文書全体を表すオブジェクトを返す。
このオブジェクトはルート要素ではない。
ルート要素の参照を取得するには、getroot()
メソッドを呼び出す。
root = tree.getroot()
ルート要素は、
http://www.w3.org/2005/Atom
名前空間にあるfeed
要素になる。
このオブジェクトの文字列表現は重要なポイントを強調している。
すなわち、xml要素というのは名前空間とタグ名(ローカル名とも呼ばれる)の組み合わせになる。この文書のすべてのタグはAtomの名前空間の中にあるので、ルート要素は{http://www.w3.org/2005/Atom
}feedと表現される。
root
<Element '{http://www.w3.org/2005/Atom}feed' at 0x000001C1AB2D0EF8>
ElementTree
は、xml要素を{namespace}localname
と表現する。
この形式は、ElementTree apiの様々な場所で見たり使ったりすることになる。
ElementTree APIでは、要素はリストのように振る舞う。 そのリストのアイテムは、その要素の子要素になっている。
ルート要素は {http://www.w3.org/2005/Atom}feed
になっている。
root.tag
'{http://www.w3.org/2005/Atom}feed'
ルート要素の「長さ」は子要素の数になる。
len(root)
8
要素自体をイテレータとして使って、すべての子要素を取得することもできる。
このfeedでは、8つの子要素がある。 最初の5つにはフィードのメタデータがすべて入っており (title, subtitle, id, updated, link)、 その後に3つのentry要素が続いている。
for child in root:
print(child)
<Element '{http://www.w3.org/2005/Atom}title' at 0x000001C1AB39ED68> <Element '{http://www.w3.org/2005/Atom}subtitle' at 0x000001C1AB39EDB8> <Element '{http://www.w3.org/2005/Atom}id' at 0x000001C1AB39EEA8> <Element '{http://www.w3.org/2005/Atom}updated' at 0x000001C1AB39EEF8> <Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB39EF98> <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3A5048> <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3A54F8> <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3A58B8>
子要素のリストは直接の子だけを含んでいる。
各々の entry
要素はさらに子要素を持っているが、それらはこのリストには含まれていない。それらは各々のentry
の子のリストには含まれるだろうが、feedの子供のリストには含まれない。
ネストされている要素を、たとえどれだけ深くネストされていても、
見つけ出す方法は存在する。
xml
は要素だけで成り立っているわけではない。
個々の要素には、それぞれ属性をつけることができる。
特定の要素への参照を取得すれば、
その要素の属性をPythonの辞書として簡単に取得することができる。
attrib
プロパティは、この要素の属性の辞書になる。
元々のマークアップは
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
である。
xml:接頭辞
は、すべてのxml文書が宣言なしに使用できる組み込みの名前空間を参照している。
root.attrib
{'{http://www.w3.org/XML/1998/namespace}lang': 'en'}
今回の場合、5番目の子(インデックスが0から始まるリストの[4])はlink要素になる。
root[4]
<Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB39EF98>
link要素は3つの属性を持っている: href
、type
、rel
。
root[4].attrib
{'href': 'http://diveintomark.org/', 'rel': 'alternate', 'type': 'text/html'}
4番目の子(インデックスが0から始まるリストの[3])はupdated要素になる。
root[3]
<Element '{http://www.w3.org/2005/Atom}updated' at 0x000001C1AB39EEF8>
updated要素は属性を持っていないので、.attribは単なる空の辞書になる。
root[3].attrib
{}
ここまでは、xml文章を「トップダウン」方式で扱ってきた。
ルート要素から始めて、その子要素を取得して、という過程を文書全体で繰り返していく手法だった。
しかし、xmlの多くの用途では、特定の要素を検索することが必要になる。
ElementTree
は、もちろんそれを行うことができる。
import xml.etree.ElementTree as etree
tree = etree.parse('examples/feed.xml')
findall()メソッドは、指定されたクエリにマッチする子要素を見つけ出す。
root.findall('{http://www.w3.org/2005/Atom}entry')
[<Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3A5048>, <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3A54F8>, <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3A58B8>]
すべての要素(ルート要素だけでなく子要素も)が findall()
メソッドを持っている。
このメソッドはクエリにマッチするすべての要素を子要素の中から見つけ出す。
このクエリは要素の子供だけを検索する。
ルート要素feedは、feedという名前の子を持っていないので、このクエリは空のリストを返す。
root.findall('{http://www.w3.org/2005/Atom}feed')
[]
この文書の中にauthor要素は3つ存在する(それぞれのentryにある)。 しかし、これらのauthor要素はルート要素の直接の子供ではなく、「孫」(文字通り、子要素の子要素)にあたる。すべてのネストレベルのauthor要素を探したいのであれば、もちろんそれは可能だが、クエリの形式は少し違ったものになる。
root.findall('{http://www.w3.org/2005/Atom}author')
[]
(etree.parse()
関数から返される)treeオブジェクトには、
ルート要素のメソッドがいくつかそのまま移植されている。
このメソッドを実行すると、
tree.getroot().findall()
を呼び出したときとまったく同じ結果がえられる。
import xml.etree.ElementTree as etree
tree = etree.parse('examples/feed.xml')
tree.findall('{http://www.w3.org/2005/Atom}entry')
[<Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3B2278>, <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3B2728>, <Element '{http://www.w3.org/2005/Atom}entry' at 0x000001C1AB3B2AE8>]
これは tree.getroot().findall('{http://www.w3.org/2005/Atom}author')
のショートカットに過ぎず、「ルート要素の子供のauthor要素を探す」ことを意味しているので空辞書を返す。
author
要素はルート要素の子供でななく、entry要素の子供になっている。
従って、このクエリはマッチする要素を一つも返さない。
tree.findall('{http://www.w3.org/2005/Atom}author')
[]
最初にマッチした要素を返す find()
メソッドもある。
このメソッドは、マッチするのは1つだけだと考えられる場合や、
複数のマッチがあったとしても、その1つ目にしか興味がないときに便利。
entries = tree.findall('{http://www.w3.org/2005/Atom}entry')
len(entries)
3
find()
メソッドは、ElementTree
のクエリを引数に取り、最初にマッチする要素を返す。
title_element = entries[0].find('{http://www.w3.org/2005/Atom}title')
title_element.text
'Dive into history, 2009 edition'
このエントリにfooという要素は存在しないので、これはNoneを返す。
foo_element = entries[0].find('{http://www.w3.org/2005/Atom}foo')
foo_element
type(foo_element)
NoneType
find()メソッドには、「ワナ」が存在する。 ブール値のコンテクストにおいて、ElementTreeの要素オブジェクトは子要素を含んでいないときに(つまりlen(element)が0のときに)Falseと評価されるのだ。
したがって、if element.find('...')
は、find()
メソッドがマッチする要素を見つけたかどうかをテストしているのではなく、マッチした要素が子要素を持つかどうかをテストしている。
find()
メソッドが要素を返したかどうかテストしたいなら、
if element.find('...') is not None
を使う。
子孫要素(つまり、子供・孫・任意のネストレベルの要素)を検索する方法は実際に存在する。
このクエリ//{http://www.w3.org/2005/Atom}link
は先ほどの例と非常に似ているが、クエリの先頭に2つのスラッシュが付いている。
これら2つのスラッシュ「//
」は、
「直接の子供だけを探さないでほしい。ネストレベルに関係なく、すべての要素を探したい」ということを意味している。
したがって、結果は1つだけではなく、4つのlink要素のリストになる。
tree.findall('{http://www.w3.org/2005/Atom}link') # feedの中のlink 1つを返す
[<Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB3B2228>]
all_links = tree.findall('.//{http://www.w3.org/2005/Atom}link') # entryの中のlink(3つ) も含まれる
all_links
[<Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB3B2228>, <Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB3B24A8>, <Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB3B2908>, <Element '{http://www.w3.org/2005/Atom}link' at 0x000001C1AB3B2C28>]
1つ目の結果はルート要素の直接の子要素。 この属性から分かるとおり、これはフィードが表しているウェブサイトのhtml版を指すフィードレベルの代替リンク。
all_links[0].attrib
{'href': 'http://diveintomark.org/', 'rel': 'alternate', 'type': 'text/html'}
他の3つはそれぞれエントリレベルの代替リンクになる。 各々のentryは単一のlink子要素を持っており、クエリの先頭に2つのスラッシュによって、このクエリはこれらすべてを探し出す。
all_links[1].attrib
{'href': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition', 'rel': 'alternate', 'type': 'text/html'}
all_links[2].attrib
{'href': 'http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress', 'rel': 'alternate', 'type': 'text/html'}
all_links[3].attrib
{'href': 'http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats', 'rel': 'alternate', 'type': 'text/html'}
ElementTreeのfindall()
メソッドは強力な機能だと言えるが、
クエリ言語には少し分かりづらい部分がある。
この点については公式に「XPath式を限定的にサポートする」と説明されている。XPathはW3C標準のxml用クエリ言語。
ElementTree
のクエリ言語は基本的な検索には十分なくらいXPathに似ているが、
すでにXPathを知っている人にとっては、十分に煩わしい程度の違いもある。
今度は、XPathを完全にサポートするようにElementTree
APIを拡張した、サードパーティ製の
xmlライブラリを見てみる。
lxml
は広く使われている libxml2
パーサの上に構築されたオープンソースのサードパーティ製ライブラリ。
このライブラリは、100%の互換性を持たせる形で
ElementTree
apiを実装しており、
さらにそこへ XPath 1.0
のフルサポートと細かな改良を付け加えている。
Windowsにはインストーラが用意されている。
Linuxユーザは、できるだけ各ディストリビューションに付属しているyum
やapt-get
などのツールを使って、リポジトリからコンパイル済バイナリを取得するようにしてほしい。
anaconda を使っていれば、
conda install lxml
でインストールできる。
インポートすれば、あとは組み込みの ElementTree
ライブラリと同じapiが使える。
from lxml import etree
parse()
関数:ElementTreeと同じように使える。
tree = etree.parse('examples/feed.xml')
tree
<lxml.etree._ElementTree at 0x1c1ab3b7188>
getroot()
メソッド:これも ElementTree
と同じように使える。
root = tree.getroot()
root
<Element {http://www.w3.org/2005/Atom}feed at 0x1c1ab414288>
findall()
メソッドも同じ。
root.findall('{http://www.w3.org/2005/Atom}entry')
[<Element {http://www.w3.org/2005/Atom}entry at 0x1c1ab414408>, <Element {http://www.w3.org/2005/Atom}entry at 0x1c1ab4143c8>, <Element {http://www.w3.org/2005/Atom}entry at 0x1c1ab414448>]
巨大なxml文書を扱う場合、lxml
の処理は組み込みの ElementTree
ライブラリよりもはるかに高速。
ElementTreeのapiだけを使っていて、かつ利用できるライブラリの中で最も速い実装を使いたいのであれば、
まずはlxmlをインポートしてみて、
エラーが出るようなら組み込みの ElementTree
にフォールバックするといい。
try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree
etree
<module 'lxml.etree' from 'C:\\Miniconda3\\lib\\site-packages\\lxml\\etree.pyd'>
lxml
は ElementTree
より高速なだけではない。
lxml
の findall()
メソッドはもっと複雑な式をサポートしている。
import lxml.etree
tree = lxml.etree.parse('examples/feed.xml')
このクエリは、xml文書全体を対象として、
href属性を持つAtom名前空間の要素をすべて探し出す。
クエリの先頭にある//
は、「(ルート要素の子供だけではなく)あらゆる要素を探索する」ことを意味する。
{http://www.w3.org/2005/Atom}
は、「Atomの名前空間の要素だけ」を意味する。
*
は「どんなローカル名を持つ要素でもいい」ことを意味する。
そして、[@href]
は「href属性を持っている」ことを意味する。
tree.findall('//{http://www.w3.org/2005/Atom}*[@href]')
[<Element {http://www.w3.org/2005/Atom}link at 0x1c1ab418148>, <Element {http://www.w3.org/2005/Atom}link at 0x1c1ab418488>, <Element {http://www.w3.org/2005/Atom}link at 0x1c1ab418448>, <Element {http://www.w3.org/2005/Atom}link at 0x1c1ab414d48>]
このクエリは、http://diveintomark.org/
を値とする href
属性を持つ Atom
要素を全て探し出す。
tree.findall("//{http://www.w3.org/2005/Atom}*[@href='http://diveintomark.org/']")
[<Element {http://www.w3.org/2005/Atom}link at 0x1c1ab418148>]
ちょっとした文字列フォーマットを施した上で(これをしないと、 合成したクエリはばかばかしいほど長くなってしまう)、
このクエリは、Atomのuri要素を子要素として持つAtomのauthor要素を検索する。これは、1つ目と2つ目のentry要素の中にある2つのauthor要素だけを返す。最後のentryにあるauthorはnameだけを含んでおり、uriは含んでいない。
NS = '{http://www.w3.org/2005/Atom}'
tree.findall('//{NS}author[{NS}uri]'.format(NS=NS))
[<Element {http://www.w3.org/2005/Atom}author at 0x1c1ab418848>, <Element {http://www.w3.org/2005/Atom}author at 0x1c1ab4188c8>]
lxml
がどのように XPath
をサポートしているのかは示す。
import lxml.etree
tree = lxml.etree.parse('examples/feed.xml')
名前空間の付いた要素の上でXPathのクエリを実行するには、名前空間の接頭辞のマッピングを定義する必要がある。これは単なるPythonの辞書。
NSMAP = {'atom': 'http://www.w3.org/2005/Atom'}
これがXPathのクエリ。
このXPath式は、accessibilityという値を持つterm属性を含んだ(Atomの名前空間の)category要素を探し出す。
しかし、実際に返されるのは category
要素のリストではない。
クエリ文字列の一番最後の
/..
という部分は、「見つけたcategory
要素の親要素を返してくれ」という意味だ。
つまり、この XPathクエリを実行すると、
<category term='accessibility'>
という子要素を持つすべての要素が返される。
entries = tree.xpath("//atom:category[@term='accessibility']/..", namespaces=NSMAP)
この xpath()
関数は、ElementTree
オブジェクトのリストを返す。
この文書には、term属性の値がaccessibility
であるcategory
要素は1つだけ存在する。
entries
[<Element {http://www.w3.org/2005/Atom}entry at 0x1c1ab418208>]
XPath
式は常に要素のリストを返すわけではない。
厳密に言えば、パースしたxml文書のdomに含まれているのは要素ではなく、
ノードだからだ。
ノードの種類に応じて、ノードの内容は要素であったり、属性であったり、テキスト内容であったりする。
entry = entries[0]
XPathクエリを実行することで得られるのはノードのリスト。
実際、このクエリはテキストノードのリストを返しており、
その中身は現在の要素の子 (./) の title
要素 (atom:title
) のテキスト内容 (text()
) になっている。
entry.xpath('./atom:title/text()', namespaces=NSMAP)
['Accessibility is a harsh mistress']
Python がサポートするのは、既存のxml文書をパースすることだけに留まらない。
xml
文書をゼロから作成することもできる。
import xml.etree.ElementTree as etree
新しい要素を作るには、Element
クラスをインスタンス化すればいい。
最初の引数として要素名(名前区間 + ローカル名)を渡す。
この文はAtomの名前区間にあるfeed要素を作成するが、これが新しい文書のルート要素になる。
新しく作成した要素に属性を追加するには、属性名と属性値の辞書をattrib引数で渡せばいい。
属性名は ElementTree
の標準形式{namespace}localname
で書かなければいけないことに注意
new_feed = etree.Element('{http://www.w3.org/2005/Atom}feed',
attrib={'{http://www.w3.org/XML/1998/namespace}lang': 'en'})
ElementTreeのtostring()関数を使えば、任意の要素(とその子要素)をいつでもシリアライズできる。
print(etree.tostring(new_feed))
b'<ns0:feed xmlns:ns0="http://www.w3.org/2005/Atom" xml:lang="en" />'
ElementTreeが名前空間付きのxml要素をシリアライズする方法は、技術的には正確だが最適なものではない。
この章のはじめに載せたxml文書のサンプルでは、「デフォルトの名前空間」(xmlns='http://www.w3.org/2005/Atom'
) を定義していた。
このようにデフォルトの名前空間を定義することは、Atomフィードのようにすべての要素が同じ名前空間にある文書にとっては有用だ。なぜなら、名前区間を一度だけ定義すれば、各々の要素はローカル名 ( <feed>、<link>、<entry>
) だけで宣言することができるため。
他の名前空間の要素を宣言するのでなければ、接頭辞をつける必要はまったく無い。
xmlパーサは、デフォルト名前空間をもつxml文書と、接頭辞のついたxml文書の違いを「理解しない」。次のシリアライズのdomは:
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
次のシリアライズのdomと同一:
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
実用上問題となる違いは、 2番目のシリアライズの方が何文字か短いということだけだ。 サンプルフィード中の開始タグと終了タグすべてにns0:接頭辞を付けると、 開始タグごとに4文字 × 79タグ + 名前空間の宣言に使う4文字で、合計320文字になる。
UTF-8エンコーディングを使うとすると、 この余分な文字だけで320バイトになる(gzip圧縮すると増分は21バイトになるが、それでも21バイトは21バイトだ)。 だから何なのかと思うかもしれないが、Atomフィードのように、 更新されるたびに何千回もダウンロードされるようなものについては、 数バイト減らすだけでもすぐに大きな差につながりうる
組み込みのElementTreeでは、名前空間のついた要素のシリアライズを細かく制御することはできないが、lxmlを使えばそれができる。
準備として、名前空間のマッピングを辞書として定義する。
辞書の値は名前空間で、辞書のキーは使用したい接頭辞。
接頭辞として None
を使うと、結果的にデフォルト名前区間を定義することになる。
import lxml.etree
NSMAP = {None: 'http://www.w3.org/2005/Atom'}
要素を作成するときに nsmap
引数(この引数はlxmlにしかない)を渡せば、
lxml
は定義した接頭辞を考慮してくれる。
new_feed = lxml.etree.Element('feed', nsmap=NSMAP)
このシリアライズはAtomの名前空間をデフォルトの名前空間として定義しており、 名前空間の接頭辞を使わずにfeed要素を宣言している。
print(lxml.etree.tounicode(new_feed))
<feed xmlns="http://www.w3.org/2005/Atom"/>
set()
メソッドを使えば任意の要素にいつでも属性を追加できる。
このメソッドは2つの引数をとる:
(このメソッドはlxml固有のものではない。 この例でlxmlに特有な部分は、シリアライズ出力の名前空間の接頭辞を制御するためのnsmap引数だけだ)。
new_feed.set('{http://www.w3.org/XML/1998/namespace}lang', 'en')
print(lxml.etree.tounicode(new_feed))
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"/>
子要素も簡単に作成できる。
既存の要素の子要素を作るには、SubElement
クラスをインスタンス化すればいい。
要求される引数は
この子要素は、親要素の名前空間のマッピングを継承するので、名前空間や接頭語を再び宣言する必要はない。
属性の辞書を渡すこともできる。キーは属性名で、値は属性値になる。
title = lxml.etree.SubElement(new_feed, 'title', attrib={'type':'html'})
新しいtitle
要素は Atom
の名前空間の中に作られ、
feed
要素の子として挿入されている。
title
要素はテキスト内容や自身の子供を持たないので、
lxmlはこれを(/>ショートカットを使った)空要素としてシリアライズする。
print(lxml.etree.tounicode(new_feed))
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title type="html"/></feed>
要素のテキスト内容を設定するには、単にその要素の.textプロパティを設定すればいい。
title.text = 'dive into …'
ここで title
要素はテキスト内容と共にシリアライズされている。
小なり記号やアンパサンドを含んだテキスト内容は、
シリアライズ時にエスケープされる必要がある。
lxmlはこのエスケープを自動的に処理してくれる。
print(lxml.etree.tounicode(new_feed))
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><title type="html">dive into &hellip;</title></feed>
シリアライズ処理に “pretty printing”
を適用することもできる。
これは、終了タグの後ろや、子要素を持つがテキスト内容を持たない要素の開始タグの後ろに改行を挿入する。
専門用語で言えば、lxmlは「意味のない空白 (insignificant whitespace)」を加えることによって、結果をより読みやすいものにする。
print(lxml.etree.tounicode(new_feed, pretty_print=True))
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"> <title type="html">dive into &hellip;</title> </feed>
xmlを生成するための別のサードパーティ製ライブラリである xmlwitch を調べてみるのもよい。このライブラリはxmlの生成コードをより読みやすくするために、with文を広く活用している。
xmlの仕様によれば、 仕様に準拠したxmlパーサは「厳格なエラー処理」を行わなくてはならないものとされている。 つまり、xml文書の中に何か一つでも整形式性エラーが見つかれば、すぐさま処理を停止して例外を送出しなくてはならない。 整形式性エラーとは、開始タグと終了タグのミスマッチや、未定義の実体、不正なUnicode文字列といったものであり、他にも数々の難解な規則が存在している。
これは html
のようなほかの一般的なデータ形式とは対照的 — ブラウザは、htmlタグを閉じ忘れたり、
属性値の中でアンパサンド (&) をエスケープし忘れても、ウェブページのレンダリングを中断することはない
(htmlにエラー処理が定義されていないというのは良くある誤解。
htmlのエラー処理は実は非常に明確に定義されているのだが、「エラーが見つかりしだい処理を停止する」よりも著しく複雑)。
一部の人々は、xmlが厳格なエラー処理を採用したのは間違いだったと考えている。 実際問題として、「整形式性」の概念は思ったよりも扱いにくいものであり、とりわけ、ウェブ上で公開され、httpを通して送信される(Atomフィード)のようなxml文書についてはそれが言える。厳格なエラー処理は1997年に標準化されており、xmlは規格としては既に成熟していると言えるが、調査は一貫として、Web上で公開されている少なからぬ割合のAtomフィードに整形式エラーが存在していることを示している。
理論的にも実用的にもxmlはエラーがあろうともパースされるべきだと考えている人もいる。 つまり、整形式性エラーを見つけたとしても、即座に処理を停止しない。 もしこの考え方に共感を覚えるなら、lxmlを使えばこの方式でパースすることができる。
ここに壊れたxml文書の断片がある。
examples/feed-broken.xml:
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
<title>dive into …</title>
...
</feed>
…
という実体参照は、xmlでは定義されていないので(htmlでは定義されている)、
これはエラーになる。この壊れたフィードをデフォルトの設定でパースしようとすると、エラーになる。
%tb
import lxml.etree
tree = lxml.etree.parse('examples/feed-broken.xml')
No traceback available to show.
File "<string>", line unknown XMLSyntaxError: Entity 'hellip' not defined, line 3, column 28
整形式性のエラーを無視して、この破損したxml文書を読み込むには、カスタムのxmlパーサを作る必要がある。
カスタムのパーサを作るには、lxml.etree.XMLParser
クラスをインスタンス化すればいい。
このクラスは様々な名前付き引数を取ることができる。
ここでrecover引数を True
に設定すると、xmlパーサは整形式性のエラーから「回復する」ために最善を尽くすようになる。
parser = lxml.etree.XMLParser(recover=True)
parser
<lxml.etree.XMLParser at 0x1c1ab4022a8>
カスタムパーサを使ってxml文書をパースするには、
2番目の引数として parser
オブジェクトを parse()
関数に渡せばいい。
lxmlは未定義の…
実体に関する例外を送出していないことに注意。
tree = lxml.etree.parse('examples/feed-broken.xml', parser)
tree
<lxml.etree._ElementTree at 0x1c1ab418748>
パーサは遭遇した整形式性エラーのログを記録する (実は、パーサがエラーから回復するように設定されていない場合も、エラーはログに記録されている)。
parser.error_log
examples/feed-broken.xml:3:28:FATAL:PARSER:ERR_UNDECLARED_ENTITY: Entity 'hellip' not defined
未定義の …
実体をどう扱えばよいのか分からないので、
パーサは端的にこの部分を無視する。title
要素のテキスト内容は'dive into '
になる。
title = tree.findall('{http://www.w3.org/2005/Atom}title')[0]
title.text
'dive into '
シリアライズ結果から分かるように、
…
実体は取り込まれていない。
パースの時点で無視された。
print(lxml.etree.tounicode(tree))
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"> <title>dive into </title> ... </feed>
xmlパーサの「回復」処理には相互運用性の保証はない。 別のパーサはhtmlの…を認識することにして、これを…で置き換えるかもしれない。これは「より良い」のだろうか? おそらくそうだ。
これは「より正しい」のだろうか? いや違う、これらは両方とも等しく誤りなのだ。(XMLの仕様に従った)正しい振る舞いは即座に処理を停止した上で例外を送出すること。 もしそうしないことに決めたのなら、それは自分の責任で行わなければならない。