IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

XML 问题: 使用 ElementTree,以 Python 语言处理 XML

API 和类似的库相比较,结果如何?

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

David Mertz,博士 (mertz@gnosis.cx), 比较者, Gnosis Software, Inc.

2003 年 3 月 01 日

Fredrik Lundh 的 ElementTree 模块是一种日益受欢迎的 API,用于以 Python 语言进行轻量级且高速的 XML 文档操作。在这篇专栏文章中,David 将 ElementTree 和其它几种致力于以对象树方式处理 XML 实例的库,尤其是他自己的 gnosis.xml.objectify 模块,作了个比照。

在本专栏以前的文章中,我已讨论了一些 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%。

我极为好奇的是, 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 的节点类型那样“装饰华丽”。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 请参加关于本文的 论坛。(您也可以通过单击本文顶部或底部的 讨论来访问论坛。)



  • 在 Fredrik Lundh 的 元素树页面上能找到更多有关 ElementTree的内容。



  • developerWorks专栏作家 Uche Ogbuji 撰写的 这篇 XML.com 文章从另一种角度介绍了该主题。



  • 请阅读 David Mertz 以前的有关 XML 库的专栏文章:
    • XML 问题 #2介绍了 gnosis.xml.objectify,简称为 xml_objectifydeveloperWorks,2000 年 8 月)。
    • XML 问题 #11对 xml_pickle 和 xml_objectify 做了更新,向读者介绍了一些 gnosis.xml.objectify早期的改进。这篇专栏文章未提及一些更新的特性,但在该模块的历史记录和其他文档文件中可以找到(2001 年 6 月)。
    • XML 问题 #14讨论了用于 Haskell 惰性纯函数型编程语言的 HaXml模块(2001 年 10 月)。
    • XML 问题 #18讨论了 Ruby 的 REXML库(2002 年 3 月)。
    专栏文章汇总页面您可以找到 David 的 XML 问题(XML Matters)专栏以前所有的文章。


  • 了解另一个 Python 的 XML API/库 - generateDS 。开发人员 Dave Kuhlman 曾写过一篇很好的 评论,比较了 generateDSgnosis.xml.objectify。简要地说, generateDS背后的思想是使用一个 XML Schema 作为正确处理 XML 实例中元素的 Python 类的基础。与处理 XML 树的通用方式不同, generateDS是 Python 模块的代码生成器,用以处理特定的 XML 文档模式;自动生成的代码可以很方便地专用于快速构成定制应用程序。



  • developerWorks XML 专区上查找更多的 XML 参考资料。



  • 试用一下 IBM WebSphere Studio,它是一套使 XML 开发自动化的工具,可以使用 Java 语言进行开发,也可以使用其它语言。它同 WebSphere Application Server紧密集成,也可以与其它 J2EE 服务器一起使用。



  • 了解如何成为一名 IBM 认证的 XML 及相关技术的开发人员




关于作者

Author photo

对 David Mertz 来说,一个原子对象也是一个许多事实的组合。可通过 mertz@gnosis.cx与 David 联系;在 http://gnosis.cx/publish/上可以了解他的生活。欢迎就这篇、以前或将来的专栏文章提出您的建议和意见。请查阅 David 的新书 Text Processing in Python




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款