XMLの論考: PythonにおけるElementTreeのXMLプロセス

APIは、類似のライブラリに匹敵するか

Fredrik LundhのElementTreeモジュールは、Pythonでの軽量で高速なXML文書の操作性により、人気を呼んでいるAPIです。今回Davidは、オブジェクト・ツリーとしてのElementTreeとXMLインスタンスの処理に専念した他の幾つかのライブラリ(特に彼自身のgnosis.xml.objectifyモジュール)を比較します。

David Mertz, Ph.D (mertz@gnosis.cx), Author, Gnosis Software, Inc.

Photo of David MertzDavid Mertz氏は多くの分野で活躍しています。ソフトウェア開発や、それについて著述もしています。その他、学術政策理念について分野を問わず、関係する雑誌に記事も書いています。かなり以前には、超限集合論、ロジック、モデル理論などを研究していました。その後、労働組合組織者として活動していました。そして、David Mertz氏自身は人生の半ばにもまだ達していないと思っているので、これから何かほかの仕事をするかもしれません。



2003年 6月 24日

以前のコラムで、その目的が所定のプログラミング言語で最もよく知られた固有の操作性で違和感なく扱えることにある幾つかのXMLライブラリを考察しました。これらの中で最初に取り上げたのは、Python用に自作したgnosis.xml.objectifyでした。同様にHaskellのHaXmlとRubyのREXMLに関しても回を設けています。ここでは取り上げませんでしたが、JavaのJDOMとPerlのXML::Grove もまた同じような目的を持っています。

最近私は、comp.lang.pythonニュースグループの多くの投稿者がPython用のネイティブXMLライブラリとして、Fredrik LundhのElementTreeを挙げているのに気付きました。もちろんPythonの標準的なディストリビューションには、DOMモジュール、SAXモジュール、expatラッパーや推奨されなくなったxmllib など幾つかのXML APIを備えています。これらのうち、xml.domだけがXML文書をメモリ内のオブジェクト(メソッドを使ってノードを操作できる)に変換します。探してみれば、少しずつ異なった特性を持った、幾つかのPython のDOMインプリメントを見つけることができるでしょう。

  • xml.minidomは、基本的なインプリメントです。
  • xml.pulldomは、必要に応じて、アクセスされたサブツリーを作成します。
  • 4SuiteのcDomlette(Ft.Xml.Domlette)は、高速化のためにPythonのコールバックを回避し、C言語でDOMツリーを作成します。

当然、著者としては、作者としてのうぬぼれもあり、私の目的と使い方に一番適しているgnosis.xml.objectifyElementTreeを比較することに、とても関心があります。ElementTreeの目的は、XML文書の表現をデータ構造として格納することです。そのデータ構造は、あなたがPythonのデータとして考える方法の多くで使用できます。ここでの焦点は、XMLに対するあなたのプログラミング・スタイルをそれに適応させることではなく、Pythonでプログラミングすると言うことにあります。

幾つかのベンチマーク

私の同僚Uche Ogbujiは、他の記事でElementTreeに関する短い論文を書いています(参考文献をご覧ください)。彼が実施したテストのうちの一つは、ElementTreeとDOMの相対速度とメモリ消費量の比較でした。Ucheは比較の対象として、自作のcDomletteを選択しました。残念なことに、私が使用しているMac OSXマシンには、4Suite 1.0a1をインストールできません(対応に向かう動きはあるようですが・・・)。しかし、それらしいパフォーマンスを推定するために、Ucheの評価を利用できます。彼の評価から、速度的にはElementTreeの方がcDomletteよりも30%遅いけれども、メモリ消費量は30%少ないことがわかります。

最も興味をそそられたのは、ElementTreegnosis.xml.objectifyにおける、速度とメモリの比較方法でした。これまでは、具体的に比較すべき方法がなかったため、実際には自分のモジュールの精密なベンチマークテストを行ったことがなかったのです。ここでは、過去にベンチマークテストで使用したことがある、289KBのシェークスピアのHamletのXMLバージョンと、3MBの(XML形式の)Webログという2つの文書を使うことにしましょう。XML文書を種々のツールのオブジェクトモデルにパースするだけで、それ以外の操作を行わないスクリプトを作成しました。

リスト1:Python用XMLオブジェクトの時間を計るスクリプト
% cat time_xo.py
import sys
from gnosis.xml.objectifyimport XML_Objectify,EXPAT
doc = XML_Objectify(sys.stdin,EXPAT).make_instance()
---
% cat time_et.py
import sys
from elementtreeimport ElementTree
doc = ElementTree.parse(sys.stdin).getroot()
---
% cat time_minidom.py
import sys
from xml.domimport minidom
doc = minidom.parse(sys.stdin)

3つのケース全てにおいて、またcDomletteでも、プログラム・オブジェクトの作成方法は似通っています。メモリ使用量は、別のウィンドウでtopの出力を観察することにより評価しました。安定性を考慮して各テストを3回実施し、結果の中間値を採用しました(メモリ使用量は、全てのケースで3回とも同一でした)。

図1:PythonにおけるXMLオブジェクトモデルのベンチマーク
図1:PythonにおけるXMLオブジェクトモデルのベンチマーク

xml.minidomは、適度な大きさを超えるようなXML文書の処理には向いていないということが明らかになりました。その他は(公正に見て)適切な結果になっています。gnosis.xml.objectifyが最もメモリに負荷をかけませんが、オリジナルのXMLインスタンスの全ての情報を保存するわけではない(データ内容は保持されますが、一部の構造的な情報は保持されません)ので、驚くにはあたりません。

以下のスクリプトを使って、同じようにRubyのREXMLをテストしてみました。

リスト2: Ruby REXMLでパースするスクリプト (time_rexml.rb)
require "rexml/document"
include REXML
doc = (Document.new File.new ARGV.shift).root

REXMLは、xml.minidomと同程度のリソースを要すると判明しました。Hamlet.xmlのパースに10秒かかり、14MBを要し、Weblog.xmlのパースには190秒かかり、150MBを要しました。一般的には、ライブラリを比較するよりもまず、プログラミング言語を選択すべきです。


XML文書のオブジェクトを扱う

素晴らしいことにElementTreeは、ラウンド・トリップなのです。つまり、XMLインスタンスを読み込み、いつものような感覚でデータ構造を変更した後に.write() メソッドを使って、整形式のXMLに書き戻すことができると言うことです。もちろん、DOMでも同じことができますが、gnosis.xml.objectifyではできません。gnosis.xml.objectifyでも独自の出力関数を作れば、さほど難しいことではありませんが、それは自動的にできるわけではありません。ElementTreeでは、ElementTreeのインスタンスの.write() メソッドを使って、個々の”要素"のインスタンスを補助関数elementtree.ElementTree.dump()を使ってそれぞれ、シリアライズできます。これはXMLインスタンスのルート・ノードを含む個々のオブジェクト・ノードから、XMLの断片を書き出せると言うことです。

ElementTreegnosis.xml.objectify のAPIを対比する簡単なタスクを紹介します。ベンチマークテストに使用したサイズの大きなweblog.xmlは、それぞれが同じコレクションの子フィールド(データ指向型XML文書の典型的な並び)からなる<entry>要素を8,500程度持っています。このファイルの処理として、各エントリーから、ある特定の値を持った(あるいは、レンジや正規表現にマッチした)いくつかのフィールドを収集するタスクを考えてみましょう。もちろん、このタスクだけを実行したいと思えば、メモリ上で文書全体をモデル化することを避けるために、SAXのようなストリーミングAPIを使うでしょう。けれども、ここでは、大きなデータ構造上でアプリケーションが実行する幾つかのタスクのうちの一つであることを前提にしています。<entry>要素は、以下のような形をしています。

リスト3:<entry>要素のサンプル
<entry>
<host>64.172.22.154</host>
<referer>-</referer>
<userAgent>-</userAgent>
<dateTime>19/Aug/2001:01:46:01</dateTime>
<reqID>-0500</reqID>
<reqType>GET</reqType>
<resource>/</resource>
<protocol>HTTP/1.1</protocol>
<statusCode>200</statusCode>
<byteCount>2131</byteCount>
</entry>

私はgnosis.xml.objectifyを使って、フィルタを通してデータを抽出する次のようなアプリケーションを書くでしょう。

リスト4:Filter-and-extractアプリケーション (select_hits_xo.py)

from gnosis.xml.objectifyimport XML_Objectify, EXPAT
weblog = XML_Objectify('weblog.xml',EXPAT).make_instance()
interesting = [entryfor entryin weblog.entry
if entry.host.PCDATA=='209.202.148.31'and entry.statusCode.PCDATA=='200']
for ein interesting:
print"%s (%s)" % (e.resource.PCDATA,
e.byteCount.PCDATA)

リストの内包表記は、データフィルタとして非常に便利です。基本的にElementTreeも同じ方法で動作します。

リスト5:

from elementtreeimport ElementTree
weblog = ElementTree.parse('weblog.xml').getroot()
interesting = [entryfor entryin weblog.findall('entry')
if entry.find('host').text=='209.202.148.31'and entry.find('statusCode').text=='200']
for ein interesting:
print"%s (%s)" % (e.findtext('resource'),
e.findtext('byteCount'))

上記の二者間の違いに注目してください。gnosis.xml.objectifyは、ノード(全てのノードは、タグ名にちなんで名づけられたカスタムクラス)の属性として、部分要素のノードに直接関連づけられています。もう一方のElementTreeは、子ノードを見つけるために、"要素"のクラスのメソッドを使用します。.findall()メソッドは、マッチした全てのノードのリストを返します。.find()は、単に最初にマッチしたノードを返し、.findtext()は、ノードのテキスト内容を返します。gnosis.xml.objectifyの部分要素上で、最初にマッチしたノードだけを望むならば、node.tag[0]のようにインデックスを付ける必要があります。ただし、当該の部分要素が単一ならば、明示的にインデックスを付加することなく参照できます。

けれどもElementTreeの例では、実際には、全ての<entry>要素をきちんと見つける必要はありません。繰り返し処理で"要素" のインスタンスは、リストのように操作できます。注目すべきポイントは、どんなタグを持っていようと、全ての子ノードに対して繰り返し処理が行われるということです。対照的にgnosis.xml.objectifyのノードでは、全ての部分要素をステップスルーする組み込みメソッドを持っていません。今まで通りに、1行でchildren() 関数(将来のリリースに含めます)を作成することは容易です。リスト6とリスト7を比べてみてください。

リスト6:ノードリスト及び特定の子タイプに対するElementTreeの繰り返し処理
>>> open('simple.xml','w.').write('''<root>
... <foo>this</foo>
... <bar>that</bar>
... <foo>more</foo></root>''')
>>> from elementtree import ElementTree
>>> root = ElementTree.parse('simple.xml').getroot()
>>> for node in root:
...     print node.text,
...
this that more
>>> for node in root.findall('foo'):
...     print node.text,
...
this more

こちらが、リスト7

リスト7: 全ての子ノードに対するgnosis.xml.objectifyの損失を伴う繰り返し処理
>>> children=lambda o: [x for x in o.__dict__ if x!='__parent__']
>>> from gnosis.xml.objectify import XML_Objectify
>>> root = XML_Objectify('simple.xml').make_instance()
>>> for tag in children(root):
...     for node in getattr(root,tag):
...         print node.PCDATA,
...
this more that
>>> for node in root.foo:
...     print node.PCDATA,
...
this more

お分かりだと思いますが、現在のところ、gnosis.xml.objectifyは、散在する<foo>及び<bar>要素のオリジナルの順序に関する情報を廃棄します。(.__parent__のような別のMagic 属性で記憶されるかもしれませんが、これに関して誰もパッチを必要としませんでしたし、意見もありませんでした)

ElementTree.attribと呼ばれるノード属性にXML属性を保管します。その属性は辞書に格納されます。gnosis.xml.objectifyはXMLの属性を、直接対応する名前のノード属性にします。私が使用するこのスタイルは、XMLの属性と要素の内容との本質的な相違をフラットにする傾向がありますが、属性と要素の内容に関しては、私のネイティブなデータ構造ではなく、XMLにとって懸念すべき重要なものだと考えます。例えば・・・

リスト8: 子要素及びXMLの属性に対するアクセス方法の違い
>>> xml = '<root foo="this"><bar>that</bar></root>'
>>> open('attrs.xml','w').write(xml)
>>> et = ElementTree.parse('attrs.xml').getroot()
>>> xo = XML_Objectify('attrs.xml').make_instance()
>>> et.find('bar').text, et.attrib['foo']
('that', 'this')
>>> xo.bar.PCDATA, xo.foo
(u'that', u'this')

今のところgnosis.xml.objectifyは、「テキストを含むノード属性を作成するXMLの属性」 と、「オブジェクトを含む(おそらく.PCDATAを持つサブノードの)ノード属性を作成するXMLの要素の内容」 との間に、若干の相違があるでしょう。


XPath と tail

ElementTree.find*() メソッド中に、XPathのサブセットを実装しています。このスタイルを使うことは、特にワイルドカードを含むXPathで、サブノードの中の各レベルを参照するために、コードをネストするよりずっと簡潔でしょう。例えば、私が自分のWebサーバにヒットされた全てのタイムスタンプに興味を持っていたとしたら、次のようにしてweblog.xmlを調べるでしょう。

リスト9: XPathを使ってネストされた部分要素を見つける
>>> from elementtree import ElementTree
>>> weblog = ElementTree.parse('weblog.xml').getroot()
>>> timestamps = weblog.findall('entry/dateTime')
>>> for ts in timestamps:
...     if ts.text.startswith('19/Aug'):
...         print ts.text

もちろん、weblog.xmlのような標準的で階層の浅い文書であれば、リストの内包表記を使って同じことをするのは簡単です。

リスト10: リストの内包表記を使い、ネストした部分要素を取り出す
>>> for ts in [ts.text for e in weblog
...            for ts in e.findall('dateTime')
...            if ts.text.startswith('19/Aug')]:
...     print ts

しかし、一般的文章(prose)型のXML文書は、はるかに多くの可変的な文書構造を持っており、典型的に少なくとも5ないし6レベルの階層にタグをネストする傾向があります。例えば、DocBook や TEI のような XMLスキーマでは、セクション、サブセクション、参考文献や時としてイタリック体のタグや引用文といった中に、他からの引用を持っているかもしれません。全ての<citation>要素を見つけ出すには、各層にわたった煩雑な(おそらくは再帰的な)検索を要求されるでしょう。さもなければXPathを使って、以下のように書くでしょう。

リスト11: XPathを使って深くネストした部分要素を見つける
>>> from elementtree import ElementTree
>>> weblog = ElementTree.parse('weblog.xml').getroot()
>>> cites = weblog.findall('.//citation')

ところが、ElementTreeではXPath のサポートは限定されています。完全なXPath に含まれる種々の関数を使えませんし、属性を検索することもできません。それでもElementTreeのXPathのサブセットは、XPathを使う上で十分意味があり、読みやすさと表現力という点で大きな手助けとなるでしょう。

この記事を終える前に、ElementTreeのもう一つの特徴的な機能について触れておきたいと思います。XML文書では、内容(コンテンツ)をいろいろと混ぜ合わせることができます。とりわけ、一般的文章型のXMLでは、PCDATAや タグ に比較的自由に点在させる傾向があります。では子ノード間に入っているテキストを、正確には何処に格納すべきでしょうか?ElementTreeの"要素"のインスタンスは、(文字列を含む)単一の.text属性を持っていますが、分断された一連の文字列を格納する余地は全くありません。ElementTreeが採用した解決策は、各ノードに.tail属性を与えることでした。.tail属性は、終了タグの後で、かつ次の要素が始まる前 か 親エレメントが閉じられる前 に位置する全てのテキストを含みます。次の例を見てください・・・

リスト12:node.tail属性に格納されるPCDATA
>>> xml = '<a>begin<b>inside</b>middle<c>inside</c>end</a>'
>>> open('doc.xml','w').write(xml)
>>> doc = ElementTree.parse('doc.xml').getroot()
>>> doc.text, doc.tail
('begin', None)
>>> doc.find('b').text, doc.find('b').tail
('inside', 'middle')
>>> doc.find('c').text, doc.find('c').tail
('inside', 'end')

まとめ

ElementTreeはPythonでXMLを処理する際、提供されている DOM以上の軽量化をもたらすオブジェクトモデルであり、すばらしい力作です。本記事においては、取り上げませんでしたが、ElementTreeは既存のXMLデータを操作するのと同じように、ゼロからXML文書を生成することにも優れています

類似のライブラリgnosis.xml.objectifyの作者として、ElementTreeの評価については完全に客観的になりえません。それでもなお、私はPythonプログラムでElementTreeが提供した以上のものを目指して、より自然なアプローチを探し続けます。ElementTreeでは一般的にデータ構造を操作する際に、アプリケーションの中に構築されたデータ構造に直接アクセスするノード属性ではなく、まだノードメソッドを使用しているからです。

しかし幾つかの分野で、ElementTreeは異彩を放っています。深くネストした要素にアクセスする場合、手動で再帰的に検索を行うよりも、XPathを使う方がはるかに簡単です。当然のことながら、DOMでもXPathは使えますが、はるかに重く、一貫性に欠けるAPIを使わざるを得ません。ElementTreeの全ての"要素"のノードは、DOMのノードタイプの一式とは違い、一貫した方法で動作します。

参考文献

  • ElementTreeの詳細は、Fredrik LundhのElement Treesページでご覧になれます。
  • developerWorks のコラムニストのUche Ogbujiによるthis XML.com articleは、より深い観点から記述されています。
  • David MertzのXML ライブラリに関する初期のコラムをお読みください。
    • XMLの論考 第2回では、その時点では、単にxml_objectifyと呼ばれたgnosis.xml.objectifyを紹介しました。(developerWorks 、2000年8月)
    • XMLの論考 第11回では、幾つかの初期改良をしたgnosis.xml.objectifyの最新情報を読者に提供しました。新しい機能の幾つかは、このコラムでは紹介しきれませんでしたが、モジュールのHISTORYと他の文書ファイルを見てください。(2001年6月)
    • XMLの論考 第14回では、Haskellの遅延パターンを使った関数型プログラム言語用にHXmlを取り上げました。(2001年9月)
    • XMLの論考 第18回では、RubyのREXMLライブラリを論じました。(2002年3月)
    以前の記事は、記事一覧でお読みになれます。
  • 他のPython 用 XML API/ライブラリに関しては、generateDSでご覧ください。開発者Dave Kuhlmanは、generateDSgnosis.xml.objectifyを比較した非常に優れた小論文を著しました。要約すると、generateDSを使って、XMLインスタンスにおいて正確にエレメントを取り扱うPythonクラスの基盤として、XMLスキーマを使うというアイディアです。一般的には、generateDSはXMLツリーを操作するよりもむしろ、特定のXML文書のスキーマを処理するPythonモジュールのコードジェネレーターと言えます。自動生成されたコードは、カスタムアプリケーションをすばやく構築するために、簡単に適用できます。
  • XMLに関する参考文献がdeveloperWorksXMLゾーンに多数あります。
  • IBM WebSphere Studioは、Javaと他の言語の両方でXML開発を自動化するツールのスイートを提供します。
  • XMLおよび関連テクノロジーのIBM認証開発者になる方法についてはこちらを参照してください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML
ArticleID=241470
ArticleTitle=XMLの論考: PythonにおけるElementTreeのXMLプロセス
publish-date=06242003