在本专栏的前几期中,我研究了XML库,其目的是模仿给定编程语言中最熟悉的本机操作。 我首先介绍的是针对Python的自己的gnosis.xml.objectify 。 我还专门介绍了Haskell的HaXml和Ruby的REXML 。 尽管这里没有讨论,但Java的JDOM和Perl的XML :: Grove也有类似的目标。

最近,我注意到comp.lang.python新闻组的许多发布者提到Fredrik Lundh的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%。

通常,我很好奇ElementTree在速度和内存上如何与gnosis.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对象模型的基准





显而易见的一件事是,对于中等大小的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

REXML被证明与xml.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文档的典型安排。 在处理此文件时,一项任务可能是从每个条目中收集几个字段,但前提是其他一些字段具有特定值(或范围,或匹配正则表达式)。 当然,如果您真的只想执行一项任务,则使用诸如SAX之类的流式API可以避免在内存中对整个文档进行建模-但假定此任务是应用程序对大型数据结构执行的多项任务之一。 一个<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将XML属性存储在名为.attrib的节点属性中; 属性存储在字典中。 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')

gnosis.xml.objectify在创建包含文本的节点属性的XML属性和创建包含对象的节点属性(也许具有.PCDATA子节点)的XML元素内容之间仍然有所区别。

XPath和尾巴

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文档往往具有更多可变的文档结构,并且通常将标记嵌套至少五到六层。 例如,诸如DocBook或TEI之类的XML模式可能在节,小节,参考书目中或有时在斜体标签中或在块引用中具有引用。 要找到每个<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和标签。 但是,您到底应该在哪里存储子节点之间的文本呢? 由于ElementTree Element实例具有单个.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')

结论

与DOM提供的元素相比, ElementTree是一项不错的尝试,旨在将重量更轻的对象模型带入Python的XML处理中。 尽管我在本文中没有提到,但ElementTree擅长从头生成XML文档,就像处理现有XML数据一样。

作为类似库gnosis.xml.objectify的作者 ,我不能完全客观地评估ElementTree ; 尽管如此,我继续在Python程序中发现自己的方法比ElementTree提供的方法更自然。 后者通常仍使用节点方法来操纵数据结构,而不是像通常在应用程序内构建的数据结构那样直接访问节点属性。

但是,在几个方面, ElementTree令人瞩目。 与手动递归搜索相比,使用XPath访问深度嵌套的元素要容易得多。 显然,DOM还为您提供了XPath,但代价是API更加繁重且统一程度较低。 ElementDOM的所有Element节点均以一致的方式运行,这与DOM的节点类型不尽相同。





翻译自: https://www.ibm.com/developerworks/xml/library/x-matters28/index.html