[关闭]
@nalan90 2017-08-01T14:35:29.000000Z 字数 5070 阅读 655

xml文件常用操作

Python高效编程技巧实战


英文原文:http://eli.thegreenplace.net/2012/03/15/processing-xml-in-python-with-elementtree/

中文译文:http://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/processing-xml-in-python-with-element-tree.html
应该使用哪个 XML 库?
Python 有非常非常多的工具来处理 XML。在这个部分我想对 Python 所提供的包进行一个简单的浏览,并且解释为什么 ElementTree 是你最应该用的那一个。

xml.dom.* 模块 - 是 W3C DOM API 的实现。如果你有处理 DOM API 的需要,那么这个模块适合你。注意:在 xml.dom 包里面有许多模块,注意它们之间的不同。

xml.sax.* 模块 - 是 SAX API 的实现。这个模块牺牲了便捷性来换取速度和内存占用。SAX 是一个基于事件的 API,这就意味着它可以“在空中”(on the fly)处理庞大数量的的文档,不用完全加载进内存。

xml.parser.expat - 是一个直接的,低级一点的基于 C 的 expat 的语法分析器。 expat 接口基于事件反馈,有点像 SAX 但又不太像,因为它的接口并不是完全规范于 expat 库的。

最后,我们来看看 xml.etree.ElementTree (以下简称 ET)。它提供了轻量级的 Python 式的 API ,它由一个 C 实现来提供。相对于 DOM 来说,ET 快了很多(见注释3)而且有很多令人愉悦的 API 可以使用。相对于 SAX 来说,ET 也有 ET.iterparse 提供了 “在空中” 的处理方式,没有必要加载整个文档到内存。ET 的性能的平均值和 SAX 差不多,但是 API 的效率更高一点而且使用起来很方便。我一会儿会给你们看演示。

建议尽可能的使用 ET 来处理 XML ,除非你有什么非常特别的需要。


将 XML 解析为树的形式
XML 是一种分级的数据形式,所以最自然的表示方法是将它表示为一棵树。ET 有两个对象来实现这个目的 - ElementTree 将整个 XML 解析为一棵树, Element 将单个结点解析为树。如果是整个文档级别的操作(比如说读,写,找到一些有趣的元素)通常用 ElementTree 。单个 XML 元素和它的子元素通常用 Element
  1. <?xml version="1.0"?>
  2. <doc>
  3. <branch name="testing" hash="1cdf045c">
  4. text,source
  5. </branch>
  6. <branch name="release01" hash="f200013e">
  7. <sub-branch name="subrelease01">
  8. xml,sgml
  9. </sub-branch>
  10. </branch>
  11. <branch name="invalid">
  12. </branch>
  13. </doc>

让我们加载并且解析这个 XML :

  1. In [12]: import xml.etree.cElementTree as ET
  2. In [13]: tree = ET.ElementTree(file='doc.xml')
  3. ## 抓根结点元素:
  4. In [14]: root = tree.getroot()
  5. ## 和预期一样,root 是一个 Element 元素。我们可以来看看:
  6. In [15]: root.tag, root.attrib
  7. Out[15]: ('doc', {})
  8. ## 看吧,根元素没有任何状态。就像任何 Element 一样,它可以找到自己的子结点:
  9. In [16]: for child_of_root in root:
  10. ....: print child_of_root.tag, child_of_root.attrib
  11. ....:
  12. branch {'hash': '1cdf045c', 'name': 'testing'}
  13. branch {'hash': 'f200013e', 'name': 'release01'}
  14. branch {'name': 'invalid'}
  15. ## 我们也可以进入一个指定的子结点:
  16. In [18]: root[0].tag, root[0].text
  17. Out[18]: ('branch', '\n text,source\n ')
  18. ## Element 对象有一个 iter 方法可以对子结点进行深度优先遍历。 ElementTree 对象也有 iter 方法来提供便利
  19. In [19]: for elem in tree.iter():
  20. ....: print elem.tag, elem.attrib
  21. ....:
  22. doc {}
  23. branch {'hash': '1cdf045c', 'name': 'testing'}
  24. branch {'hash': 'f200013e', 'name': 'release01'}
  25. sub-branch {'name': 'subrelease01'}
  26. branch {'name': 'invalid'}
  27. ## 遍历所有的元素,然后检验有没有你想要的。ET 可以让这个过程更便捷。 iter 方法接受一个标签名字,然后只遍历那些有指定标签的元素
  28. In [20]: for elem in tree.iter(tag = 'branch'):
  29. ....: print elem.tag, elem.attrib
  30. ....:
  31. branch {'hash': '1cdf045c', 'name': 'testing'}
  32. branch {'hash': 'f200013e', 'name': 'release01'}
  33. branch {'name': 'invalid'}

来自 XPath 的帮助
为了寻找我们感兴趣的元素,一个更加有效的办法是使用 XPath 支持。 Element 有一些关于寻找的方法可以接受 XPath 作为参数。 find 返回第一个匹配的子元素, findall 以列表的形式返回所有匹配的子元素, iterfind 为所有匹配项提供迭代器。这些方法在 ElementTree 里面也有。
  1. In [21]: for elem in tree.iterfind('branch/sub-branch'):
  2. ....: print elem.tag, elem.attrib
  3. ....:
  4. sub-branch {'name': 'subrelease01'}
  5. ## 这个例子在 branch 下面找到所有标签为 sub-branch 的元素。然后给出如何找到所有的 branch 元素,用一个指定 name 的状态即可:
  6. In [22]: for elem in tree.iterfind('branch[@name="release01"]'):
  7. ....: print elem.tag, elem.attrib
  8. ....:
  9. branch {'hash': 'f200013e', 'name': 'release01'}

建立 XML 文档
ET 提供了建立 XML 文档和写入文件的便捷方式。 ElementTree 对象提供了 write 方法。

现在,这儿有两个常用的写 XML 文档的脚本。

修改文档可以使用 Element 对象的方法:
  1. In [23]: root = tree.getroot()
  2. In [24]: del root[2]
  3. In [25]: root[0].set('foo', 'bar')
  4. In [26]: for subelem in root:
  5. ....: print subelem.tag, subelem.attrib
  6. ....:
  7. branch {'foo': 'bar', 'hash': '1cdf045c', 'name': 'testing'}
  8. branch {'hash': 'f200013e', 'name': 'release01'}
  9. ## 我们在这里删除了根元素的第三个子结点,然后为第一个子结点增加新状态。然后这个树可以写回到文件中。
  10. In [27]: import sys
  11. In [28]: tree.write(sys.stdout)
  12. <doc>
  13. <branch foo="bar" hash="1cdf045c" name="testing">
  14. text,source
  15. </branch>
  16. <branch hash="f200013e" name="release01">
  17. <sub-branch name="subrelease01">
  18. xml,sgml
  19. </sub-branch>
  20. </branch>
  21. </doc>
注意状态的顺序和原文档的顺序不太一样。这是因为 ET 讲状态保存在无序的字典中。语义上来说,XML 并不关心顺序。

建立一个全新的元素也很容易。ET 模块提供了 SubElement 函数来简化过程:
  1. In [29]: a = ET.Element('elem')
  2. In [30]: c = ET.SubElement(a, 'child1')
  3. In [31]: c.text = 'some text'
  4. In [32]: d = ET.SubElement(a, 'child2')
  5. In [33]: b = ET.Element('elem_b')
  6. In [34]: root = ET.Element('root')
  7. In [35]: root.extend((a, b))
  8. In [36]: tree = ET.ElementTree(root)
  9. In [37]: tree.write(sys.stdout)
  10. <root><elem><child1>some text</child1><child2 /></elem><elem_b /></root>

使用 iterparse 来处理 XML 流

就像我在文章一开头提到的那样,XML 文档通常比较大,所以将它们全部读入内存的库可能会有点儿小问题。这也是为什么我建议使用 SAX API 来替代 DOM 。

我们刚讲过如何使用 ET 来将 XML 读入内存并且处理。但它就不会碰到和 DOM 一样的内存问题么?当然会。这也是为什么这个包提供一个特殊的工具,用来处理大型文档,并且解决了内存问题,这个工具叫 iterparse 。

我给大家演示一个 iterparse 如何使用的例子。我用 自动生成 拿到了一个 XML 文档来进行说明。这只是开头的一小部分:

  1. <?xml version="1.0" standalone="yes"?>
  2. <site>
  3. <regions>
  4. <africa>
  5. <item id="item0">
  6. <location>United States</location> <!-- Counting locations -->
  7. <quantity>1</quantity>
  8. <name>duteous nine eighteen </name>
  9. <payment>Creditcard</payment>
  10. <description>
  11. <parlist>
  12. [...]
我已经用注释标出了我要处理的元素,我们用一个简单的脚本来计数有多少 location 元素并且文本内容为“Zimbabwe”。这是用 ET.parse 的一个标准的写法:
  1. tree = ET.parse(sys.argv[2])
  2. count = 0
  3. for elem in tree.iter(tag='location'):
  4. if elem.text == 'Zimbabwe':
  5. count += 1
  6. print count
所有 XML 树中的元素都会被检验。当处理一个大约 100MB 的 XML 文件时,占用的内存大约是 560MB ,耗时 2.9 秒。

注意:我们并不需要在内存中加载整颗树。它检测我们需要的带特定值的 location 元素。其他元素被丢弃。这是 iterparse 的来源:
  1. count = 0
  2. for event, elem in ET.iterparse(sys.argv[2]):
  3. if event == 'end':
  4. if elem.tag == 'location' and elem.text == 'Zimbabwe':
  5. count += 1
  6. elem.clear() # discard the element
  7. print count
这个循环遍历 iterparse 事件,检测“闭合的”(end)事件并且寻找 location 标签和指定的值。在这里 elem.clear() 是关键 - iterparse 仍然建立一棵树,只不过不需要全部加载进内存,这样做可以有效的利用内存空间(见注释7)。

处理同样的文件,这个脚本占用内存只需要仅仅的 7MB ,耗时 2.5 秒。速度的提升归功于生成树的时候只遍历一次。相比较来说, parse 方法首先建立了整个树,然后再次遍历来寻找我们需要的元素(所以慢了一点)。

结论

在 Python 众多处理 XML 的模块中, ElementTree 真是屌爆了。它将轻量,符合 Python 哲学的 API ,出色的性能完美的结合在了一起。所以说如果要处理 XML ,果断地使用它吧!

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注