このブログを検索

第12章 XML

没頭しよう

この本のほとんどの章ではサンプルコードが話の中心です。しかし、XMLはコードに関することでははなく、データに関する話です。XMLの一般的な使われ方の1つは『配信フィード』というもので、blog、掲示板の他、頻繁に更新されるウェブサイトの最新記事をリストにしたものです。よく使われているブログソフトウェアの多くでは、新しい記事、コメント欄、ブログ投稿が発行されたときにフィードを作ったり更新します。ブログフィードを『購読』すればフォローできますし、複数のブログをフォローしたいなら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 &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds&amp;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>

12.2. XMLの5分間集中コース

すでにXMLについて知っているのであれば、このセクションは飛ばしてもかまいません。

XMLは階層的データを表現する一般的な方法です。XMLドキュメントは1つ以上の要素を持っていて、開始タグ、終了タグで囲まれています。以下は確かにXMLドキュメントです(つまならないですが)。
<foo>   # ①
</foo>  ②
①要素fooの開始タグです。

②要素fooの対応する終了タグです。文章や数式、コードと同じように、開始タグは対応する終了タグで閉じなければなりません。

要素は何層にでもネストできます。fooの中にあるbarは、fooのサブエレメントあるいは子要素と呼ばれます。
<foo>
  <bar></bar>
</foo>
すべてのXMLドキュメントの最初の要素はroot要素と呼ばれます。1つのXMLドキュメントにroot要素は1つだけです。以下のものは要素が2つあるので、XMLドキュメントではありません
<foo></foo>
<bar></bar>
要素は、属性(名前−値のぺア)を持つことができます。属性は開始タグの中に配置され、属性間にはスペースが入ります。同じ属性の名前を1つの要素の中で繰り返し使うことはできません。属性の値はクォーテーションで囲まれますが、シングルでもダブルでもどちらでもかまいません。
<foo lang='en'>                          # ①
  <bar id='papayawhip' lang="fr"></bar>  # ②
</foo>
①foo要素は属性を1つ持っていて、langという名前です。lang属性の値はenです。

②bar要素は属性を2つ持っていて、名前はidとlangです。lang属性の値はfrですが、foo要素とは競合しません。要素はそれぞれ属性のセットを持っているからです。

要素が2つ以上の属性を持っているとき、属性の順番は特に重要ではありません。要素の属性は、Pythonの辞書ライクなkeyとvalueの順序のないsetを作っています。各要素に対して定義する属性の数に制限はありません。

要素はテキストコンテンツを持つことができます。
<foo lang='en'>
  <bar lang='fr'>PapayaWhip</bar>
</foo>
以下のように、テキストや子要素を持たない要素は空(empty)です。
<foo></foo>
空要素を書くための簡単な方法があります。開始タグに / を付けることで、終了タグを省略することができます。上のXML文はこのように書くこともできます。
<foo/>
Pythonの関数が異なるモジュールで宣言されるように、XML要素も異なる名前空間で宣言されます。たいていの名前空間はURLのようになっています。xmlns宣言を使ってデフォルト名前空間を定義することができます。名前空間の定義は属性に似ていますが、目的が違います。
<feed xmlns='http://www.w3.org/2005/Atom'>  # ①
  <title>dive into mark</title>             # ②
</feed>
① feed要素は名前空間http://www. w3 org/2005/Atomにあります。

# ② title要素も同じ名前空間にあります。名前空間の宣言は、宣言された要素と、その子要素すべてに影響します。

xmlns:prefixとして宣言すると、名前空間とprefixを関連付けることができます。その場合は、名前空間の各要素で明示的にprefixも合わせて宣言しなければなりません。
<atom:feed xmlns:atom='http://www.w3.org/2005/Atom'>  # ①
  <atom:title>dive into mark</atom:title>             # ②
</atom:feed>
① feed要素は名前空間 http://www.w3.org/2005/Atom にあります。

② title要素も名前空間 http://www.w3.org/2005/Atom にあります。

XMLパーサーから見ると、prefixのありなし2つのXMLドキュメントは同一のものとなります。名前空間+要素名=XMLのアイデンティティ だからです。Prefixは名前空間に言及するためだけに存在しているので、prefix名(atom:)が何であるかは関係ありません。名前空間がマッチして、要素名がマッチして、属性(あるいは属性がないこと)がマッチして、各属性のテキストコンテンツがマッチしたら、XMLドキュメントは同一と言えるのです。

最後に、XMLドキュメントは文字エンコーディング情報をroot要素の前の最初の行に含めることができます。(ドキュメントがパースされる前に、必要な情報をどうやって保持しているのか、ということに興味があるならば、(外部リンク)このセクションFのXML仕様にこのCatch-22について詳しく書いてあります) 

<?xml version='1.0' encoding='utf-8'?>
ここまでで、XMLが危険であることが充分にわかったことでしょう!

12.3. Atomフィードの構造

ウェブログを見てみましょう。コンテンツが頻繁に更新されるものなら何でもかまいません。例をあげるとCNN. comです。サイトにはタイトル("CNN. com")があり、さらにサブタイトル("Breaking News, U. S., World, Weather, Entertainment & Video News")、最終更新日時("updated 12:43 p. m. EDT, Sat May 16,2009")、他の時刻に投稿された記事のリストがあります。それぞれの記事にはタイトルがあり、初出の日時が(訂正やタイポ修正があれば最終更新日時も)、ユニークなURLがついています。

Atom配信フォーマットは標準フォーマットですべての情報を扱えるように設計されています。私のウェブログとCNN. comはデザイン、スコープ、対象において大幅に違うものですが、基本的には同じ構造になっています。CNN. comにはタイトルがあり、私のブログにもタイトルがあります。CNN. comは記事を出し、私も記事を出します。トップレベルにはroot要素があり、すべてのAtomフィードがhttp://www.w3.org/2005/Atom 名前空間を共有しています。
<feed xmlns='http://www.w3.org/2005/Atom'  # ①
      xml:lang='en'>                       # ②
① http://www.w3.org/2005/Atom はAtom名前空間です。

②どの要素もxml:lang属性によって、要素とその子要素の言語を宣言できます。この場合は、XML:lang属性はroot要素で一度に宣言されているので、フィードはすべて英語であるという意味になります。

Atomフィードには、フィードそのものの情報がいくつか入っています。ルートレベルのフィード要素の子要素として宣言されます。
<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/'/>  ⑤
① このフィードのタイトルはdive in to markです。

② このフィードのサブタイトルはcurrently between addictions. です。

# ③ すべてのフィードにはグローバルユニーク識別子が必要です。作り方はRFC4151を参照してください。

④更新されたのはMarch 27, 2009, at 21:56 GMTです。通常は直近の記事の最終更新と等しいです。

⑤ここからが面白くなってきます。このリンク要素にはテキストがありませんが、3つの属性rel、type、hrefがあります。relの値はリンクの種類です。rel='alternate'が意味するのは、 リンク先がこのフィードの代わりに表示されるということです。type='text/html'属性が意味するのは、このリンク先はHTMLページであるということです。 リンク先ターゲットはhref属性によって与えられます。

このフィードが"dive into mark"という名前のサイトでURLはhttp://diveintomark. org/、最終更新がMarch 27,2009であるとわかりました。

👉XMLドキュメントでは要素の順番に意味がある場合もありますが、Atomフィードでは意味はありません。

フィードレベルのメタデータのあとには、新しい記事がリストになっています。記事はこのようになります。
<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 &amp;mdash; my longest chapter so far
    would be 75 printed pages, and it loads in under 5 seconds&amp;hellip;
    On dialup.</summary>
</entry>      
# ① author要素は記事の著者を示します。http://diveintomark.org/. を見るとMarkという人であるとわかります。(この場合はフィードメタデータのalternate linkと同じですが、同じである必要はありません。ウェブログの多くは複数の著者がいて、それぞれ個人ウェブサイトを持っていることもあります)

②タイトル要素に記事のタイトル、"Dive into historyl 2009 edition"があります。

③ フィードレベルfeed-level alternateのように、このリンク要素はこの記事のHTML版のアドレスを示しています。

④ フィードと同じように、エントリには個別識別子が必要です。

# ⑤ エントリには2つの日付があります。最初に発行された日(published)と最後に更新された日です(updated)。

⑥エントリは任意の数のカテゴリを持つことができます。この記事はdiveintopython、docbook、htmlというカテゴリがあります。

⑦サマリ要素は記事の短い要約です。(全記事をフィードの中に入れたいのであれば、ここには表示されていないが他にコンテンツ要素があります)。このサマリ要素はAtom特有のtype='html'属性を持っており、このサマリがHTMLのスニペットであって平文ではないことを示しています。このことは重要で、HTML特有のエンティティ(&mdash; や &hellip;)が入っているので、そのまま表示されずに『—』や『...』と変換されて表示されることになります。

⑧最後に、エントリ要素の終了タグがあり、この記事のメタデータの終了点を表します。

12.4. XMLをパースする

PythonでXMLドキュメントをパースする方法はいくつかあります。DOMやSAXパーサーといった昔からのものがありますが、ここではElementTreeというライブラリを使います。

>>> import xml.etree.ElementTree as etree    # ①
>>> tree = etree.parse('examples/feed.xml')  # ②
>>> root = tree.getroot()                    # ③
>>> root                                     # ④
<Element {http://www.w3.org/2005/Atom}feed at cd1eb0>
# ① ElementTreeライブラリはPython標準ライブラリのxml.etree.ElementTreeに入っています。

②ElementTreeの主エントリポイントはparse()関数で、ファイル名やファイルのようなオブジェクト(第3章)を受け取ることができます。この関数はドキュメントすべてを一度にパースすることができます。メモリに余裕がないなら、XMLドキュメントを逐次パースすることもできます(リンク)

③parse()関数は、ドキュメント全体を表すオブジェクトを返します。これはroot要素ではありません。root要素の参照は、getroot()メソッドを呼び出すと取得できます。

④ 期待どおり、root要素はhttp://www.w3.org/2005/Atom 名前空間のフィード要素です。このオブジェクトの文字表現は重要な点を表しています。つまり、XML要素は名前空間とタグ名(ローカル名)の組合せということです。このドキュメントのすべての要素はAtom名前空間の中にあるので、root要素は{http://www.w3.org/2005/Atom}feedと表されます。

👉ElementTreeはXML要素を {namespace} localnameと表現します。このフォーマットをElem entTreeAPIで繰り返し見たり使ったりするでしょう。

12.4.1. 要素はリストである

ElementTree APIでは、要素はリストのように振る舞います。 リストのアイテムは要素の子要素です。
# continued from the previous example
>>> root.tag                        # ①
'{http://www.w3.org/2005/Atom}feed'
>>> len(root)                       # ②
8
>>> for child in root:              # ③
...   print(child)                  # ④
...
<Element {http://www.w3.org/2005/Atom}title at e2b5d0>
<Element {http://www.w3.org/2005/Atom}subtitle at e2b4e0>
<Element {http://www.w3.org/2005/Atom}id at e2b6c0>
<Element {http://www.w3.org/2005/Atom}updated at e2b6f0>
<Element {http://www.w3.org/2005/Atom}link at e2b4b0>
<Element {http://www.w3.org/2005/Atom}entry at e2b720>
<Element {http://www.w3.org/2005/Atom}entry at e2b510>
<Element {http://www.w3.org/2005/Atom}entry at e2b750>
① 先の例から続いて、 root要素は{http://www.w3.org/2005/Atom}feedです。

② root要素の"長さ"は、子要素の数です。

③ 要素をイテレータとして使って、子要素すべてをループできます。

④出力からわかるように、8つの子要素があります。エントリ要素に続いて、(title, subtitle, id, update,link)といったフィードレベルメタデータがあります。

すでに想像したことかもしれませんが、敢えて指摘しておきます。子要素のリストには直接の子要素しか含みません。各エントリ要素は子要素を持っていますが、それらはリストに含まれません。各エントリの子要素は、各エントリのリストには含まれるが、フィードの子要素のリストには含まれないのです。どれだけ深くネストしていても要素を見つける方法があります。その2つの方法についてこの章で学びます。

12.4.2. 属性は辞書である

XMLは単に要素が集まっているだけではありません。各要素は属性のセットを持つことができます。ある要素に対して参照すれば、要素の属性を辞書形式で簡単に取得することができます。

# 前の例からの続き

>>> root.attrib                           # ①
{'{http://www.w3.org/XML/1998/namespace}lang': 'en'}
>>> root[4]                               # ②
<Element {http://www.w3.org/2005/Atom}link at e181b0>
>>> root[4].attrib                        # ③
{'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'}
>>> root[3]                               # ④
<Element {http://www.w3.org/2005/Atom}updated at e2b4e0>
>>> root[3].attrib                        # ⑤
{}
①attribプロバティは要素の属性の辞書です。これの元のマークアップ は <feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>となります。プリフィックスxml:はすべてのXMLドキュメントが宣言なしに使うことができるビルトイン名前空間を参照しています。

②5番目の子要素(0始まりリストの[4])はlink要素です。

③要素は3つの属性href、type、relを持っています。

④4番目の子要素(0始まりリストの[3])はupdate要素です。

⑤update要素に属性はないので、.attribは空の辞書になります。

12.5. XMLドキュメント内のノードを探す

ここまで、XMLドキュメントについて"トップダウンで"、つまりroot要素から始まって子要素、ドキュメント全体を順に見てきました。しかし、xmlを使う多くの場面では特定の要素を探さないといけません。Etreeではそういう探し方もできます。
>>> import xml.etree.ElementTree as etree
>>> tree = etree.parse('examples/feed.xml')
>>> root = tree.getroot()
>>> root.findall('{http://www.w3.org/2005/Atom}entry')    # ①
[<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b510>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
>>> root.tag
'{http://www.w3.org/2005/Atom}feed'
>>> root.findall('{http://www.w3.org/2005/Atom}feed')     # ②
[]
>>> root.findall('{http://www.w3.org/2005/Atom}author')   # ③
[]
①findall()メソッドは特定のクエリにマッチする子要素を見つけます。(クエリフォーマットはこのあとでてきます)

②各要素(root要素だけでなく子要素も含む)は、findall()メソッドを持っています。これは要素の子の中でマッチするすべての要素を見つけるものです。何も結果が出ないのは何故でしょうか?明示的ではないかもしれませんが、この特別なクエリが探すのは要素の子だけなのです。rootのfeed要素にはfeedという名の子要素がないので、このクエリは空のリストを返します。

③この結果には驚きかもしれません。このドキュメントにはauthor要素があります。実は、3つあります(各エントリに1つ)。これらのaurhoer要素はroot要素の直系の子要素ではなく、"孫要素"なのです(文字どおり、子要素の子要素です)。すべてのネスト階層でauthor要素を見たい場合は、クエリ形式が少し変わります。
>>> tree.findall('{http://www.w3.org/2005/Atom}entry')    # ①
[<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b510>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
>>> tree.findall('{http://www.w3.org/2005/Atom}author')   # ②
[]
①便利なことに、ツリーオブジェクト(etrde. parse()関数から返る)はroot要素からコピーしたメソッドを持っていて、結果はtree.getroot().findall()メソッドを呼び出したものと同じになります。

②驚きかもしれませんが、クエリはこのドキュメントのauthor要素を見つけられません。何故でしょうか?これはtree.getroot().findall('{http://www.w3.org/2005/Atom}author') へのショートカットに過ぎず、『root要素の子要素であるauthor要素を探せ』という意味になるからです。aruthor要素はroot要素の子要素ではありません。Entry要素の子要素なのです。つまり、クエリは何もマッチしません。

find()メソッドも最初のマッチした要素を返します。これは1つだけマッチするのを想定している状況や、複数のマッチの中で最初だけを調べればよいときに有効です。
>>> entries = tree.findall('{http://www.w3.org/2005/Atom}entry')           # ①
>>> len(entries)
3
>>> title_element = entries[0].find('{http://www.w3.org/2005/Atom}title')  # ②
>>> title_element.text
'Dive into history, 2009 edition'
>>> foo_element = entries[0].find('{http://www.w3.org/2005/Atom}foo')      # ③
>>> foo_element
>>> type(foo_element)
<class 'NoneType'>
①これは前の例でも見ました。すべてのatom:entry要素を見つけます。

②find()メソッドはElementTreeクエリを受け取って最初にマッチした要素を返します。

③このfooという名前のエントリには要素がないので、何も返りません。

👉find()メソッドでは最終的に"見つけた"となります。

ブール文では、ElementTree要素オブジェクトの子要素がなければFalseと判定します(例: len (element)が0であるとき)。つまり、if element.find ('...')という部分はfind()メソッドがマッチする要素を見つけたかどうかをテストしているのではなく、マッチした要素が子要素を持っているかをテストしているのです! find()メソッドが要素を返したかどうかをテストするには、if element.find ('...')がNoneかどうかを調べます。子要素や孫要素といった子孫の要素を探したり、どのネストレベルにあるどの要素でも探すことができる方法があります。
>>> all_links = tree.findall('//{http://www.w3.org/2005/Atom}link')  # ①
>>> all_links
[<Element {http://www.w3.org/2005/Atom}link at e181b0>,
 <Element {http://www.w3.org/2005/Atom}link at e2b570>,
 <Element {http://www.w3.org/2005/Atom}link at e2b480>,
 <Element {http://www.w3.org/2005/Atom}link at e2b5a0>]
>>> all_links[0].attrib                                              # ②
{'href': 'http://diveintomark.org/',
 'type': 'text/html',
 'rel': 'alternate'}
>>> all_links[1].attrib                                              # ③
{'href': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
 'type': 'text/html',
 'rel': 'alternate'}
>>> all_links[2].attrib
{'href': 'http://diveintomark.org/archives/2009/03/21/accessibility-is-a-harsh-mistress',
 'type': 'text/html',
 'rel': 'alternate'}
>>> all_links[3].attrib
{'href': 'http://diveintomark.org/archives/2008/12/18/give-part-1-container-formats',
 'type': 'text/html',
 'rel': 'alternate'}
①//{http://www.w3.org/2005/Atom}link 一というこのクエリは前の例とよく似ていて、異なる点はスラッシュ2つがクエリの始めについていることです。この2つのスラッシュは"直接の子要素だけではなく、どんな要素でも、どんなネストレベルでも良い"という意味です。結果は1つだけではなく4つのlink要素のリストとなります。

②結果の1番目はroot要素の直接の子要素です。属性を見ると、これはフィードレベルのalternateリンクで、フィードが記述したウェブサイトのHTML版を示しています。

③他の3つの結果はそれぞれエントリレベルのalternateリンクです。各エントリは子要素のリンクを1つ持っていますが、クエリの冒頭に2重スラッシュがあるため、このクエリはリンクをすべて見つけます。

まとめると、ElementTreeのfindall()メソッドはとても強力な機能ですが、クエリ言語には少し驚きがあるかもしれません。公式に"Xpath表現を限定的にサポートする"と述べられています。XPathはXMLドキュメントのクエリのW3C標準です。ElementTreeのクエリ言語はXPathが基本的な検索をする場合によく似ていますが、XPathなら知っていると不満を感じるほど似ているわけではありません。では次に、XPathをフルサポートしたElementTree APlを拡張するサードパーティXMLライブラリを見てみましょう。

12.6. LXMLで更に進める

lxmlはサードパーティのオープンソースライブラリで、人気のある libxml2パーサに基づいて作られています。ElementTree APIと100%の互換性があり、XPath1.0のフルサポートや他にも便利な機能とともに拡張されています。Windowsにはインストーラが用意されていますが、Linuxユーザはディストリビューションに応じたyumやapt-getコマンドでプリコンパイルされたバイナリファイルをレポジトリからインストールしましょう。もしくはlxmlをマニュアルでインストールすることになります。
>>> from lxml import etree                   # ①
>>> tree = etree.parse('examples/feed.xml')  # ②
>>> root = tree.getroot()                    # ③
>>> root.findall('{http://www.w3.org/2005/Atom}entry')  # ④
[<Element {http://www.w3.org/2005/Atom}entry at e2b4e0>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b510>,
 <Element {http://www.w3.org/2005/Atom}entry at e2b540>]
①lxmlをインポートすると、ビルトインElementTreeライブラリと同じAPIを使えるようになります。

②parse()関数: ElementTreeと同じです。

③getroot()メソッド: これも同じです。

④findall()メソッド: 全く同じです。

大きなXMLドキュメントに対しては、IXMLはビルトインElementTreeライブラリよりかなり速くなります。もしElementTree APlだけを使っていて、最速の入手可能な実装を使いたいのであれば、lxmlをインポートしてビルトインElementTreeの代替として試すのがよいでしょう。
try:
    from lxml import etreeexcept ImportError:
    import xml.etree.ElementTree as etree
lxmlはElementTreeより速いだけではありません。findall()メソッドはより複雑な表現にも対応しています。
>>> import lxml.etree                                                                   # ①
>>> tree = lxml.etree.parse('examples/feed.xml')
>>> tree.findall('//{http://www.w3.org/2005/Atom}*[@href]')                             # ②
[<Element {http://www.w3.org/2005/Atom}link at eeb8a0>,
 <Element {http://www.w3.org/2005/Atom}link at eeb990>,
 <Element {http://www.w3.org/2005/Atom}link at eeb960>,
 <Element {http://www.w3.org/2005/Atom}link at eeb9c0>]
>>> tree.findall("//{http://www.w3.org/2005/Atom}*[@href='http://diveintomark.org/']")  # ③
[<Element {http://www.w3.org/2005/Atom}link at eeb930>]
>>> 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 eeba80>,
 <Element {http://www.w3.org/2005/Atom}author at eebba0>]
①この例では、import lxml.etree としようとしていますが、(from lxml import etreeではなくて)、こうすることで機能をlxmlに限定していることを強調しています。

②このクエリはAtom名前空間のドキュメントのどの場所からもhref属性をもつすべての要素を見つけます。クエリの最初の//の意味は、(root要素の子要素だけではなく) "どの要素も"ということです。{http://www.w3.org/2005/Atom}はAtom名前空間の要素に限定という意味で、[ * ]は"すべてのローカル名の要素"という意味です。[@href]は"href属性をもつもの"という意味になります。

③クエリはhrefのAtom要素でhttp://diveintomark.linkorg/ の値を持ったものをすべて見つけます。

④高速な文字列フォーマットを行ったあと(そうしないとこれらのクエリは馬鹿みたいに長くなるので)、このクエリはAtomのaruthor要素でuri要素を子要素に持つものを探します。この結果、author要素を2つだけ返しますが、1番目と2番目のエントリのものです。最後のエントリのauthorは名前しか含んでおらず、uriではありません。

充分でしょうか?lxmlは任意のXPath1.0表現に対してサポートしています。ここではXPath文法について深く説明するつもりはありません。それ自体で1冊の本になってしまいますからね! lxmlの中にどうやって実装していくかを説明していきます。
>>> import lxml.etree
>>> tree = lxml.etree.parse('examples/feed.xml')
>>> NSMAP = {'atom': 'http://www.w3.org/2005/Atom'}                    # ①
>>> entries = tree.xpath("//atom:category[@term='accessibility']/..",  # ②
...     namespaces=NSMAP)
>>> entries                                                            # ③
[<Element {http://www.w3.org/2005/Atom}entry at e2b630>]
>>> entry = entries[0]
>>> entry.xpath('./atom:title/text()', namespaces=NSMAP)               # ④
['Accessibility is a harsh mistress']
①XPathクエリを名前空間の要素に実行するときは、名前空間のプリフィックスマッピングを定義する必要があります。これはPythonの辞書と同じです。

②これがXPathクエリです。XPath表現によって値がaccessibilityであるterm属性をもったcategory要素が(Atom名前空間で)検索されます。ところが、実はこれはクエリ結果ではありません。クエリ文字列の最後を見ると、/.. という部分があることに気がつきましたか?これは『見つけたcategory要素の親要素を返す』という意味です。つまりこの1文のXPathクエリは <category term='accessibility'>の子要素をすべて探します。

③xpath()関数はElementTreeオブジェクトのリストを返します。このドキュメントでは、termがaccessibilityであるcategoryを持つエントリが1つしかありません。

④XPath表現は常に要素のリストを返すというわけではありません。厳密には、パースされたXMLドキュメントのDOMは要素を含んでおらず、ノードを含んでいます。型に応じて、ノードは要素、属性、テキストにもなれます。Xparthクエリの結果はノードのリストです。このクエリはテキストノードのリストを返します。つまり、カレント要素(. /)の子要素であるtitle要素(atom:title)のテキスト内容text() )を返すのです。

12.7. XMLを作る

XMLでPythonがサポートしているのは既存ドキュメントのパースだけではありません。一からXMLドキュメントを作ることもできます。

>>> import xml.etree.ElementTree as etree
>>> new_feed = etree.Element('{http://www.w3.org/2005/Atom}feed',     # ①
...     attrib={'{http://www.w3.org/XML/1998/namespace}lang': 'en'})  # ②
>>> print(etree.tostring(new_feed))                                   # ③
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
①新しい要素を作るためには、Elementクラスを初期化します。要素名(名前空間+ローカル名)を第一引数として渡します。この一文によって、フィード要素がAtom名前空間に生成されます。これが今回見ていく新しいドキュメントのroot要素です。

②新しく作った要素に属性を追加するためには、名前と値の属性辞書をattrib引数に渡します。属性名は標準のElementTreeフォーマットで、"{名前空間}ローカル名"となることに注意しましょう。

③すべての要素(とその子要素)はElementTreeのtostring()関数を使えば、いつでもシリアル化することができます。

シリアル化できることに驚きましたか?名前空間にあるXML要素をElementTreeがシリアル化することは厳密には間違っていませんが、最適ではありません。この章の冒頭にあったサンプルのXMLドキュメントでは、デフォルトの名前空間(xmlns='http://www.w3.org/2005/Atom')を定義していました。デフォルトの名前空間を定義することは、すべての要素が同じ名前空間にあるーAtomフィードのような一ドキュメントには有効です。名前空間を1度宣言すれば、各要素はローカル名(<feed>, <link>, <entry>)で宣言することができるからです。他の名前空間で要素を宣言するのでなければ、頭に何も付ける必要がありません。

XMLパーサーにとっては、xmlドキュメントがデフォルト名前空間のものであっても、プリフィックスした名前空間のものでも差がありません。結果的にこのシリアル化のDOMはこのようになります。
<ns0:feed xmlns:ns0='http://www.w3.org/2005/Atom' xml:lang='en'/>
 こちらと同一の結果になります。
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'/>
実用上で唯一違う点は、2番目のシリアル化の方が文字数が短いということです。サンプルフィードすべてをns0:プリフィックスを使ってstart, endタグ毎に再キャストするとすれば、startタグごとに4文字追加x79タグ+名前空間の宣言に4文字で、合計320文字になります。UTF-8エンコーディングであれば、320バイト増加します(gzip圧縮をすれば、この差は21バイトまで落ちますが、それでも21バイトは21バイトです)。これは大きな問題にならないかもしれませんが、変化がある度に何千回でもダウンロードされる可能性があるAtomフィードにとっては、1リクエストあたりで数バイトの節約であっても直ぐに効果が出ることになります。

ビルトインElementTreeライブラリは、名前空間にある要素のシリアル化においてこのようなきめ細かい操作はできませんが、lxmlにはできます。
>>> import lxml.etree
>>> NSMAP = {None: 'http://www.w3.org/2005/Atom'}                     # ①
>>> new_feed = lxml.etree.Element('feed', nsmap=NSMAP)                # ②
>>> print(lxml.etree.tounicode(new_feed))                             # ③
<feed xmlns='http://www.w3.org/2005/Atom'/>
>>> new_feed.set('{http://www.w3.org/XML/1998/namespace}lang', 'en')  # ④
>>> print(lxml.etree.tounicode(new_feed))
①名前空間のマッピングを辞書として定義します。辞書の値は名前空間、キーは希望するプリフィックスです。プリフィックスにNoneを使うことで、デフォルト名前空間を宣言することができます。

②要素を作成するときlxml特有のnsmapという引数を渡すと、IXMLはすでに定義した名前空間プリフィックスを理解してくれます。

③期待どおり、このシリアル化はAtom名前空間をデフォルト名前空間として定義し、名前空間のプリフィックスは無しでフィード要素を宣言します。

④おっと、xml:lang属性を加えるのを忘れていました。setメソッドを使うと、どの要素にでも属性を加えることができます。setメソッドは2つの引数、つまり標準ElementTreeフォーマットの属性名と、属性値を受け取ります。(このメソッドはIXML特有のものではありません。この例で唯一XML特有の部分は引数nsmapで、シリアル化した出力での名前空間プリフィックスを操作できます)。

XMLドキュメントでは、1つのドキュメントに1つの要素となっているのでしょうか?いいえ、もちろん違います。子要素を簡単に作ることができます。
>>> title = lxml.etree.SubElement(new_feed, 'title',          # ①
...     attrib={'type':'html'})                               # ②
>>> print(lxml.etree.tounicode(new_feed))                     # ③
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'/></feed>
>>> title.text = 'dive into &hellip;'                         # ④
>>> print(lxml.etree.tounicode(new_feed))                     # ⑤
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'><title type='html'>dive into &amp;hellip;</title></feed>
>>> 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&amp;hellip;</title>
</feed>
①既存の要素の子要素を作るには、SubElementクラスをインスタンス化します。必要となる引数は親要素(この場合はnew feed)、新しい要素の名前だけです。子要素は親の名前空間マッピングを継承するので、ここでは名前空間プリフィックスを改めて宣言する必要はありません。

②属性を辞書形式で渡すこともできます。キーは属性名、値は属性値です。

③期待どおり、新しいtitle要素がAtom名前空間に作られ、フィード要素の子として挿入されました。title要素はテキストを持っておらず、子要素もないので、lxmlによって空の要素としてシリアル化されています(/>ショートカットを使っています)。

④テキストを要素として追加するには、textプロバティを加えます。

⑤これでtitle要素がテキストでシリアル化されました。">" や "$" が入っているテキストをシリアル化するときは、エスケープします。lxmlはこのエスケープを自動でやってくれます。

⑥シリアル化するときに"pretty printing"を適用することで、終了タグのあとや、子要素があるがテキストがない開始タグのあとに改行を挿入します。用語を使って言うと、 lxmlは" insignificant whitespace"を追 加して出力を読みやすく出力するのです。

👉XMLを作成するサードパーティライブラリであるxmlwitchをチェックするとよいかもしれません。そちらではXML生成時にwith文を多用することで、コードを読みやすくしています。

12.8. 壊れたXMLをパースする

XMLの仕様では、準拠するXMLパーサーは『ドラコニアン(厳格な)・エラーハンドリング』を導入しているということを必須としています。つまり、どんな種類であってもwellformednessエラーをXMLドキュメントで見つけたら、ただちに止まって撃たなければ(エラーを出さなければ)なりません。wellformedness 工ラーは、開始タグと終了タグのミスマッチや、定義されていないエンティティがあるとき、Unicode文字の違反、他多数のマイナなルールを含んでいます。これはHTMLのような他のフォーマットと明確に違っています一HTMLではタグを閉じ忘れていても、属性値の中で&をエスケープし忘れていても、ブラウザはウェブページのレンダリングをやめることはありません。(HTMLにはエラーハンドリングが定義されていないというのは、よくある誤解です。HTMLエラーハンドリングは実際に精巧に定義されていますが、"最初のエラーで止まって撃て"よりも、はるかに複雑です。)

XMLの開発者がドラコニアン・エラーハンドリングをXMLに強制したのは失敗だったと考えている人もいます(私もそうです)。誤解しないで欲しいのですが、エラーハンドリングをシンプルにした利点を私はよく理解しています。しかし、実用上は"wellformedness"という概念は思うよりも難しいもので、特にweb上で公開されhttpを介して提供されるXMLドキュメント(Atomのような)では難しいです。XMLはドラコニアン・エラーハンドリングを1997年に標準化しており、成熟していると言えますが、調査によるとWeb上のAtomフィードの大部分がwellformednessエラ一に悩まされていることが継続的に示されています。

理論性と実用性の両方の理由から、私はXMLドキュメントを"なんとしてでも"パースするのです。つまり、最初のwellformednessエラーで止まって撃ったりはしません。そうしたいとあなた自身も気がついたのであれば、lxmlが助けてくれます。

ここにあるのは、壊れたXMLドキュメントの欠片です。wellformednessエラーをハイライトしています。
<?xml version='1.0' encoding='utf-8'?>
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into &hellip;</title>
...</feed>
この部分がエラーです。なぜなら &hellip; エンティティはXMLでは定義されていません(HTMLでは定義されています)。デフォルトの設定で、この壊れたフィードをパースしようとすれば、lxmlは定義されていないエンティティで窒息してしまいます。
>>> import lxml.etree
>>> tree = lxml.etree.parse('examples/feed-broken.xml')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "lxml.etree.pyx", line 2693, in lxml.etree.parse (src/lxml/lxml.etree.c:52591)
  File "parser.pxi", line 1478, in lxml.etree._parseDocument (src/lxml/lxml.etree.c:75665)
  File "parser.pxi", line 1507, in lxml.etree._parseDocumentFromURL (src/lxml/lxml.etree.c:75993)
  File "parser.pxi", line 1407, in lxml.etree._parseDocFromFile (src/lxml/lxml.etree.c:75002)
  File "parser.pxi", line 965, in lxml.etree._BaseParser._parseDocFromFile (src/lxml/lxml.etree.c:72023)
  File "parser.pxi", line 539, in lxml.etree._ParserContext._handleParseResultDoc (src/lxml/lxml.etree.c:67830)
  File "parser.pxi", line 625, in lxml.etree._handleParseResult (src/lxml/lxml.etree.c:68877)
  File "parser.pxi", line 565, in lxml.etree._raiseParseError (src/lxml/lxml.etree.c:68125)
lxml.etree.XMLSyntaxError: Entity 'hellip' not defined, line 3, column 28
wellformednessエラーがあるにも関わらず、この壊れたxmlドキュメントをパースするのであればカスタムXMLパーサーを作る必要があります。
>>> parser = lxml.etree.XMLParser(recover=True)                  # ①
>>> tree = lxml.etree.parse('examples/feed-broken.xml', parser)  # ②
>>> parser.error_log                                             # ③
examples/feed-broken.xml:3:28:FATAL:PARSER:ERR_UNDECLARED_ENTITY: Entity 'hellip' not defined
>>> tree.findall('{http://www.w3.org/2005/Atom}title')
[<Element {http://www.w3.org/2005/Atom}title at ead510>]
>>> title = tree.findall('{http://www.w3.org/2005/Atom}title')[0]
>>> title.text                                                   # ④
'dive into '
>>> print(lxml.etree.tounicode(tree.getroot()))                  # ⑤
<feed xmlns='http://www.w3.org/2005/Atom' xml:lang='en'>
  <title>dive into </title>
.
. [rest of serialization snipped for brevity]
.
①カスタムパーサーを作るためには、lxml.etree.XMLParserクラスをインスタンス化します。このクラスは多くの種類の引数を受け取ることができます。ここで興味がある引数はrecoverで、これをTrueにす ると、XMLパーサーはwellformednessエラーから" recover"するように全力を尽くすことになります。

②カスタムパーサーを使ってXMLドキュメントをパースするためには、パーサーオブジェクトを2番目の引数としてparse()関数に渡します。lxmlは定義されていない &hellip; エンティティに関してエラーを上げないようにしてくれます。

③パーサーは見つかったwellformednessエラーのログを保持します(実は、エラーから復帰するように設定されているに関わらず、これはTrueです)

④定義されていない &hellip; エンティティに関してはパーサーは何も知らないので、何も告げずにドロップしました。要素titleのテキストコンテントは'dive into 'になりました。

⑤シリアル化からわかるように、&hellip;エンティティは移動しただけではなく削除されています。

重要なことなので繰り返しますが、XMLパーサーを"recover"するときに相互運用性の保証はありません。別のパーサーが &hellip; エンティティをHTMLで認識して、&amp;hellip; を代わりに置き換えるかもしれません。それが"ベター"でしょうか?そうかもしれません。それが"より正確"でしょうか?いいえ、両方共同じくらい間違っています。正しい挙動は(XML仕様に従うと)、停止してエラーを出すことです。そうしないと決めたなら、あとは自分の責任です。

12.9. さらに読むには


    XML on Wikipedia.org
    The ElementTree XML API
    Elements and Element Trees
    XPath Support in ElementTree
    The ElementTree iterparse Function
    lxml
    Parsing XML and HTML with lxml
    XPath and XSLT with lxml
    xmlwitch

0 件のコメント:

コメントを投稿