Python の xml.dom.minidom で GetElementById をするメモ

Pythonxml.dom.minidom モジュールを使って XML ファイルを操作する時に、getElementById がうまくいかなかったのでその対処方法についてメモっておく。

●お題編

次のような XML でサイトのリストを作ってみる。Google サイトのタグに id属性をつけている。

data.xml:

<?xml version="1.0"?>
<sites>
<site>
<name>Yahoo!</name>
<url>http://www.yahoo.co.jp/</url>
</site>
<site id="my_favorite_site">
<name>Google</name>
<url>http://www.google.co.jp/</url>
</site>
<site>
...以下省略...
</sites>

これをこんな python スクリプトで id属性="my_favorite_site" の要素配下を表示してみる。

pick_up_node.py:

# -*- encoding: utf-8 -*-

"""
特定のid属性をもつ要素配下を表示するサンプル
(Python2)
"""

import xml.dom.minidom, sys
from textwrap  import dedent

# 特定のXMLファイルを読み込み、特定のid属性をもつ要素配下を表示する関数
# 引数:id:id属性の値
# 引数:xml_file:対象となるXMLファイル
def pick_up_node(id, xml_file):
  # XMLファイルをパースして DOM を取得
  dom = xml.dom.minidom.parse(xml_file)

  # 特定のid属性のノードを検索
  node = dom.getElementById(id)

  # 該当するノードがないとき
  if not node:
    print "Not found!"
    return

  # 該当するノードがあるときはXMLの形式で表示
  print node.toxml()

def usage():
  print >> sys.stderr, dedent("""\
    Usage:
    python %s id xml_file
  """ % sys.argv[0])

def main(argv):
  if len(argv) < 3:
    usage()
    quit()
  id, xml_file = argv[1:]
  pick_up_node(id, xml_file)

if __name__ == '__main__':
  main(sys.argv)

ところが、実行するとうまくいかない。。。

C:\work>python pick_up_node.py my_favorite_site data.xml
Not found!

HTML+JavaScript で DOM の処理をする時のように、ちょろっとスクリプトを書いただけではだめみたいだ。

●解答編(その1)

ソースファイル (minidom.py) を眺めてみたところ、適切に ID属性型であることが DTD で宣言されていないとうまく検索してくれないようだ。

冷静に考えれば、HTML+JavaScript で getElementById できるのは、HTML(XHTML?)のスキーマ定義ですべてのタグに ID属性型を持つよう前もって定義されているからなのだと予想される。

従って、上のスクリプトを生かしつつ、XML の流儀にしたがって記述を適切に修正し、DTD を追加してやればよい。

data2.xml:

<?xml version="1.0" ?>

<!DOCTYPE sites [
<!ELEMENT sites (site+)>
<!ELEMENT site (name,url)>
<!ATTLIST site id ID #IMPLIED>
<!ELEMENT name (#PCDATA)>
<!ELEMENT url (#PCDATA)>
]>

<sites>
<site>
<name>Yahoo!</name>
<url>http://www.yahoo.co.jp/</url>
</site>
<site id="my_favorite_site">
<name>Google</name>
<url>http://www.google.co.jp/</url>
</site>
...以下省略...
</sites>

上の、

<!ATTLIST site id ID #IMPLIED>

の行で ID属性型を宣言している。これで期待通りの結果となる。

C:\work>python pick_up_node.py my_favorite_site data2.xml
<site id="my_favorite_site">
<name>Google</name>
<url>http://www.google.co.jp/</url>
</site>

しかし。

XML の厳密な世界ではこれでいいのでしょうが、DTD を意識しなければならないのはうっとうしすぎる。。。

もっと簡単に済ましたいですよね。。。

●解答編2

もう、自前で getElementById を実装してしまいましょう。

自分で責任を持つから、自由にさせて下さい!!!

以下、修正版 pick_up_node2.py の差分です:

from xml.dom.minidom import (Node, Document)

def _my_getElementById(self, id):
  # 子ノードを再帰的にたどっていくサブ関数
  def _get_element_by_id_helper(parent, id):
    for node in parent.childNodes:
      if node.nodeType == Node.ELEMENT_NODE and \
        node.getAttribute("id") == id:
        return node
      r = _get_element_by_id_helper(node, id)
      if r:
        return r
    return None
  return _get_element_by_id_helper(self.documentElement, id)

# Document クラスにインスタンスメソッド getElementById2 を追加
Document.getElementById2 = _my_getElementById

def pick_up_node(id, xml_file):
  # XMLファイルをパースして DOM を取得
  dom = xml.dom.minidom.parse(xml_file)

  # 特定のid属性のノードを検索
  #node = dom.getElementById(id)
  node = dom.getElementById2(id) # ← 追加したメソッドを実行

  # 該当するノードがないとき
  if not node:
    print "Not found!"
    return

  # 該当するノードがあるときはXMLの形式で表示
  print node.toxml()

これだと DTD を追加する前の XML でも期待通りの動作をします。

C:\work>python pick_up_node2.py my_favorite_site data.xml
<site id="my_favorite_site">
<name>Google</name>
<url>http://www.google.co.jp/</url>
</site>

最近 JSON ばかり使っていたので、いまごろ XML の制約を眼にしました、という次第であります。