Amara XML Toolkitマニュアル(version 1.2)
ユーザーマニュアル日本語訳

This version
Revision 1.2
原文
Amara/Manual - xml3k Wiki
訳注
Jintrick.netによる意訳+メモです。原文との違いに留意してください。

概要


目次

  1. 導入
  2. スタートガイド
  3. Amara Binery: XMLをPython風に
    1. より複雑な例
    2. DTDによる検証
    3. Binderyの仕事
    4. 複雑な子要素
    5. ノードの構造を調べる
    6. XMLを書き出す
    7. XPath
    8. バインダリノードにXSLTを適用する
    9. 名前空間
    10. 名前に関するあれこれ
    11. プッシュバインディング
    12. 変更、修正
    13. 要素(と属性)の作成
    14. 属性の変更、修正
    15. XML断片の追加
    16. 完全なdocumentの作成
    17. ノード(子孫含めて)をコピーする
    18. PIとコメント
    19. 文書型(Document types)
      1. parseの将来のdoctypeサポート
    20. バインディングのカスタマイズ
      1. 単純な文字列値としていくつかの要素を扱う
      2. 特定の要素を完全に無視する
      3. 特定のノードタイプを完全に無視する
      4. 空白文字の除去
      5. 要素のスケルトンの作成
      6. 型の推論
      7. カスタム規則における名前空間の使用
    21. プッシュバインディイングと規則
    22. カスタムバインディングのクラスをを使用する
      1. カスタマイズされたバインディングについての一般的な注意点
    23. バインダリの拡張ガイド
      1. バインダリの法則
  4. Scimitar: 最も柔軟なプログラミング言語のための最も柔軟なスキーマ言語
  5. AmaraのDOMツール: DOMにPython風のインタフェイスを
    1. The pushdom
    2. ジェネレータツール
    3. 与えられたノードのXPathを得る
  6. AmaraのSAXツール: 脳みそが爆発しないSAX
  7. Flextyper: XML処理のためのユーザ定義のPythonデータ型

導入

Amara XML ToolsはXMLデータバインディングのためのPythonツールコレクションである。Pythonで書かれたのは単なる気まぐれではなく、ゼロから徹底的にPythonのイディオムが使われ、また他のプログラミング言語に対するPythonの多くのアドバンテージが利用されている。

Amaraは4Suiteに基盤としている。しかし4SuiteがよりXML標準の従順な実装を目指しているのに対して、Amaraは4Suiteの力にPython風のインタフェイスを追加するものである。

Amaraの主要な構成要素:

Bindary
データバインディングツール(極めてPython風のXML APIをしゃべる「しゃれた」方法)

他の構成要素:

Scimitar
ISO ShcematronというXMLスキーマ言語の実装で、SchematronファイルをPythonスクリプトに変換する
domtools
Python DOMを増補するツール群
saxtools
SAXをPythonで使いやすくするツール群

スタートガイド

使い始めるもっとも簡単な方法は、EasyInstallを使って、Amaraとその依存関係にあるパッケージ全てを一気にインストールしてしまうことだ。easy_install amaraとタイプするだけで全て設定される。実行時トラブルがあるなら、EasyInstallを持っていないからだろうが、これはとても簡単にセットアップされる。それでも問題がある場合、メーリングリストで彼らに報告してほしい。

Amara Binery: XMLをPython風に

次の例は、シンプルなXMLファイル monty.xml、からバインディングを作成する方法を示す。(取り上げられている全ての例示用ファイルは、Amaraパッケージのdemoディレクトリに置かれている。)

まず、monty.xmlの内容は次のようになっている:

<?xml version="1.0" encoding="utf-8"?>
<monty>
  <python spam="eggs">
    What do you mean "bleh"
  </python>
  <python ministry="abuse">
    But I was looking for argument
  </python>
</monty>

ここで、バインディングを作成するコードは次のようになる。

import amara
doc = amara.parse('monty.xml')

docはデータバインディングの結果であり、XMLを表現するオブジェクトである。XML文書全体をバインダーに与えると、トップレベル要素を表現したメンバーを持つ文書、それそのものを表現したオブジェクトを得ることができた。

それは簡単なものだ。"eggs"値を(Python Unicodeオブジェクトとして)得るには、次のように書ける:

doc.monty.python.spam

あるいは"But I was looking for argument"という内容をもった要素を得るには、こう書ける:

doc.monty.python[1]

parse関数には、file-likeオブジェクト(stream)も、文字列も渡すことができる。ローカルファイルパスやURLからXMLを取得したXMLをパースするためのparse_path関数もある(※)。

※ 訳注: 手持ちのamara version 1.2.0.2では、amara.parse_path関数がなく、amara.parse関数に統一されているようだ。そして、amara.binderytoolsモジュールにおいて、引数のタイプ別にbind_file関数, bind_stream関数, bind_string関数, bind_uri関数の4つに細分化されている。

amara.parseの引数には、(Unicodeオブジェクトではない)XML文字列、open-file-likeオブジェクト、ファイルパス、URIを与えることができる。

※訳注: parse_path関数にファイルパスを与えるとFt.Lib.UriExceptionが投げられる場合がある。bind_file関数ならこの問題は起こらないようだ。

より複雑な例

以下の例はXBELファイル(Python専用のXML-SIGが開発したブックマークのための人気のXMLフォーマット)よりバインディングを作成する方法を示す:

doc = amara.parse('xbel.xml')

次のサンプルは最初と二番目のブックマークタイトルをprintしている。

print doc.xbel.folder.bookmark.title
print doc.xbel.folder.bookmark[1].title

このバインダリはできるだけ自然な感じに扱おうと試みるのが特徴だ。通常、子要素にアクセスするにはただそれらの名前を使えば良い。一つ以上の同じ名前のものがあった場合、最初のものをバインドする。list index(list[num])を用いて、同じ名前をもった複数の子要素の一つを特定することができる。本来、より明示的な方法で、最初のブックマークのタイトルを得る方法は、次のようになる:

print doc.xbel.folder.bookmark[0].title

titleはテキストのみを含んだ要素である。表現doc.xbel.folder.bookmark[0].titleはこの要素を表現したバインディングオブジェクトを返す。そのようなオブジェクトは、Unicode変換メソッドを持っているため、それらの要素の子孫テキスト(その要素のサブツリーのすべてのテキストノード)を得ることができる。上記のコードラインはそれらをprintする。次の4つのコードラインは、全て等価である:

print doc.xbel.folder.bookmark.title
print doc.xbel.folder.bookmark[0].title
print unicode(doc.xbel.folder.bookmark.title)
print unicode(doc.xbel.folder.bookmark[0].title)

Amaraにおいて、unicode変換をelementノードでcallするのは、XPathにおいて、string関数をelementノードでcallするのに非常に似ている。

次のコード片は、ファイル内のすべてのブックマークURLをプリントアウトする再帰関数である。

def all_titles_in_folder(folder):
    #Warning: folder.bookmark will raise an AttributeError if there are no bookmarks
    for bookmark in folder.bookmark:
        print bookmark.href
    if hasattr(folder, "folder"):
        #There are sub-folders
        for folder in folder.folder:
            all_titles_in_folder(folder)
    return

for folder in doc.xbel.folder:
    all_titles_in_folder(folder)

しかし、実際の使用では恐らくこういうことはしないだろう。このタスクはXPathを使って、より小さなコードで、再帰を用いず、そしてより速い方法で実現できる。これは後述のセクションでカバーする。

同名の要素の数をカウントしたいなら、lenを使う:

len(doc.xbel.folder)

これでトップレベルフォルダの数を得ることができる。

DTDによる検証

amara.parse()において、validate=Trueを設定することで、基礎的なパーサーにDTDによる検証を行わせることができる。

バインダリの仕事

デフォルトでは、バインドされたXML要素は専用オブジェクトとなる。つまりそれぞれの一般識別子(要素名)に対して、bindery.element_baseを継承したPythonクラスが作られる。一方属性は、単純なデータメンバ、つまり属性値をもったUnicodeオブジェクト値となる。

要素オブジェクトは単一オブジェクトとして扱えるようにするために特別に構築されている。(そのケースでは対応した名前の最初の子要素が選択されるが、リストアイテムアクセスすることも、イテレートすることもできる)

例に戻ろう。binding.xbel.folder.bookmarkは、binding.xbel.folder.bookmark[0]と同じである。どちらも最初のフォルダの最初のブックマークを返す。最初のフォルダの二番目のブックマークを得るには、binding.xbel.folder.bookmark[1]を使う。

複雑な子要素

バインダリは、ソースXMLの順番に関する情報を初期より保持していて、文書オブジェクトと子要素リストを通じてこれにアクセス可能だ:

folder.xml_children

子要素リストにおいて、子要素はそれぞれに対応するバインディングオブジェクトを使って、子テキストは単純にUnicodeオブジェクトなって表現される。バインドされた子テキストはデフォルトで正規化される、つまりバインディングはxml_childrenにおいて隣り合う二つのテキストノードには一切置き換わらないことに注意すべきだ。

要素ノードが子要素とテキストを内容としていた場合に、どうやってテキストノードにアクセスするのだろうか。要素の子テキストを得るにはxml_child_textプロパティを使う。文書、<a>1<b>2</b>3<c/></a>において、a.xml_child_textu'13'を返す。一方、Unicode変換(unicode(a))はu'123'を返す。

ノードの構造を調べる

xml_docメソッドをcallすることで、最もバインダリなオブジェクトの構造についての情報を得ることができる。

>>> import amara
>>> doc = amara.parse('monty.xml')
>>> print doc.xml_doc()
Object references based on XML child elements:
monty (1 element) based on 'monty' in XML
>>> print doc.monty.xml_doc()
Object references based on XML child elements:
python (2 elements) based on 'python' in XML
>>> print doc.monty.python.xml_doc()
Object references based on XML attributes:
spam based on 'spam' in XML
Object references based on XML child elements:
>>> print doc.monty.python.spam
eggs

これは「人が読むための」便利なツールである。しかし「計算機が読むための」形式をしたノードの構造情報も得たいだろう。xml_propertyはXMLの属性と要素のオブジェクト参照名を表現するkeyをもった辞書を返す。

>>> import amara
>>> doc = amara.parse("<a x='1'>hello<b/>lovely<c/>world</a>")
>>> doc.a.xml_properties
{u'x': u'1', u'c': <amara.bindery.c object at 0xb7bbcdcc>, u'b': <amara.bindery.b object at 0xb7bbcb8c>}

xml_child_elementsは似たような辞書を返すが、子要素だけに絞られている。

>>> #Continuing from the above snippet
>>> doc.a.xml_child_elements
{u'c': <amara.bindery.c object at 0xb7bbcdcc>, u'b': <amara.bindery.b object at 0xb7bbcb8c>}

XMLを書き出す

文書順に関する情報の保持は、バインダリがバインディングオブジェクトをXMLフォームに戻すために必要であり、その為にはxmlメソッドを使う。

print doc.xml()

xmlメソッドは符号化されたテキストを返す。Unicodeではない。デフォルトの符号化方式はUTF-8である。文書の一部分をシリアライズすることもできる。

print doc.xbel.folder.xml() #Just the first folder

streamに出力を渡すこともできる。

doc.xml(sys.stdout)

出力する符号化方式を制御することもできる。綺麗にプリントしたり、XML宣言を出力したり等、XSLTのxsl:outputと同等の出力制御パラメータを用いることができる。たとえば、omitXmldeclaration=u"yes"を与えたなら、XML宣言は出力されない。その他の設定可能なパラメタを示す:

Amaraに(ルートノードや)要素で、関連付けられていようといまいと、名前空間宣言を強制することができる。これは要素の内容においてQNameを使う際に特に便利である(隠れた名前空間)。

doc.xml(force_nsdecls={u'x': u'http://example.com/'})

force_nsdeclsはkeyにUnicodeの接頭辞、valueに名前空間URIをもった辞書である。これはたとえば、知られている全ての名前空間宣言をトップに強制する("分別のある"文書)のを簡単にする:

doc.xml(force_nsdecls=doc.prefixes)

訳注:1.2.0.2でprefixesプロパティは存在しないが……

警告:文書中の名前空間宣言は、上記のような方法で強制されたそれに常に打ち勝つ。これはデフォルト名前空間をnullにする暗黙的な宣言を含む。言い換えると、XML名前空間を使っていない文書を、前述のような簡単な行程でデフォルト名前空間を持つ文書に変化させることはできない。

XPath

バインダリはXPathのサブセットをサポートしており、これはほとんど標準に準拠している。サポートされていないXPathの特長は非常にレアなものであり、実際に使用する場面においては、完全なサポートであるとみなしても良いだろう。次の例は全てのトップレベルフォルダを取得する:

tl_folders = doc.xbel.xml_xpath(u'folder')
for folder in tl_folders:
    print folder.title

XPathクエリのコンテクストにしたいオブジェクトのxml_xpathメソッドを呼び出している。最初のフォルダの最初のブックマークの最初の子要素を(要素名を指定せず)得るには、このようにするか:

oc.xbel.folder.bookmark.xml_xpath(u'*[1]')

あるいは:

doc.xbel.xml_xpath(u'folder[1]/bookmark[1]/*[1]')

あるいは:

doc.xbel.xml_xpath(u'/folder[1]/bookmark[1]/*[1]')

あるいは:

doc.xml_xpath(u'xbel/folder[1]/bookmark[1]/*[1]')

等のようにする。

警告:Pythonでは、リストのインデックスは0から始まるが、XPathでは1から始まる。

訳注:XPathのそれはリストのインデックスではない。[position() = index]の省略形に過ぎない。

注意:このXPathはノード集合を返し、Pythonではノードのリストとして扱われる。一つのノードを含むリストということになるが、しかし[0]を使って抽出しなければならない。

戻り値はXPath表現(expr)に依存する。

次の例はファイル内の全てのブックマークURLをプリントアウトする。しかしもっとシンプルでより簡潔なコードを前述している

bookmarks = doc.xml_xpath(u'//bookmark')
for bookmark in bookmarks:
    print bookmark.href

次は、attributeクエリを使って、単に文書内に登場する全てのhrefを返す:

hrefs = doc.xml_xpath(u'//@href')
for href in hrefs:
    print unicode(href)

次は4Suiteプロジェクトへのブックマークのタイトルをプリントする:

url = u"http://4suite.org/"
title_elements = doc.xml_xpath('//bookmark[@url="%s"]/title'%url)
#XPath node set expression always returns a list
print unicode(title_elements[0])

バインダリノードにXSLTを適用する

バインダリ文書または要素に対して、直接XSLTを適用することができる。もちろんxml()してシリアライズしたものをXSLTに適用することもできるが、恐らくノードに対して直接行ったほうが簡単だし効果的だ。これを行うには、xml_xslt()を使う。

警告: この関数はかなり制限されており、最も簡単な変換しか使わない。この方法で変換が動作しない場合、xml()でシリアライズしてから、Ft.Xml.Xslt.transformを使うこと。これは完全なXSLT適合である。

result_string = doc.xml_xslt('transform.xslt')

必要なただ一つの引数は、XSLTスタイルシートへの参照である。これは(Unicodeでない)文字列、file-likeオブジェクト(stream)、ファイルパス、URIまたはFt.Xml.InputSource.InputSourceインスタンスのいずれでもよい。文字列かstreamの場合、それはself-contained XMLでなければならない。すなわち、外部実体参照やinclude等の他のリソースへのアクセスを要求してはならない。

オプションのparam引数にはスタイルシートパラメタからなる辞書を指定できる。このkeyは、名前空間を持っていないならUnicodeオブジェクトで、持っている場合(uri, localname)のtupleで与えることができる。

オプションのoutput引数は、出力が(行程とともに徐々に)書き出されるfile-likeオブジェクトである。

次の例を見てみよう:

params = params={u'id' : u'2', (u'http://example.com/ns', u'tags') : [u'a', u'b', u'c']}
result_string = doc.xml_xslt('transform.xslt', params=params)

これはidという(接頭辞のない)トップレベルパラメタと、xがhttp://example.com/nsの接頭辞、xをもつx:tagsというトップレベルパラメタを渡したXSLTを実行する。後者はテキストノードのノード集合をパラメタとして提供している。

doc.xml_xslt(TRANSFORM_STRING, output=open('/tmp/foo.txt', 'w'))

これは文字列として与えられたXSLTを実行し、ファイル/tmp/foo.txtに出力ストリームを吐く(XSLT出力は、テキスト、XMLまたはHTMLになり得る点に留意されたい)。このようにoutputが明示されている場合、戻り値はない。

result_string = doc.xml_xslt(open('transform.xslt'))

これはXSLTファイルをopen fileオブジェクトから読み込む。

名前空間

バインダリは名前空間をもった文書に対応している。次の例はRSS1.0フィードの中身の概要を表示する:

import amara

#Set up customary namespace bindings for RSS
#These are used in XPath query and XPattern rules
RSS10_NSS = {
    u'rdf': u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
    u'dc': u'http://purl.org/dc/elements/1.1/',
    u'rss': u'http://purl.org/rss/1.0/',
    }

doc = amara.parse('rss10.rdf', prefixes=RSS10_NSS)

#Create a dictionary of RSS items
items = {}
item_list = doc.xml_xpath(u'//rss:item')
items = dict( [ ( item.about, item) for item in item_list ] )
print items

for channel in doc.RDF.channel:
    print "Channel:", channel.about
    print "Title:", channel.title
    print "Items:"
    for item_ref in channel.items.Seq.li:
        item = items[item_ref.resource]
        print "\t", item.link
        print "\t", item.title

次はバインダリオブジェクトにおいて名前空間の構成要素がどのように保持されるかを示している。

#Show the namespace particulars of the rdf:RDF element
print doc.RDF.namespaceURI
print doc.RDF.localName
print doc.RDF.prefix

属性については、xml_attributesが、その要素の全ての属性の名前空間の情報を含んだ辞書である。詳細に関しては以下のAttributesの章を参照されたい。

名前空間はXPathで自然に機能する:

#Get the RSS item with a given URL
item_url = u'http://www.oreillynet.com/cs/weblog/view/wlg/532'
matching_items = doc.RDF.xml_xpath(u'//rss:item[@rdf:about="%s"]'%item_url)
print matching_items
assert matching_items[0].about == item_url

上の例では名前空間接頭辞と名前空間名のマッピングを手動でセットしてある。しかし、XML名前空間が行儀よく使用されるなら、このステップを踏む必要はないだろう。バインダリのdocumentオブジェクトはパースした文書であれば、トップレベル要素に作られた名前空間宣言を自動で記憶する。

名前に関するあれこれ

amaraバインディングで使用されるPythonオブジェクトの参照及びクラス名は、対応したXML IDに基づいて作られている。しかしそのようなマッピングには制限がある。その一例として、XMLはPythonのIDで許されない、-(ダッシュ)のような文字をもを許してしまう。このようなケースでは、Amaraは名前を壊し、デフォルトではアンダースコアを使用する。<a-1 b-1="" />のような文書のケースでは、doc.a_1を使って要素にアクセスでき、またdoc.a_1.b_1を使って属性にアクセスできる。

これはXMLを照会する一般的なコードを書く際に問題をはらんでいる。何故なら文書のコンテクストによっては異なる方法で要素名が壊されるかもしれないからである。正しいXMLの名前を使ったオブジェクトへのアクセスは、XPathを用いればいつでも可能だ:

doc.xml_xpath(u"a-1/b-1")

またPythonのマッピングプロトコルを使ってオブジェクトにアクセスすることもできる:

doc[u'a-1'][u'b-1']

これは次と等価である:

doc[None, u'a-1'][None, u'b-1']

このNoneは、名前空間名だが、望まれるオブジェクトが実際にオリジナルの文書における名前空間にあるなら、もちろんUnicodeオブジェクトとして明示すべきである。Amaraはかなりのレベルで曖昧性の除去を行う:

from xml.dom import Node E = Node.ELEMENT_NODE doc[E, None, u'a-1'][E, None, u'b-1']

これは属性の名前がそのownerの子要素の名前と衝突するという、非常にまれなシチュエーションに対応する。たとえば、<a-1 b-1=""><b-1/></a-1>では上記のようにしてb-1要素にアクセス可能であり、同じ名前の属性には、A = Node.ATTRIBUTE_NODEを済ませているならdoc[E, None, u'a-1'][A, None, u'b-1']でアクセスできる。

オリジナルのXMLの名前に関する情報は、DOM由来のプロパティを使って得ることができる。

そして属性に関する似たような情報を得るには、elementノードのxml_attributeの辞書を利用できる。

プッシュバインディング

巨大なXMLファイルを扱っている場合、同時に全てのデータバインディングをメモリに保持したいとは思わないかも知れない。少しずつインスタンス化したいはずだ。もし文書をどのように解体したいのかについて明確なパターンをつかんでいるなら、amara.pushbind関数を使うことができる。次の例を見てみよう:

import amara

for folder in amara.pushbind('xbel.xml', u'/xbel/folder'):
    title = folder.title
    bm_count = len(list(folder.bookmark))
    print "Folder", title, "has", bm_count, "top level bookmarks"

すばらしいのは、このプログラムが一時にバインディング全体をメモリに保持しない点である。それぞれのイテレーションにおいて、これはただ各トップレベルのfolder要素を表現するのに必要なだけのXMLをロードする。pushbindは、呼び出される毎に、それぞれ小さなサブツリーをyieldするジェネレータである。

一般的に、pushbindのサブツリーは、その最初の引数(※)として与えられたXSLTパターン(訳注:XPath patternのことらしい)に基づいたamaraバインダリオブジェクトである。XMLソース、文字列(string = keywordを使う)、URIまたはfile名(source = keywordを使う)を渡すこともできる。

※訳注:上の例では二番目の引数に与えているように見えるのだが

インタラクティブなセッション:

>>> XML="""\
... <doc>
...   <one><a>0</a><a>1</a></one>
...   <two><a>10</a><a>11</a></two>
... </doc>
... """
>>> import amara
>>> chunks = amara.pushbind(XML, u'a')
>>> a = chunks.next()
>>> print a
0
>>> print a.xml()
<a>0</a>
>>> a = chunks.next()
>>> print a.xml()
<a>1</a>
>>> a = chunks.next()
>>> print a.xml()
<a>10</a>
>>> a = chunks.next()
>>> print a.xml()
<a>11</a>
>>> a = chunks.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration

上の例において、XPath pattern "a"はファイル内のこの名前を持つ要素全てにマッチする。このコードにおいては、"a"要素の「外側の」XMLは原則的に無視されるので、その他の関心のある全ての部分を含めるにはXPath patternを調整する必要が出てくる。次の例では、最初の二つの"a"要素だけが、ジェネレータのyieldに含まれている。

>>> chunks = amara.pushbind(XML, u'one/a')
>>> a = chunks.next()
>>> print a.xml()
<a>0</a>
>>> a = chunks.next()
>>> print a.xml()
<a>1</a>
>>> a = chunks.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
StopIteration

/doc/one/aというパターンも、サンプル文書について似た結果をもたらすだろう。

これらのパターンに名前空間を使うには、pushbind関数に接頭辞を定義した辞書を渡せば良い。

import amara
#Set up customary namespace bindings for RSS
#These are used in XPath query and XPattern rules
RSS10_NSS = {
u'rdf': u'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
u'dc': u'http://purl.org/dc/elements/1.1/',
u'rss': u'http://purl.org/rss/1.0/',
}

#Print out all titles in the RSS feed
items = amara.pushbind('demo/rss10.rdf', u'rss:item',
                               prefixes=RSS10_NSS)
for item in items:
    print item.title
#Print out all titles in the RSS feed, slightly different approach
chunks = amara.pushbind('demo/rss10.rdf', u'rss:item/rss:title',
                               prefixes=RSS10_NSS)
for title in chunks:
    print title

変更、修正

バインディングに変更を加えるのはとても容易である。属性値の変更は、簡単な代入でよい。

import amara
doc = amara.parse('monty.xml')
doc.monty.foo = u'bar'
doc.monty.spam = u'[attr modified]'

テキストノードを一つだけもった要素の内容も、似たような書き方で置き換えられる:

doc.monty.python = u'[elem 1 modified]\n'
doc.monty.python[1] = u'[elem 2 modified]\n'

上記のコードの結果はこうなる:

<monty>
  <python spam="[attr modified]" foo="bar">[elem 1 modified]
</python>
  <python ministry="abuse">[elem 2 modified]
</python>
</monty>

要素のすべての子を空にするにはこう:

doc.monty.python.xml_clear()

新たな要素やテキストを加えよう。newという名前の新しい空要素を作成し、それを最初のpython要素の最後の子供として追加してみる:

doc.monty.python.xml_append(doc.xml_create_element(u'new'))

テキストノードを追加する。

doc.monty.python.new.append(u'New Content')

訳注: これは恐らく次のtypo:

doc.monty.python.new.xml_append(u'New Content')

存在する子ノードの前後といった特定の位置に新しい要素やテキストを挿入することもできる。

#Create a new `python` element as the second element child of `monty`
doc.monty.xml_insert_after(doc.monty.python,
                           doc.xml_create_element(u'python'))
#Create a new `python` element as the first element child of `monty`
doc.monty.xml_insert_before(doc.monty.python,
                            doc.xml_create_element(u'python'))

特定の要素、属性、テキストノードを削除することもできる。

del doc.monty.python

これは次と同じである:

del doc.monty.python[0]

xml_remove_childメソッドを使うこともできる:

child = doc.monty.python
doc.monty.xml_remove_child(child)

削除方法ついて徹底的に列挙してもいい。あるオブジェクトがその兄弟ノードの中でどの位置にあるかを、それを親から取り除くために使用することができる。この「位置」を得るにはxml_index_on_parentプロパティを用いる。

ix = doc.monty.python.xml_index_on_parent

最初のpython要素はmontyの二番目の子なので、これはixに値「1」を代入する。

ix = doc.monty.python[1].xml_index_on_parent

二番目のpython要素はmontyの4番目の子であるため、これはixに値「3」を代入する。indexを得たなら、それをxml_remove_child_atに渡すことで特定の子要素を削除するのに使用することができる。

doc.monty.xml_remove_child_at(3)

これは上で決定されたindexを用いて、二番目のpython要素を削除する。これはテキストノードにも同様に動作する:

doc.monty.xml_remove_child_at(0)

これは最初のテキストノードを削除する。このメソッドを呼ぶときにはindexを省略することもできる。デフォルトではそれは最後の子ノードを削除する:

doc.monty.xml_remove_child_at()

もし名前に関するあれこれの章で取り上げたような一意的な名前に関する問題に突き当たったなら、変更修正に関するマッピングプロトコルを使用可能だ。たとえば「b-1」という子要素を、次の構造
<a-1 b-1=""><b-1/></a-1>
から削除したいなら、その一つの方法はこのようになる:

from xml.dom import Node
E = Node.ELEMENT_NODE
del doc[E, None, u'a-1'][E, None, u'b-1']

要素(と属性)の作成

名前空間をもった新しい要素を作成するにはこのようにする:

e = doc.xml_create_element(element_qname, element_ns)

これは次と等価だ:

e = doc.xml_create_element(element_qname, namespace=element_ns)

訳注:amara version 1.2.0.2ではnamespaceキーワード引数を使うとTypeError: xml_create_element() got an unexpected keyword argument 'namespace'と言われる。nsというキーワードならあるようだ。

要素生成時、属性も簡単に追加できる:

import amara
doc = amara.parse('monty.xml')
#Create a third python element
e = doc.xml_create_element(u'python', attributes={u'life': u'brian'})
doc.monty.xml_append(e)
print doc.xml()

これは次のような出力を得る:

<?xml version="1.0" encoding="UTF-8"?>
<monty>
  <python spam="eggs">
    What do you mean "bleh"
  </python>
  <python ministry="abuse">
    But I was looking for argument
  </python>
<python life="brian"/></monty>

新しい要素に属性がどう現れるかが分かる。

辞書、attributes={u'life': u'brian'}は実際には省略形である。正しくは次のように書く:

{
  (<attr1_qname>, <attr1_namespace>): attr1_value},
  (<attr2_qname>, <attr2_namespace>): attr2_value},
  ...
  (<attrN_qname>, <attrN_namespace>): attrN_value},
}

訳注:書き間違いだと思うので次のように訂正しておく:

{
  (<attr1_qname>, <attr1_namespace>): attr1_value,
  (<attr2_qname>, <attr2_namespace>): attr2_value,
  ...
  (<attrN_qname>, <attrN_namespace>): attrN_value,
}

名前空間がNone(即ちその属性が名前空間を持っていないなら)、(<attrN_qname>, <attrN_namespace>)は単に<attrN_qname>と書くことができる。

したがって名前空間で修飾された属性は次のように追加できる:

import amara
NS = u'urn:bogus'
doc = amara.parse('monty.xml')
#Create a third python element
e = doc.xml_create_element(
    u'python',
    attributes={(u'ns:life', NS): u'brian'},
    content=u'unfortunate'
)
doc.monty.xml_append(e)
print doc.xml()

今回ちょっとした「内容」も入れたことに留意のこと。結果はこうなる:

<?xml version="1.0" encoding="UTF-8"?>
<monty>
  <python spam="eggs">
    What do you mean "bleh"
  </python>
  <python ministry="abuse">
    But I was looking for argument
  </python>
<python xmlns:ns="urn:bogus" ns:life="brian">unfortunate</python></monty>

属性の変更、修正

要素を作成した後に属性を作成するには、xml_set_attributeメソッドを使う。先述の例で使用した名前空間で修飾された属性を追加するには、次のようにする:

import amara
NS = u'urn:bogus'
doc = amara.parse('monty.xml')
#Create a third python element
e = doc.xml_create_element(
        u'python',
        content=u'unfortunate'
    )
doc.monty.xml_append(e)
doc.monty.python[2].xml_set_attribute((u'ns:life', NS), u'brian')
print doc.xml()

これは上と同じXMLを生成する。名前空間を必要としないなら、xml_set_attributeの最初の引数は、単にその属性の名前になる。

xml_set_attributeメソッドはその結果生成される属性の名前を返す。

Pythonの文法を使って属性値をセットすることもできる:

doc.monty.python[2].life = u'Pi'

名前空間の情報を含めて、要素の属性について情報は、xml_attributesに保持されている。これは、それぞれの属性のローカル名をkeyに、名前空間で修飾された属性名(訳注:QNameのことらしい)とその名前空間URLのtupleを値にした辞書である。たとえば次のようなコードがあるとしよう:

import amara
NS = u'urn:bogus'
doc = amara.parse('monty.xml')
#Create a third python element
e = doc.xml_create_element(
    u'python',
    content=u'unfortunate'
)
doc.monty.xml_append(e)
#Add a namespace qualified attribute
doc.monty.python[2].xml_set_attribute((u'ns:life', NS), u'brian')
#Add an attribute with no namespace
doc.monty.python[2].xml_set_attribute(u'foo', u'bar')

このときdoc.monty.python[2].xml_attributesは次の値を持つことになる:

{u'life': (u'ns:life', u'urn:bogus'), u'foo': (u'foo', None)}

お遊びだが、XML名前空間の特別な状態を示す興味深いバリエーションがある:

import amara
from xml.doc import XML_NAMESPACE as XML_NS
doc = amara.parse('monty.xml')
#Create a third python element
e = doc.xml_create_element(
    u'python',
    attributes={(u'xml:lang', XML_NS): u'en'},
    content=u'Ni!'
)
doc.monty.xml_append(e)
print doc.xml()

これは次のような出力を得る。

<?xml version="1.0" encoding="UTF-8"?>
<monty>
  <python spam="eggs">
    What do you mean "bleh"
  </python>
  <python ministry="abuse">
    But I was looking for argument
  </python>
<python xml:lang="en">Ni!</python></monty>

標準が許すように、XML名前空間の宣言がない点に留意のこと。

XML断片の追加

Amaraでは、要素のxml_append_fragmentメソッドを通じて、XML文書を書き加える上で強力な「利便性」を提供している。リテラルなXMLの断片文字列(Unicodeではない、なぜならこれはパースの操作なのだから)をこのメソッドに渡すと、それはパースされ、要素に追加される。XML断片はwell-formedな解析される外部実態でなければならない。基本的に複数のルート要素も許されるが、それらは正確に均衡を保ち、特別な文字列はエスケープされる、等々が要求される。文書型宣言は禁止されている。XMLの規則によると、符号化文字列はUTF-8かUTF-16であるとみなされるが、XML宣言(訳注:原文ではXML text declaration)<?xml version="1.0" encoding="ENC"?>を書くか、またはこの関数に符号化についてのパラメタを渡すことで上書き可能だ。

import amara
doc = amara.parse('monty.xml')
doc.monty.xml_append_fragment('<py3 x="1">p</py3><py4 y="2">q</py4>')
print doc.monty.xml_child_elements.keys()

xml_append_fragmentの呼び出しにおいて二つの要素が追加され、出力結果は[u'python', u'py3', u'py4']となる。

オプションの「encoding」はXML断片をパースする際に用いられる符号化された文字列である。このパラメタが明示されると、XML断片のいかなる宣言も上書きされる。

#Latin-1 ordinal 230 is the a-e ligature
doc.monty.xml_append_fragment('<q>P%an</q>'%chr(230), 'latin-1')

完全なdocumentの作成

文書全体をゼロから作ることもできる:

doc = amara.create_document()
doc.xml_append(doc.xml_create_element(u"hello"))
doc.xml()

これは次を生成する:

<?xml version="1.0" encoding="UTF-8"?>
<hello/>

空の文書に特定のルート要素を作成することができる。

doc = amara.create_document(u"hello")
doc.hello.xml_append(doc.xml_create_element(u"world"))
doc.xml()

これは次を生成する:

<?xml version="1.0" encoding="UTF-8"?>
<hello><world/></hello>

これは次と等価である:

doc = amara.create_document()
doc.xml_append(doc.xml_create_element(u"hello"))
doc.hello.xml_append(doc.xml_create_element(u"world"))
doc.xml()

ルート要素に、nsパラメタを用いて名前空間をセットすることが可能である。他にもたくさんの便利なパラメタがある。詳細はimport amara; help(amara.create_document)をPythonプロンプトで試してみよう。

ノード(子孫含めて)をコピーする

XSLTに通じているなら、xsl:copy-ofと同じことをAmaraで行うにはどうすればいいかを疑問に思うかもしれない。このような深いコピーを作成するには、Python標準ライブラリのcopyモジュールを使う。

import copy
import amara
doc = amara.parse('monty.xml')
#Clone the document
doc2 = copy.deepcopy(doc)
#This modification only affects the clone
doc2.monty.python.spam = u"abcd"

#This output will just like what was read in
print doc.xml()
#This output will show the change from "eggs" to "abcd"
print doc2.xml()

PIとコメント

バインダリはかなり自然な方法でXMLの構築をサポートする。ソース文書にPIやコメントがあるなら、PIやコメントを表現するオブジェクトがある:

import amara
DOC = """\
<?xml-stylesheet url="xxx.css"?>
<!--A greeting for all-->
<hello-world/>
"""
doc = amara.parse(DOC)
print doc.xml_children

これは次のリストとなる:

[
<amara.bindery.pi_base instance at 0x433f6c>,
<amara.bindery.comment_base instance at 0x433fac>,
<amara.bindery.hello_world object at 0x433fec>
]

最初のアイテムはバインドされたPIオブジェクトで、二番目はバインドされたコメントである。より深く掘り下げることもできる:

pi = doc.xml_children[0]
comment = doc.xml_children[1]
print repr(pi.target) #shows u'xml-stylesheet'
print repr(pi.data) #shows u'url="xxx.css"'
print repr(comment.data) #shows u'A greeting for all'

これらのオブジェクトを作成することも変更することも可能だ:

doc = amara.create_document()
pi = bindery.pi_base(u"xml-stylesheet", u'url="xxx.css"')
doc.xml_append(pi)
doc.xml_append(doc.xml_create_element(u"A"))

訳注:最後の行は明らかに意図するコードではないはず。実際にコメントノードを生成するには、次のようにする:

from amara import bindery
cmt = bindery.comment_base(u"this is comment.")

xml_appendに渡せば他のノードと同じ用に文書にコメントノードを追加できる。

文書型

文書型宣言のためのAPIもある。

文書型宣言を作成するには、次のようにする:

doc = amara.create_document(
    u"xsa",
    pubid=u"-//LM Grashol//DTD XML Software Autoupdate 1.0//EN//XML",
    sisid=u"http://www.garshol.priv.no/download/xsa/xsa.dtd"
)

結果は文書ツリーにおいて次と等価である:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xsa PUBLIC "-//LM Garshol//DTD XML Software Autoupdate 1.0//EN//XML"
                     "http://www.garshol.priv.no/download/xsa/xsa.dtd">
<xsa/>

これが文書要素を(別途xml_appendを必要とせずに)自動的に作成する点に注意。文書要素の属性や内容を生成するのに、これをさらに詳細に指定することができる:

from xml.dom import XML_NAMESPACE as XML_NS

doc = amara.create_document(
    u"xsa",
    attributes={(u'xml:lang', XML_NS): u'en'},
    content=u'  ',
    pubid=u"-//LM Garshol//DTD XML Software Autoupdate 1.0//EN//XML",
    sysid=u"http://www.garshol.priv.no/download/xsa/xsa.dtd"
)

これは次の結果となる:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xsa PUBLIC "-//LM Garshol//DTD XML Software Autoupdate 1.0//EN//XML"
    "http://www.garshol.priv.no/download/xsa/xsa.dtd">
<xsa xml:lang="en"> </xsa>

すると文書型宣言の詳細にアクセスすることができる:

assert doc.xml_pubid == u"//LM Garshol//DTD XML Software Autoupdate 1.0//EN//XML"
assert doc.xml_sysid == u"http://www.garshol.priv.no/download/xsa/xsa.dtd"
assert doc.xml_doctype_name == u"xsa"

parseの将来のdoctypeサポート

文書型宣言のある文書をパースした時点でルートノードが文書要素のQName、公開識別子、システム識別子を含むというアイデアはまだ動作しないが、これはPython/XMLライブラリの「癖」に起因している。次のバージョンでこれらのライブラリへの依存をなくし、これを改善すべきだ。これが動作するようになったなら次のように書けるのが望ましい:

#DOES NOT YET WORK

XSA = """\
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xsa PUBLIC "-//LM Garshol//DTD XML Software Autoupdate 1.0//EN//XML"
    "http://www.garshol.priv.no/download/xsa/xsa.dtd">
<xsa>
  <vendor>
    <name>Fourthought, Inc.</name>
    <email>info@fourthought.com</email>
    <url>http://fourthought.com</url>
  </vendor>
  <product id="FourSuite">
    <name>4Suite</name>
    <version>1.0a1</version>
    <last-release>20030327</last-release>
    <info-url>http://4suite.org</info-url>
    <changes>
 - Begin the 1.0 release cycle
    </changes>
  </product>
</xsa>
"""
doc = amara.parse(XSA)
assert doc.xml_pubid == u"-//LM Garshol//DTD XML Software Autoupdate 1.0//EN//XML"
assert doc.xml_sysid == u"http://www.garshol.priv.no/download/xsa/xsa.dtd"
assert doc.xml_doctype_name == u"xsa"

内部のDTDサブセットの構造を、バインディングは保持しない。

バインディングのカスタマイズ

バインダリは、XMLノードをイテレートし、ノードタイプ他の詳細によって引き金をひかれた規則の集合を「発射」することによって機能する。デフォルトのバインディングは、それぞれのノードタイプに登録されたデフォルトの規則の結果をバインディングした結果なのだが、ユーザ定義の規則を登録することを許可することによって、簡単に微調整できる。

単純な文字列値としていくつかの要素を扱う

XBELのtitle要素は常にシンプルなテキストであり、それらに対して豊潤なPythonオブジェクトを作成するのは、多くの場合「やり過ぎ」である。それらは要素内容のUnicode値というシンプルなデータメンバでしかないということもあり得る。この最適化をバインダリに行うには、simple_string_element_rule規則のインスタンスを登録する。この規則は、どの要素が簡素化されているかを指し示すXSLTパターン(訳注:XPathパターンのことらしい)のリストを(引数に)取る。

import amara
from amara import binderytools
#Specify (using XSLT patterns) elements to be treated similarly to attributes
rules = [
    binderytools.simple_string_element_rule(u'title')
]
#Execute the binding
doc = amara.parse('xbel.xml', rules=rules)

#title is now simple unicode
print doc.xbel.folder.bookmark.title.__class__ #訳注:<type 'unicode'>

訳注:上の例では、binderytools.simple_string_element_ruleクラスの__init__にただ一つのXPathパターンu'title'が与えられてインスタンス化されているが、これは原文でも前述されているようにリストでもよい:

rules = [
    binderytools.simple_string_element_rule([u'title', u'url'])
]

それより個人的には、amara.parse関数のrulesキーワード引数が取るのは、iterableなオブジェクトで、各アイテムが規則のインスタンスでなければならない点にこそ注意が必要。

特定の要素を完全に無視する

文書の一部分にだけ焦点を当てたい、またメモリを節約し、複雑さを取り除くために、関心の外にある特定の要素をバインディング時には無視したい。そのようなケースではomit_element_ruleを使うことができる。

次の例はフォルダのタイトルには一切バインディングを作成しない(しかしブックマークのタイトルは保持される):

import amara
from amara import binderytools
#Specify (using XSLT patterns) elements to be ignored
rules = [
    binderytools.omit_element_rule(u'folder/title')
]
#Execute the binding
doc = amara.parse('xbel.xml', rules=rules)

#Following would now raise an exception
#print doc.xbel.folder.title

特定のノードタイプを完全に無視する

PI、コメント、テキストノードなども、omit_nodetype_rule規則を使うことで文書から省くことができる。この規則はただ一つのパラメタを取り、それはxml.dom.Node.COMMENT_NODE, xml.dom.Node.PROCESSING_INSTRUCTION_NODE, またはxml.dom.Node.TEXT_NODEという3つのノードタイプのうちの一つである。

次の例には、コメントノードのバインディングが存在しない:

import amara
from amara import binderytools
from xml.dom impot Node

DOCUMENT = """<!-- Printing the best language -->
<language>Python</language>"""

rules = [binderytools.omit_nodetype_rule(Node.COMMENT_NODE)]
doc = amara.parse(DOCUMENT, rules=rules)
print doc.xml()

出力結果はこうなる:

<?xml version="1.0" encoding="UTF-8"?>
<language>Python</language>

空白文字の除去

純粋に空白文字だけから成るノードを取り除いて「children」リストを汚さないようにしたいというのは、共通のニーズだろう。バインダリはこれに応えるためws_strip_element_rule規則を提供する。このパターンにマッチする要素は、空白文字を除去される。

import amara
from amara import binderytools
#Specify (using XSLT patterns) elements to be stripped
#In this case select all top-level elements for stripping
rules = [
    binderytools.ws_strip_element_rule(u'/*')
]
#Excecute the binding
doc = amara.parse('xbel.xml', rules=rules)

空白の文字を除去し、さらに特定要素を無視するといった、複数の規則を組み合わせることもできる。

要素のスケルトンの作成

要素と属性の構造が関心の全てであり、そのテキスト内容はどうでもいいのなら、element_skeleton_ruleが使える。

このパターンにマッチする要素は、全ての文字データを除去される。

import amara
from amara import binderytools
#Specify (using XSLT patterns) elements to be bound as skeletons
#In this case select all elements
rules = [
    binderytools.element_skeleton_rule(u'*')
]
#Execute the binding
doc = amara.parse('xbel.xml', rules=rules)

型の推論

データバインディングの基礎的な考え方は、XMLをネイティブデータ型に変換することである。Amaraは、値がネイティブなPythonデータ型、具体的にはint, float, datetimeの値に推論可能かを知るために各XMLノード検査する規則を提供する。

TYPE_MIX = """\
<?xml version="1.0" encoding="utf-8"?>
<a a1="1">
  <b b1="2.1"/>
  <c c1="2005-01-31">
    <d>5</d>
  <e>2003-01-30T17:48:07.848769Z</e>
  </c>
  <g>good</g>
</a>"""

import amara
from amara import binderytools
rules=[binderytools.type_inference()]
doc = amara.parse(TYPE_MIX, rules=rules)
doc.a.a1 == 1     #type int
doc.a.b.b1 == 2.1 #type float
doc.a.c.c1 == datetime.datetime(2005, 1, 31) #type datetime.

カスタム規則における名前空間の使用

ビルトインのカスタム規則はXPathパターンを使用しており、それらは名前空間を明示するために接頭辞を用いる。バインダーツールに、どのような名前空間バインディングがあるかを教える必要があるだろう。

import amara
from amara import binderytools
#Set up customary namespace bindings for RSS
#These are used in XPath query and XPattern rules
RSS10_NSS = {
    'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
    'dc': 'http://purl.org/dc/elements/1.1/',
    'rss': 'http://purl.org/rss/1.0/',
}
rules = [
    binderytools.simple_string_element_rule(u'title')
]
#Execute the binding
doc = amara.parse('rss10.rdf', prefixes=RSS10_NSS, rules=rules)

しかしながら、先述の議論のように、トップレベル要素において全ての名前空間を宣言しているのなら、それらを明示的に接頭辞の辞書で繰り返す必要はない。

プッシュバインディングと規則

プッシュバインドにも規則を使用することができる。「a」要素から得たいのがそのテキスト内容だけだったとしよう。このとき次のようにすることができる。注意して見てほしい。このサンプル文書は今度はわずかに違っている。

>>> XML="""\
... <doc>
...   <one><a>0</a><b>1</b></one>
...   <two><a>10</a><b>11</b></two>
... </doc>
... """
>>> import amara
>>> from amara import binderytools
>>> #This rule says "treat all elements at the third level of depth as simple strings"
>>> rule = binderytools.simple_string_element_rule(u'/*/*/*')
>>> #Push back bindings of all second level elements ('one' and 'two')
>>> chunks = amara.pushbind(XML, u'/*/*', rules=[rule])
>>> elem = chunks.next()
>>> print elem.a.__class__
<type 'unicode'>
>>> print elem.a
u'0'
>>> print elem.b.__class__
<type 'unicode'>
>>> print elem.b
u'1'

カスタムバインディングのクラスをを使用する

もっと洗練された微調整を必要とするなら、ユーザが独自にカスタマイズしたバインディングクラスを登録したいと思うのではないだろうか。次の例はbookmark要素にあるメソッドを与える。retrieveである。これはウェブページの本文を取得する:

import urllib
from xml.dom import Node
import amara
from amara import bindery

#Subclass from the default binding class
#We're adding a specialized method for accessing a bookmark on the net 

class specialized_bookmark(bindery.element_base):
    def retrieve(self):
        try:
            stream = urllib.urlopen(self.href)
            content = stream.read()
            stream.close()
            return content
        except IOError
            import sys; sys.stderr.write("Unable to access %s\n"%self.href)

doc = amara.parse(XML, binding_classes={(None, u'bookmark'): specialized_bookmark})

#Show specialized instance
print doc.xbel.folder.bookmark.__class__

#Exercise the custom method
print "Content of first bookmark:"
print doc.xbel.folder.bookmark.retrieve()

次の行を見てみよう:

doc = amara.parse(XML, binding_classes={(None, u'bookmark'): specialized_bookmark})

ある要素型のバインドにおいて使用するクラスを登録するときは、その要素の名前空間URIとローカル名を明示する。XBELの例のように名前空間に属していないことを知っているなら、Noneを使う。NoneはほとんどのPython/XMLツールにおいて「名前空間に属していない」ことを示す正しいシグナルであり、空文字列""ではない。

accesstrigger.pyのデモも参照のこと。

カスタマイズされたバインディングについて一般的な注意点

バインダリはバインディングから書き戻したXMLの辻褄が合うように、そしてXPathは期待された結果を返すように何とかしようとするのだが、バインディングをカスタマイズするとヘンテコな結果を引き起こすことになりやすい。

一つの例として、simple_string_element_ruleを使用しxmlメソッドで再シリアライズするなら、その単純化された要素は子要素ではなく、属性として書き出されてしまうだろう。もしバインディングをカスタマイズした結果そのような不本意な結果に遭遇してしまったら、通常の処方箋はカスタマイズしたxmlメソッドを書くか、それに特化したXPathのラッパーを書くかである(XPathラッピングについてはbinderyxpath.xpath_wrapper_mixinを参照)。

バインダリの拡張ガイド

バインダリは拡張性を考慮してデザインされている。しかしXMLの表現の巨大なフレキシビリティであるとか、開発者たちが多くの異なる方法でPythonオブジェクトを生成したいであろうことを考えると(逆もまた同様)、これがただ一つのやり方とはいえない。バインダリの拡張を書くことにより、どのようなニーズにでも応えられるのだが、管理しやすくするための幾つかの基礎的なルールはある。

バインダリの法則

  1. XML文書に対応するバインディングオブジェクトは、他のすべてのオブジェクトがナビゲーション的な属性を通じてアクセス可能な、単一のルートオブジェクトを持つ(no, fancy method calls don't count)

[TODO: more on this section to come. If you try tweaking bindery extensions and have some useful notes, please pitch in by sending them along.]

Scimitar: 最も柔軟なプログラミング言語のための最も柔軟なスキーマ言語

ScimitarはISO Schematronの実装で、SchematronスキーマをPythonのvalidator scriptにコンパイルする。

ScimitarはISO Schematronのspec草案を全てサポートしている。Schimitar convenienceにおける既知のギャップについてはTODOファイルを参照。

Scimitarを利用する典型的な二つの局面を紹介しよう。Schematronスキーマ「shcema1.stron」があり、それに対して複数のXMLファイル「instance1.xml, instance2.xml, instance3.xml」をvalidateしたいとしよう。

最初に、Scimitarのコンパイラスクリプト、scimitar.pyを通じてschema1.stronを実行する。

scimitar.py schema1.stron

schema1.py(同名が.py拡張子で置き換えられる)というファイルが現在の作業ディレクトリに生成される。異なるロケーション、異なるファイル名を使いたいときは、"-o"オプションを使う。生成されたファイルはPythonで書かれたvalidatorスクリプトであり、schema1.stronで定義されたSchematron規則をチェックする。

これで、検証したいXMLファイルに対して、生成されたvalidatorスクリプトを実行することができるようになった:

python schema1.py intance1.xml

検証結果のレポートは、デフォルトでは標準出力に生成される。さもなくば"-o"オプションを使ってファイルにリダイレクトすることもできる。

検証結果レポートは解析済みXML外部実体である。言い換えると、well-formedなXML文書によく似たファイルであるが、埋め込まれたタグを許可するために、いくつかの制限が解かれている。

Schematron 1.5 勧告から持ってきた例を使って実際に検証してみる:

$ cat simple1.stron
<?xml version="1.0" encoding="UTF-8"?>
<sch:schema xmlns:sch="http://www.ascc.net/xml/schematron" version="ISO">
 <sch:title>Schematron Schema例</sch:title>
 <sch:pattern>
   <sch:rule context="dog">
    <sch:assert test="count(ear) = 2"
    >'dog' 要素は二つの'ear'要素を内容に持つべきです。</sch:assert>
    <sch:report test="bone"
    >この dog は bone を持っています。</sch:report>
   </sch:rule>
  </sch:pattern>
</sch:schema>

$ scimitar.py simple1.stron
$ ls simple*.py
simple1-stron.py
$ cat instance2.xml
<dog><ear/></dog>

$ python simple1-stron.py instance2.xml
<?xml version="1.0" encoding="UTF-8"?>
Processing schema: Example Schematron Schema

Processing pattern: [unnamed]

Assertion failure:
'dog' 要素は二つの'ear'要素を内容に持つべきです。

AmaraのDOMツール: DOMにPython風のインタフェイスを

DOMはJavaの世界からやってきたものであり、Python風のAPIからは程遠い(これまで述べてきたバインダリの手順を参照)。いくつかのDOM風の実装、たとえば4SuiteのDomletteにも、幾分Python風のイディオムが紛れ込んでいる。AmaraのDOMツールはさらにそれを推し進めたものだ。

pushdom

xml.dom.pulldomに通じている人もいるだろう。これはSAXとDOMの素晴らしいハイブリッドを提供し、SAX風のやり方で文書の重要な部分を効率的に断片化する。そしてきめ細かい操作のためにDOMを使うのである。Amaraのpushdomは、このプロセスを幾分便利にしている。これにXPatternの集合を渡すと、そのパターンにしたがったDOMの断片を次々生成するジェネレータを提供してくれる。

これにより、非常に小さなメモリの使用で巨大なファイルをプロセスすることが可能になるにもかかわらず、DOMの利便性のほとんどを利用できる。

for docfrag in domtools.pushdom('demo/labels.xml', u'/labels/label'):
    label = docfrag.firstChild
    name = label.xpath('string(name)')
    city = label.xpath('string(address/city)')
    if name.lower().find('eliot') != -1:
        print city.encode('utf-8')

これは "Stamford"をプリントする。

このXML-DEV メッセージも参照のこと。

ジェネレータツール

ジェネレータツールの詳細に関しては、"Generating DOM Magic"という記事を参照のこと。

与えられたノードのXPathを得る

domtools.abs_pathで、ノードの絶対パスを得ることができる。次のコード:

from amara import domtools
from Ft.Xml.Domlette import NonvalidatingReader
from Ft.Lib import Uri
file_uri = Uri.OsPathToUri('labels.xml', attemptAbsolute=1)
doc = NonvalidatingReader.parseUri(file_uri)

print domtools.abs_path(doc)
print domtools.abs_path(doc.documentElement)
for node in doc.documentElement.childNodes:
    print domtools.abs_path(node)

これは次を表示する:

/
/labels[1]
/labels[1]/text()[1]
/labels[1]/label[1]
/labels[1]/text()[2]
/labels[1]/label[2]
/labels[1]/text()[3]
/labels[1]/label[3]
/labels[1]/text()[4]
/labels[1]/label[4]
/labels[1]/text()[5]

abs_pathに関する詳細は、"Location, Location, Location"という記事を参照のこと。

AmaraのSAXツール: 脳みそが爆発しないSAX

Tenorsax(amara.saxtools.tenorsax)はSAXのロジックを「リニアライズする」ためのフレームワークであり、多少自然な流れを作りとともに必要とされる技術的なステートメントが少なくて済む。

これを文書化する時間がないので、例として、test/saxtools/xhtmlsummary.py を参照のこと。

また このXML-DEV messageも参照のこと。

Flextyper: XML処理のためのユーザ定義のPythonデータ型

Flextyperは、Jeni TennisonのData Type Library Language(DTLL: ISO Document Schema Definition Languages(DSDL)のpart 5になる方向である)の実装である。Flextyperは4SuiteのRELAX NGライブラリで使えるデータタイプを含んだPythonモジュールを生成する。

FlextyperはDTLLファイルを、含まれたデータタイプを実装するPythonモジュールにコンパイルする。次のように実行する:

flextyper.py dtll.xml

DTLLで定義されたデータタイプ名前空間一つ一つについてファイルがあり、そのセットが作成される。デフォルトでは出力ファイル名は入力に基づいて決められる。すなわち、dtll-datatypes1.py, dtll-datatypes2.py等々である。

これで4SuiteのRELAX NG実装のプロセッサインスタンスでこれらのデータタイプモジュールを登録できるようになる。

webmaster@jintrick.net
Published: 2009-02-27, Updated: 2009-03-16