Python2.4.2でPHP5のSimpleXMLを真似

@追記
このエントリで書いてあるのはPythonのクラスにアトリビュートやインデックス付きでアクセスする際のサンプルですので,実用には向きません.PythonXMLを操作したい場合は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