2006年10月12日

    最近一直在用python处理xml文件,查阅了不少资料,看到一篇挺有用的文章,所以转过来与大家分享。

 

ElementTree,一个用于 Python 的本机 XML 库。在 Python 的标准分发版中早已包括了几个 XML API,包括:DOM 模块、SAX 模块、 expat包装器和不赞成使用的 xmllib。其中,只有 xml.dom将 XML 文档转换为内存中的对象,您可以通过节点上的方法调用来操作这些对象。实际上,您将发现存在几种不同的 Python DOM 实现,其特性各有不同:

  • xml.minidom是一个基本的实现。
  • xml.pulldom只在需要时构建被访问的子树。
  • 考虑到速度问题,4Suite 的 cDomletteFt.Xml.Domlette)用 C 语言构建 DOM 树,避免了使用 Python 回调。

当然,出于我身为作者的自负,我最想做的是将 ElementTree和我自己的 gnosis.xml.objectify及其它几种目的和行为都极其接近的库进行比较。 ElementTree的目标是以数据结构的形式存储 XML 文档的表示,这些数据结构的行为方式同您在 Python 中考虑数据的方式非常相似。这里的关注焦点在于以 Python 进行编程,而不是使您的编程风格顺应 XML。

一些基准测试

我的同事 Uche Ogbuji 曾为另一个出版物写过一篇关于 ElementTree的短文。(请参阅 参考资料。)他对 ElementTree和 DOM 做了几个测试,其中之一比较了它们的相对速度和内存消耗。Uche 选用了他自己的 cDomlette 作为比较对象。很遗憾,我不能在我使用的 Mac OSX 机器上安装 4Suite 1.0a1(我正在研究一种变通方法)。然而,我可以根据 Uche 的评估来估计大致性能 - 他指出 ElementTreecDomlette相比,速度慢 30%,但消耗的内存也要少 30%。

我极为好奇的是, ElementTreegnosis.xml.objectify在速度和内存上比较,结果会如何。实际上,之前我从未对我的模块进行过非常精确的基准测试,因为我始终没有一个具体的 可比对象。我选择了两个过去我曾用于基准测试的文档:莎士比亚的 哈姆雷特289 KB XML 版本,及 3 MB XML Web 日志。我创建了几个脚本,仅用于将 XML 文档解析为几种工具的对象模型,但此外不作任何其它操作:

清单 1. 对用于 Python 的 XML 对象模型计时的脚本

% cat time_xo.py

        import sys

        from gnosis.xml.objectify
        import XML_Objectify,EXPAT
doc = XML_Objectify(sys.stdin,EXPAT).make_instance()
---
% cat time_et.py

        import sys

        from elementtree
        import ElementTree
doc = ElementTree.parse(sys.stdin).getroot()
---
% cat time_minidom.py

        import sys

        from xml.dom
        import minidom
doc = minidom.parse(sys.stdin)
      

在所有三个案例中,程序对象的创建非常类似,对于 cDomlette 也一样。我在另一个窗口观察 top 的输出,以评估内存使用情况;每种测试进行三遍以确保其一致性,并取其结果的平均值(每次运行使用的内存是相同的)。

图 1. 以 Python 编写的 XML 对象模型的基准测试结果
以 Python 编写的 XML 对象模型的基准测试

很明显,对于稍大一点的 XML 文档, xml.minidom很快就变得不实用了。其它则都还算合理(公正地说)。 gnosis.xml.objectify消耗内存最少,但这并不奇怪,因为它不保存原始 XML 实例中 所有的信息(保存了数据内容,但不保存所有的结构信息)。

我也对 Ruby 的 REXML进行了测试,使用了以下脚本:

清单 2. Ruby REXML 解析脚本(time_rexml.rb)


require "rexml/document"
include REXML
doc = (Document.new File.new ARGV.shift).root

测试结果表明, REXMLxml.minidom一样消耗大量资源:解析 Hamlet.xml 用了 10 秒,占用了 14 MB 内存;解析 Weblog.xml 用了 190 秒,占用了 150 MB 内存。显然,编程语言的选择通常优先于库的比较。


回页首

处理 XML 文档对象

ElementTree 的一个优点在于它能够被循环运行。这是指,您可以读入一个 XML 实例,修改数据结构使之非常类似于本机风格,然后调用 .write() 方法进行重新序列化得到格式良好的 XML。当然,DOM 也能做到这一点,但 gnosis.xml.objectify不行。为 gnosis.xml.objectify构造一个定制输出函数用于生成 XML 也不是 那么困难 - 但这不能自动进行。使用 ElementTree 以及 ElementTree 实例的 .write() 方法,通过便利函数 elementtree.ElementTree.dump() 可以序列化单独的 Element 实例。这让您可以从单独的对象节点 - 其中包括 XML 实例的根节点 - 编写 XML 片段。

我提出了一个简单的任务来比较 ElementTreegnosis.xml.objectify 的 API。用于基准测试的大型文档 weblog.xml 包含大约 8,500 个 <entry> 元素,每个元素都含有相同的子字段集合 - 这是一个面向数据的 XML 文档的典型布局。在处理该文件时,任务之一可能是从每一个 entry 收集一些字段,但这只是在其它某些字段有特定值(或范围,或匹配的部分内容)的情况下。当然,如果您确实只想要运行这一个任务,可使用一个流 API(如 SAX)以避免在内存中为整个文档建模 - 但这里假定该任务是应用程序在大型数据结构上运行的任务之一。一个 <entry> 元素可能像这样:

清单 3. <entry> 元素样本


<entry>
  <host>64.172.22.154</host>
  <referer>-</referer>
  <userAgent>-</userAgent>
  <dateTime>19/Aug/2001:01:46:01</dateTime>
  <reqID>-0500</reqID>
  <reqType>GET</reqType>
  <resource>/</resource>
  <protocol>HTTP/1.1</protocol>
  <statusCode>200</statusCode>
  <byteCount>2131</byteCount>
</entry>

如果使用 gnosis.xml.objectify,我也许会这样编写一个“过滤和抽取”应用程序:

清单 4. “过滤和抽取”应用程序(select_hits_xo.py)


        from gnosis.xml.objectify
        import XML_Objectify, EXPAT
weblog = XML_Objectify(
        'weblog.xml',EXPAT).make_instance()
interesting = [entry
        for entry
        in weblog.entry

        if entry.host.PCDATA==
        '209.202.148.31' 
        and
             entry.statusCode.PCDATA==
        '200']

        for e
        in interesting:

        print
        "%s (%s)" % (e.resource.PCDATA,
                     e.byteCount.PCDATA)
      

列表理解用作数据过滤器是相当方便的。本质上, ElementTree的工作方式与此相同:

清单 5. “过滤和抽取”应用程序(select_hits_et.py)


        from elementtree
        import ElementTree
weblog = ElementTree.parse(
        'weblog.xml').getroot()
interesting = [entry
        for entry
        in weblog.findall(
        'entry')

        if entry.find(
        'host').text==
        '209.202.148.31' 
        and
             entry.find(
        'statusCode').text==
        '200']

        for e
        in interesting:

        print
        "%s (%s)" % (e.findtext(
        'resource'),
                    e.findtext(
        'byteCount'))
      

请注意上面的不同之处。 gnosis.xml.objectify 将子元素节点直接作为节点的属性进行连接(每个节点都是一个根据标记名命名的定制类)。另一方面, ElementTree 使用 Element 类的方法查找子节点。 .findall() 方法返回所有匹配节点的列表; .find() 则仅返回首次匹配的节点; .findtext() 返回节点的文本内容。如果您只想要 gnosis.xml.objectify 子元素上的首次匹配,只要为其建立索引即可 - 例如, node.tag[0] 。但如果这样的子元素只有一个,那么无需建立显式的索引,您也可以引用它。

但是在 ElementTree的示例中,其实您并不 需要显式查找所有 <entry> 元素;迭代时 Element 实例的行为方式类似于列表。在这里要注意一点,不管子节点有何标记,对 所有的子节点都进行迭代。相比之下, gnosis.xml.objectify 节点没有内置方法可遍历它所有的子元素。尽管如此,构造一个一行的 children() 函数还是挺简单的(我会在将来的发行版中包含该函数)。比照清单 6:

清单 6. ElementTree 对节点列表和特定子类型进行的迭代


>>> open('simple.xml','w.').write('''<root>
... <foo>this</foo>
... <bar>that</bar>
... <foo>more</foo></root>''')
>>> from elementtree import ElementTree
>>> root = ElementTree.parse('simple.xml').getroot()
>>> for node in root:
...     print node.text,
...
this that more
>>> for node in root.findall('foo'):
...     print node.text,
...
this more

和清单 7:

清单 7. gnosis.xml.objectify 对所有子节点进行的有损耗的迭代


>>> children=lambda o: [x for x in o.__dict__ if x!='__parent__']
>>> from gnosis.xml.objectify import XML_Objectify
>>> root = XML_Objectify('simple.xml').make_instance()
>>> for tag in children(root):
...     for node in getattr(root,tag):
...         print node.PCDATA,
...
this more that
>>> for node in root.foo:
...     print node.PCDATA,
...
this more

正如您所见, gnosis.xml.objectify 目前抛弃了有关散布在代码中的 <foo><bar> 元素原始顺序的信息( 能够通过另一个奇妙的属性,如 .__parent__ 记住该顺序,但没有人需要或发送一个补丁来做这件事)。

ElementTree 在一个称为 .attrib 的节点属性中存储 XML 属性;这些属性被存储在字典中。 gnosis.xml.objectify将 XML 属性直接放置到相应名称的节点属性中。我使用的样式往往弱化 XML 的属性和元素内容之间的差异 - 我认为,这应该是由 XML,而不是我的本机数据结构所担心的问题。举例来说:

清单 8. 访问子节点和 XML 属性时的差异


>>> xml = '<root foo="this"><bar>that</bar></root>'
>>> open('attrs.xml','w').write(xml)
>>> et = ElementTree.parse('attrs.xml').getroot()
>>> xo = XML_Objectify('attrs.xml').make_instance()
>>> et.find('bar').text, et.attrib['foo']
('that', 'this')
>>> xo.bar.PCDATA, xo.foo
(u'that', u'this')

在 XML 属性(创建了包含有文本的节点属性)和 XML 元素内容(创建了包含对象 - 也许还包括具有 .PCDATA 的子节点 - 的节点属性)之间, gnosis.xml.objectify仍造成了 一些差异。


回页首

XPath 和 tail 属性

ElementTree 在其 .find*() 方法中实现了一个 XPath 的子集。对于在子节点层次之间进行查找而言,使用该样式与使用嵌套代码相比,要简洁许多,尤其对含有通配符的 XPath 更是如此。举例来说,如果我想知道对我的 Web 服务器所有访问的时间戳记,可以这样检查 weblog.xml:

清单 9. 使用 XPath 查找嵌套子元素


>>> from elementtree import ElementTree
>>> weblog = ElementTree.parse('weblog.xml').getroot()
>>> timestamps = weblog.findall('entry/dateTime')
>>> for ts in timestamps:
...     if ts.text.startswith('19/Aug'):
...         print ts.text

当然,对于像 weblog.xml 这样标准、浅显的文档,使用列表理解很容易就可以做同样的工作:

清单 10. 使用列表理解查找并过滤嵌套子元素


>>> for ts in [ts.text for e in weblog
...            for ts in e.findall('dateTime')
...            if ts.text.startswith('19/Aug')]:
...     print ts

然而,面向散文的 XML 文档,其文档结构往往拥有更多的变化,且嵌套标记通常有至少五或六层深。举例来说,一个 XML 模式(如 DocBook 或 TEI)可能会在节、子节、参考书目中含有引证,或者是在斜体标记、块引用中含有引证,等等。查找每个 <citation> 元素会要求涉及多个层次,进行繁琐(可能需要递归)的搜索。而使用 XPath,您只要这样写:

清单 11. 使用 XPath 查找深层嵌套子元素


>>> from elementtree import ElementTree
>>> weblog = ElementTree.parse('weblog.xml').getroot()
>>> cites = weblog.findall('.//citation')

然而, ElementTree对 XPath 的支持是有限的:您不能使用完整 XPath 所包含的各种函数,也不能按属性进行搜索。可是,在可行范围内,在 ElementTree中使用 XPath 子集可以大大提高其可读性和表达能力。

在结束本文前我还想要再提一点 ElementTree比较奇怪的地方。XML 文档可以是混合内容。尤其是面向散文的 XML 往往会任意散布 PCDATA 和标记。但是您应该在哪里正确地 存储子节点之间的文本呢?由于 ElementTreeElement 实例有一个单一的 .text 属性 - 包含一个字符串 - 它并不真正为断开的字符串序列保留空格。 ElementTree 采用的解决方案赋予了每个节点一个 .tail 属性,它包含了位于结束标记之后,下一元素开始或父元素结束之前所有的文本。举例来说:

清单 12. 存储在 node.tail 属性中的 PCDATA


>>> xml = '<a>begin<b>inside</b>middle<c>inside</c>end</a>'
>>> open('doc.xml','w').write(xml)
>>> doc = ElementTree.parse('doc.xml').getroot()
>>> doc.text, doc.tail
('begin', None)
>>> doc.find('b').text, doc.find('b').tail
('inside', 'middle')
>>> doc.find('c').text, doc.find('c').tail
('inside', 'end')

回页首

结束语

ElementTree是一个非常不错的模块,和 DOM 相比,它提供了一个更轻量级的对象模型,用于以 Python 处理 XML。虽然我没有在本文中提及, ElementTree在从头生成 XML 文档方面和它在操作现有的 XML 数据方面一样出色。

作为与之类似的库 gnosis.xml.objectify的作者,我无法完全客观地评价 ElementTree;尽管如此,与那些 ElementTree所提供的方法相比,我始终尝试在 Python 程序中用我自己的方法更简单自然地予以实现。ElementTree 通常仍利用节点的方法来操作数据结构,而不是像人们通常处理应用程序中构建的数据结构那样直接访问节点属性。

然而,在有些方面, ElementTree很出色。使用 XPath 访问深层嵌套元素要比手工递归搜索容易得多。显然,在 DOM 中也可使用 XPath,但代价是形成一个过于庞大且不够统一的 API。 ElementTree 所有的 Element 节点的工作方式是一致的,不像 DOM 的节点类型那样“装饰华丽”。

2006年10月09日

    这是一部去年的电影,我到今年才看,甚是惭愧。这是一部有关宗教,英雄,骑士的电影,对骑士,贵族,基督教和伊斯兰教有兴趣的朋友都不容错过。当然,里面的战争场面也非常宏大,喜欢军事的朋友同样值得一看。

    《天国王朝》(Kingdom of Heaven)的故事发生在第二次和第三次十字军东征之间,时间大约是1183年到1187年撒拉丁攻占耶路撒冷。

    电影里奥兰多布鲁姆扮演的铁匠贝里昂非常帅气,然而我个人感觉这是一部男人的电影,所以帅气的男主角并不能给电影带来太多东西。这个人物的原型是伊贝林的贝里昂,在历史上是有名的贵族,也非铁匠出身。

    让我们来看看那段时期真正的两位主角,历史上极其著名的萨拉丁和麻风鲍德温。萨拉丁是阿拉伯世界真正的王者,是近代以来但凡有点抱负的阿拉伯领导人梦寐以求成为的人物。他的伟大功绩不仅在于夺取并守住了耶路撒冷,更重要的是,他的仁慈。

    与88年前十字军攻克耶路撒冷时大开杀戒形成鲜明的对比,萨拉丁进入耶路撒冷没有杀一个人,没有烧一栋房子。根据受降时签订的协议,耶路撒冷每个男人要缴纳10第纳尔赎金,每个女人缴纳5第纳尔,儿童1第纳尔;无力缴纳的人则成为奴隶。萨拉丁免去了7000穷人的赎金。萨拉丁的弟弟向萨拉丁要了1000名奴隶,随即将他们释放。耶路撒冷主教也随即效仿,向他要了700名奴隶然后释放。最后,萨拉丁自己宣布释放了所有战俘,不要一分赎金。

    在十字军占领期间,阿克萨清真寺被改为圣殿骑士团的总部,磐石清真寺成了教堂。萨拉丁将它们恢复为清真寺。磐石清真寺金顶上的十字架被拆除,宣礼的声音再次回荡在阿克萨清真寺的上空。有人向他建议拆毁耶路撒冷的圣墓大教堂,萨拉丁没有同意。相反,他将耶路撒冷的圣地向所有宗教开放。

    在后来狮心王的第三次十字军东征中,萨拉丁还让弟弟阿迪勒给理查送去两匹好马。最后萨拉丁和理查都病倒  了,萨拉丁派人给自己的死对头理查送去了水果,还派去了医生。双方签订和约,穆斯林占有巴勒斯坦内地,基督教徒占有海岸,耶路撒冷向朝觐的基督教徒开放。
    看过电影的人,一定对戴着面具的麻风国王印象深刻。虽然受着麻风病的煎熬,身上已经血肉模糊,但是其处事之英明,还是让人赞叹。在历史上,他的第一次成功在1177年。那年秋天,撒拉丁的三万骑兵分成两路发动进攻,其中两万进攻圣殿骑士团所在的加沙地带(这个地名是不是很熟?),一万包围阿斯卡伦。耶路撒冷几乎没有任何准备,一开始就被打了个措手不及。他们事前甚至没有得到什么情报,情报工作失误导致的结果是,在敌军到达阿斯卡伦之前,竟然让国王在少数部队的陪同下到了那个地方,自动把羊羔送到饿虎的嘴边。这一年的麻风国王十六岁,仍然整日生活在面具之下。塞尔柱人很快就清楚了耶路撒冷王正被他们围困在阿斯卡伦。欣喜若狂的撒拉丁马上组织部队对该地区进行猛烈的攻击,誓要生擒敌酋。国王的骑士卫队镇定而毫无主意地保卫在君主的周围,抱定了进行最后死战的决心。然而这时候,他们却从身后的孩子口中听到了冷静清醒而又条理清晰的指令。骑士们惊奇地转身看着国王,然后鞠躬并举剑示意,执行命令。在打退塞尔柱人的进攻后,鲍德温家的少年战术天才抓住对方组织攻势的间隙,率领帐下突围而走。撒拉丁闻讯大发雷霆并派遣马木留克骑兵卫队狂追,但是无济于事。鲍德温突围后并不向耶路撒冷撤退,他派出通讯员命令各地骑士立即前来与他相会,同时往医院骑士团的驻地进发。在那里,他与带领圣殿骑士团残部突围的雷纳德相遇。狼狈的雷纳德原以为被围的国王已经提前归天,想不到国王已经在部署决战的事宜。他第一次感到那个银面具下所散发的气度,也第一次认识到了国王的权利和威严。于是,耶路撒冷国王集结主力军队,与同样收束军队前来夺取耶路撒冷的撒拉丁在蒙吉萨相遇。11月25日,双方大战。结果以撒拉丁的溃败而告终,其最精锐的马木留克近卫部队几乎全灭。

    1179年,撒拉丁率军偷袭了在泉水谷的雷纳德和圣殿骑士团,鲍德温闻讯马上亲提大军前来交战。双方对峙许久,撒拉丁无法占到便宜。于是双方缔结两年的休战协议。沙漠之王终于被拒绝在耶路撒冷国土之外。

    1183年,撒拉丁进攻耶路撒冷。撒拉丁进行了数次挑战,使用各种手段引诱摄政王盖伊外出决战,眼见即将成功。但是,那个麻风病人,那个半死不活的麻风病人还是来了。沙漠之王在军帐里沉默了很久,最后下令撤军,默默地回到沙漠去等待他的麻风对手死去的那天。

    1185年3月,麻风病人,沙漠之雄撒拉丁永远无法跨越的对手,耶路撒冷王国最后的强者,也是最脆弱的强者——鲍德温四世,终于得到了身体和灵魂的最后解脱。在《天国王朝》片中,鲍德温临死时对西比拉说,他仿佛又回到了16岁那一年,一个意气风发的少年指挥王国击败了撒拉丁的精锐。这一战即是1177年的蒙吉萨之战。

惺惺相惜