XMLツリーのタグへの参照を属性アクセスで実現する

PythonXMLを読んで辞書オブジェクトの木に変換するプログラムを書いたけどなんかいけてないので、タグへのアクセスを属性アクセスで実現する方法を考えてみました。要は

<root>
    <children>
        <child>A</child>
        <child>B</child>
        <child>C</child>
        <child>D</child>
    </children>
</root>

こういうXMLを読んで、Pythonから

root['children']['child'][0]

こうアクセスできるようにしたけど、さらに手を加えて

root.children.child[0]

こう書きたい。

Pythonでは、__getattr____setattr__をオーバーライドすることで、オブジェクトに対する属性アクセスの挙動を変更できます。

3.4.2 属性値アクセスをカスタマイズする
参考:Pythonのクラスシステム

class A(object):
    def __init__(self):
        self.lst = ['alice', 'bob', 'carol']
    def __getattr__(self, name):
        if name[:2] == 'at':
            return self.lst[int(name[2:])]
        return self.__dict__[name]
    def __setattr__(self, name, value):
        if name[:2] == 'at':
            self.lst[int(name[2:])] = value
        self.__dict__[name] = value

a = A()
print a.lst # => ['alice', 'bob', 'carol']
print a.at0 # => 'alice'
print a.at1 # => 'bob'

a.at1 = 'bump'
print a.at1 # => 'bump'

これを利用して、属性にアクセスされた時に子ノードを探索し対応するオブジェクトを返して、タグ名による属性アクセスを可能にします。そのようなクラスをNodeクラスとして、タグ参照の結果として戻る値を常にNodeクラスまたはプリミティブ型とすれば、a.b.cのようにどんどん木を下に下りていくこともできるはずです。

以下のプログラムソースをlibxmlload.pyというファイル名で保存します。

# coding: utf-8
import urllib
from xml.etree.ElementTree import *

class Node(object):
    def __init__(self, name, info):
        self.__dict__['_info'] = info
        self.__dict__['_name'] = name
    def __getattr__(self, name):
        if name in self.__dict__['_info']:
            return self.__dict__['_info'][name]
        return self.__dict__[name]
    def __setattr__(self, name, value):
        self.__dict__['_info'][name] = value;
    def __iter__(self):
        return self._info.iteritems()
    def __repr__(self):
        args = self._name, ','.join(self._info.keys())
        return "<%s keys=%s>" % args

def xmltrans(xmlnode):
    node = Node(xmlnode.tag, {})
    for child in xmlnode:
        key = child.tag
        if len(child) == 0:
            val = child.text
            if val is not None:
                val = val.strip()
            assign(node._info, key, val)
        else:
            assign(node._info, key, xmltrans(child))
    return node

def assign(info, key, value):
    if key in info:
        if isinstance(info[key], list):
            info[key].append(value)
        else:
            info[key] = [info[key], value]
    else:
        info[key] = value

def urlopen(url):
    # http access
    conn = urllib.urlopen(url)
    xmlstr = conn.read()
    # create xml
    xmldoc = fromstring(xmlstr)
    # xml to object tree
    otree = xmltrans(xmldoc)
    return otree

def parse(filepath):
    fobj = open(filepath)
    xmlstr = fobj.read()
    xmldoc = fromstring(xmlstr)
    otree = xmltrans(xmldoc)
    return otree

XMLファイルを読み込んでみます。
まず、下のXMLtest.xmlとして保存します。

<root>
    <state>0</state>
    <timestamp>1234567890</timestamp>
    <userlist>
        <user>
            <name>A</name>
            <lang>Japanese</lang>
        </user>
        <user>
            <name>B</name>
            <lang>Swedish</lang>
        </user>
        <user>
            <name>C</name>
            <lang>English</lang>
        </user>
        <user>
            <name>D</name>
            <lang>French</lang>
        </user>
        <user>
            <name>E</name>
            <lang>Chinese</lang>
        </user>
    </userlist>
</root>

次にインタプリタからXMLファイルを読み込みます。

$ python
>>> import libxmlload
>>> root = libxmlload.parse('test.xml')
>>> root.timestamp
'1234567890'
>>> root.state
'0'
>>> root.userlist
<userlist keys=user>
>>> root.userlist.user
[<user keys=lang,name>, <user keys=lang,name>, <user keys=lang,name>, <user keys=lang,name>, <user keys=lang,name>]
>>> root.userlist.user[0].name
'A'
>>> map(lambda usr:usr.name, root.userlist.user)
['A', 'B', 'C', 'D', 'E']
>>> map(lambda usr:usr.lang, root.userlist.user)
['Japanese', 'Swedish', 'English', 'French', 'Chinese']
>>> 

XMLノードへのアクセスを属性アクセスで実現できているのがわかります。

このプログラムではこのほか、ネットワーク上からファイルを持ってくる関数urlopenを定義しました。

最後に、これを使ってTwitterAPIのpublic_timelineからつぶやきを取得してみます。

$ python
>>> import libxmlload
>>> statuses = libxmlload.urlopen('http://api.twitter.com/1/statuses/public_timeline.xml')
>>> statuses
<statuses keys=status>
>>> len(statuses.status)
20
>>> statuses.status[0]
<status keys=favorited,contributors,truncated,text,created_at,retweeted,coordinates,source,in_reply_to_status_id,in_reply_to_screen_name,in_reply_to_user_id,place,retweet_count,geo,id,user>
>>> for s in statuses.status:
...     print s.user.screen_name
... 
pcgirl65
Xornvestite
windsurfingnews
co2levels
_SOCIAIS_
Bedford76021
Pamposa04
Rachhh_xo
kiyoohara
iyodenden
m0n3y_bag5
VisaVis_theater
cordies_
AARTYinc
babygr33ney3s
mai_geek
manoelagnoronha
SaSSy_AsH
SuperRenatoo
_thaisrawr
>>>