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
名前空間
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を参照。
cEleementTreeのXMLParser(XMLTreeBuilderと等価)にある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を列挙してみる。
RSSはXMLのサブセットなのでElementTreeで処理できるが、名前空間の指定を忘れないように。
<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
極めて簡単。