ElementTree の思い出

昔、訳あってElementTreeのソースコードを電車の中で読んでいたら、となりの人に、これは何かと尋ねられたことを思い出した。その後、プログラムとは何か、ソースとは何かを説明することになるのだが、まぁ、どうでもいいか。

要素の後に来るテキスト

<p>hello<span>world</span>thanks</p>

span要素の後にあるテキストthanksをどうやって取り出すかというと、span要素の tail で取得する*1

# python 2.5
import xml.etree.ElementTree as ET
root = ET.fromstring('<p>hello<span>world</span>thanks</p>')
print root.text, root[0].text, root[0].tail

# -- 結果
# hello world thanks

名前空間

名前空間に属する要素は、{URI}要素名 で表す*2

import xml.etree.ElementTree as ET

xml = r"""
<body xmlns:foo="http://d.hatena.ne.jp/itasuke/">
  <foo:p>hello</foo:p>
  <foo:p>world</foo:p>
  <p>thanks</p>
</body>
"""

root = ET.fromstring(xml)
for i in root:
    print i.tag

# -- 結果
# {http://d.hatena.ne.jp/itasuke/}p
# {http://d.hatena.ne.jp/itasuke/}p
# p

この表記はは、James Clarkという大御所が説明用に使い始めたやり方らしい*3

ちなみに、読み込んだXML文章を再び書き出すと、名前空間の接頭辞は連番に置き換えられてしまう。下のコードでは foo:p だったのが ns0:p になってしまっている。

import sys
import xml.etree.ElementTree as ET

xml = r"""
<body xmlns:foo="http://d.hatena.ne.jp/itasuke/">
  <foo:p>hello</foo:p>
  <foo:p>world</foo:p>
  <p>thanks</p>
</body>
"""

root = ET.fromstring(xml)
tree = ET.ElementTree(root)
tree.write(sys.stdout)

# -- 結果
# <body>
#   <ns0:p xmlns:ns0="http://d.hatena.ne.jp/itasuke/">hello</ns0:p>
#   <ns0:p xmlns:ns0="http://d.hatena.ne.jp/itasuke/">world</ns0:p>
#   <p>thanks</p>
# </body>

細かい部分まで制御するにはElementTreeではなくminidomを使ったほうがいい。

XPath

XPathとは何か? 誤解を恐れずに言うと「XMLから要素や属性、テキストなどを取り出す正規表現のような物」と思えばよい。現在のElementTreeではXPathのうち、基本的な構文のみをサポートしている*4

差し当たりは

  • 下位要素 /
  • 全ての下位要素 //
  • 全ての子要素 *

の3つを使えるようにするといい。

/ を使うと、ディレクトリのように要素を指定できる:

from StringIO import StringIO
import xml.etree.ElementTree as ET

xml = r"""
<html>
  <head>
    <title>foo</title>
  </head>
  <body>
    <div>
      <p>hello</p>
      <p>world</p>
    </div>
    <div>
      <p>thanks</p>
    </div>
    <address>
      <p>who</p>
      <div>you</div>
    </address>
  </body>
</html>
"""
tree = ET.parse(StringIO(xml))
print tree.find('/head/title').text
print tree.find('/body/address/div').text
print tree.find('/body/div/p').text
# -- 結果
# foo
# you
# hello

findall/ を組み合わせる:

for i in tree.findall('/body/div/p'):
    print i.text
# -- 結果
# hello
# world
# thanks

// を使って全てのp要素を列挙:

for i in tree.findall('//p'):
    print i.text
# -- 結果
# hello
# world
# thanks
# who

ある要素の下にあるp要素を列挙:

root = tree.getroot()
div1 = root[1][0]
for i in div1.findall('.//p'):
    print i.text
# -- 結果
# hello
# world

* を使って、ある要素の下にある全ての要素を列挙:

for i in tree.findall('/body/address/*'):
    print i.text
# -- 結果
# who
# you

親要素(非推奨)

ある要素の親要素にアクセスする方法はない

無理やり実現する方法もなくはない。

Python 2.5に入ってるソースコードを開き、_ElementInterfaceクラスのappendメソッドの最後にでも element.parent = self と加えればできると思う。どうしてこうなっていないのかは分からない。たぶん循環参照になってガベージコレクションがやりにくくなってしまうからだろうか?

ソースコードを書き換えず、継承だけでやる方法もある。先頭に _ が付いたプライベート・クラスを継承し、通常自動で生成されるものを全て手動で生成する。

from StringIO import StringIO
import xml.etree.ElementTree as ET

xml = r"""
<body>
  <p>hello</p>
  <p>world</p>
  <ul><li>thanks</li></ul>
</body>
"""

# _ から始まるオブジェクトを使うのはマナー違反
class MyElementFactory(ET._ElementInterface):
    def append(self, element):
        ET._ElementInterface.append(self, element)
        element.parent = self

# 自作のMyElementFactoryを使って木構造を生成させる
tree = ET.parse(StringIO(xml), parser=ET.XMLTreeBuilder(target=ET.TreeBuilder(element_factory=MyElementFactory)))
root = tree.getroot()

print root is root[0].parent  # True
print root is root[1].parent  # True
print root is root[2][0].parent.parent  # True

cElementTree

ここまでずっと xml.etree.ElementTree を使ってきたが、通常はC言語で実装された xml.etree.cElementTree を使った方がいい。

import xml.etree.ElementTree as ET   # Python実装
import xml.etree.cElementTree as ET  # C実装

run your programs without any problems

http://effbot.org/zone/celementtree.htm

(置き換えても)問題なくプログラムは走る

とのこと。

Shift_JISとかEUC-JPとか

XMLで標準じゃない文字コードで書かれたファイル(sjis.xml)を読ませるにはどうしたらいいか?

<?xml version="1.0" encoding="shift_jis"?>
<body>
  <p>日本語の表示</p>
</body>

http://mail.python.org/pipermail/python-list/2006-March/372707.htmlを参照

cEleementTreeXMLParserXMLTreeBuilderと等価)にあるencodingを使うか*5

import codecs
import xml.etree.cElementTree as ET

# http://effbot.org/zone/celementtree-encoding.htm
# より文法ミスを修正して拝借
def myparser(file, encoding):
    f = codecs.open(file, "r", encoding)
    p = ET.XMLParser(encoding="utf-8")
    while 1:
        s = f.read(65536)
        if not s:
            break
        p.feed(s.encode("utf-8"))
    return ET.ElementTree(p.close())

tree = myparser('sjis.xml', 'shift_jis')
root = tree.getroot()
print root[0].text

# -- 結果
# 日本語の表示

もしくは、XML宣言を除去してしてから、ElementTreeに渡せということらしい。

import codecs
import xml.etree.ElementTree as ET

def read_except_declaration(f):
    # XML宣言の前に空白文字はない
    # http://www.atmarkit.co.jp/aig/01xml/declaration.html
    line = f.readline()
    end = line.find('?>') 
    if end > 0:
        f.seek(end + 2)
    return f.read()

f = codecs.open('sjis.xml', 'r', 'shift_jis')
xml = read_except_declaration(f).encode('utf-8')
root = ET.fromstring(xml)
print root[0].text

# -- 結果
# 日本語の表示

実践

はてなダイアリーのRSSからタイトルとURLを列挙してみる。

RSSXMLのサブセットなのでElementTreeで処理できるが、名前空間の指定を忘れないように。

はてなRSSを見ると、ルート要素の属性に

<rdf:RDF
	xmlns="http://purl.org/rss/1.0/"

とあるので、これ以降、接頭辞の付いていない要素は全部 http://purl.org/rss/1.0/ 空間に存在する。

全てのitem要素を抽出し、その中にあるtitleとlink要素のテキストをprintするという方向で行こう:

import urllib
import xml.etree.ElementTree as ET

tree = ET.parse(urllib.urlopen('http://b.hatena.ne.jp/hotentry/diary/rss'))
for i in tree.findall('/{http://purl.org/rss/1.0/}item'):
    print i.find('{http://purl.org/rss/1.0/}title').text
    print i.find('{http://purl.org/rss/1.0/}link').text
    print

極めて簡単。