使用 ElementTree,以 Python 语言处理 XML

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

 

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

  • xml.minidom是一个基本的实现。
  • xml.pulldom只在需要时构建被访问的子树。
  • 考虑到速度问题,4Suite 的 cDomlette( Ft.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 的评估来估计大致性能 - 他指出 ElementTree同 cDomlette相比,速度慢 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 对象模型的基准测试结果
以 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 片段。

我提出了一个简单的任务来比较 ElementTree 和 gnosis.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 和标记。但是您应该在哪里正确地 存储子节点之间的文本呢?由于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')

回页首

结束语

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值