Python2.4.2でPHP5のSimpleXMLを真似
@追記
このエントリで書いてあるのはPythonのクラスにアトリビュートやインデックス付きでアクセスする際のサンプルですので,実用には向きません.PythonでXMLを操作したい場合はlxmlやElementTreeといったモジュールを使うことをお勧めします.PHPのSimpleXMLとまったく同じではないにせよ,オブジェクトとしてアクセスすることもできます.
サンプルはこちら:id:tomoemon:20071019
@ここまで
python+pygameで画像の描画に関するデータをXMLファイルにまとめて、ゲームの実行時にうまく連携できる仕組みを考えているのですが、getAttributeとかいちいち書いているのが面倒になってきました。これまでもXMLに関する関数はある程度まとめていたのですが、やはりPHP5に導入されたSimpleXMLには到底かないません。
ちなみにSimpleXMLについてご存じない方は下のリンクとかを参照してみると良いかも
オープンソース - [PHPウォッチ]第3回 PHP5でXMLサポートが大幅強化:ITpro
PHP: 一覧 - SimpleXML 関数
PHPのDOMインターフェースを使ったやり方というのがXMLを扱うための標準的な仕組みで、Pythonの現行のバージョンもほぼ同じやり方になっています。見てわかる通りDOMは目的のタグ名からデータを取ってくるのがなかなか面倒です。新しくデータを追加するのは結構楽なんですけどね。SimpleXMLはこうした弱点を埋めて楽にデータを取り出せるようにしたモデルです。
というわけでPythonでSimpleXMLの真似をしてみよう
と思ってやってみたらさっくり作れちゃうところがさすがPythonです。
今はXMLファイルからSimpleXMLを真似したオブジェクトを返すだけですが、逆方向の変換(オブジェクトからDOM)もできるようにしたいと思います。あと、XML→DOMに展開→SimpleXMLに展開といろいろ経ているので大きなXMLファイルにはきっと向きません。速度的にはSAXを使ってSimpleXML化したほうが良さそうです。
試しに使ってみるデータを示します。サンプルにありがちな本のリストです。
<?xml version="1.0" encoding="utf-8"?> <books> <book asin="4873112109"> <title>初めてのPython</title> <authors> <name>ルッツ・マーク</name> <name>アスカー・デイビッド</name> <name>夏目大</name> </authors> <price>5040</price> </book> <book asin="4894714019"> <title>Pythonで学ぶプログラム作法</title> <authors> <name>アラン・ゴールド</name> <name>松葉素子</name> </authors> <price>3150</price> </book> </books>
使い方がこんな感じです。
def main(): root = simplexml_load_file("simpletest.xml") # 1冊目の詳細を表示 print "タイトル:", root.book[0].title[0] print "ASIN:", root.book[0]["asin"] for name in root.book[0].authors[0].name: print "著者:", name print "金額:", root.book[0].price[0] print "" # すべての本のタイトルと金額を表示 for i,book in enumerate(root.book): print (i+1), "冊目" print "タイトル:", book.title[0] print "金額:", book.price[0], "円" print ""
SimpleXMLを使ったことがある人ならPHP版とは少し違うことがわかると思います。PHPのSimpleXMLでは、あるノードが持っているテキストを以下のような式で得ることができます。
$title = (string)$root->book[0]->title
しかし、今回作ったものではtitle[0]とインデックスを書くことを強制しています。というのも、PHPのようにやった場合にtitleノードが1つだけならば良いのですが、titleノードが複数あったらどうすればいいんだという疑問があるからです。「すべての本のタイトルと金額を表示」のところでやっているとおり、インデックスを付けないでノード名を指定すると、そのノードのリストを返します。これはPHP版でも同様ですので以下のような式が使えます。しかし、PHPではさらに、これに(string)を付けると文字列にしてくれるわけです。
//PHP版 foreach($root->book as $book){ print $book->title }
#Python版 for book in $root->book: print book->title[0]
つまり、$root->bookというのもただの配列ではなくて、配列を真似したオブジェクトになっているんですね。これをPythonでやろうとすると同様にシーケンス型とマップ型を両方エミュレートしないといけないので、ぶっちゃけ面倒ですしあまりメリットを感じないのでとりあえずこのままで行こうと思います。(以上、言い訳でした)
そして、出力がこんな感じになります。
タイトル: 初めてのPython ASIN: 4873112109 著者: ルッツ・マーク 著者: アスカー・デイビッド 著者: 夏目大 金額: 5040 1 札目 タイトル: 初めてのPython 金額: 5040 円 2 札目 タイトル: Pythonで学ぶプログラム作法 金額: 3150 円
ソースは長いので(といっても100行程度)続きを読むで。
Python版SipmleXMLの作り方
from xml.dom.minidom import parse from xml.dom.minidom import getDOMImplementation class SimpleObject: """ XML上の各ノードを表現するオブジェクト 子ノードはa.titleという形でアクセスできるように setattr()を使いインスタンスの属性として追加していく """ def __init__(self): self._data = {} def __getitem__(self,key): return self._data[key] def __setitem__(self,key,value): self._data[key] = value def __delitem__(self,key): del self._data[key] def __setattr__(self, name, value): self.__dict__[name] = value def __getattr__(self, name): if name in self.__dict__: return self.__dict__[name] def __delattr__(self, name): if name in self.__dict__[name]: del self.name def __str__(self): return self._text def xpath(self,path): pass def attributes(self): return self._data.iteritems() def child_nodes(self): return self.__dict__.keys() def simplexml_load_file(xmlfile): dom = parse(xmlfile) root = dom.documentElement removeWhiteSpace(dom, root) return simple(root) def removeWhiteSpace(dom, parentnode): """ テキストノードから空白文字を全て取り除く """ child_array = parentnode.childNodes if len(child_array) == 0: return isOnlyText = True # 子ノードがテキストノードだけか混合ノードかをチェック for child in child_array: if child.nodeType != child.TEXT_NODE: isOnlyText = False removeWhiteSpace(dom,child) # テキストノードだけなら空白を取り除いたテキストノードを作成 if isOnlyText == True: text = getText(parentnode) textnode = dom.createTextNode(text) for child in child_array: parentnode.removeChild(child) parentnode.appendChild(textnode) # 混合ノードならテキストノードから空白を取り除き、空白だけのノードは削除 else: for child in child_array: if child.nodeType == child.TEXT_NODE: text = child.data.strip() if text != '': textnode = dom.createTextNode(text) parentnode.replaceChild(textnode,child) else: parentnode.removeChild(child) def getText(parentnode): """ parentnodeが持つテキストを返す """ child_array = parentnode.childNodes text = [] # 全ての子ノードに対して for child in child_array: # テキストノードなら値を取得 if child.nodeType == child.TEXT_NODE: text.append(child.data) return "".join(text).strip() def simple(parentnode): """ parentnode以下のすべてのノードについてSimpleObjectを生成して返す """ data = SimpleObject() # 属性値をSimpleObjectの辞書に追加する if parentnode.attributes != None: for i in range(len(parentnode.attributes)): attrname = parentnode.attributes.item(i).name attrvalue= parentnode.getAttribute(attrname) data[attrname] = attrvalue text = getText(parentnode) if text != "": setattr(data,"_text",text) # 子ノードはノード名をSimpleObjectのインスタンスの要素名として追加する child_nodes = parentnode.childNodes childlist = [] for child in child_nodes: # #TEXT や #COMMENTなどのノードは無視する if child.nodeName.startswith("#"): continue if child.nodeName not in childlist: setattr(data,child.nodeName,[]) childlist.append(child.nodeName) newchild = getattr(data,child.nodeName) newchild.append(simple(child)) return data