读书人

应用由 Python 编写的 lxml 实现高性能

发布时间: 2012-10-27 10:42:26 作者: rapoo

使用由 Python 编写的 lxml 实现高性能 XML 解析
应用由 Python 编写的 lxml 实现高性能 XML 解析应用由 Python 编写的 lxml 实现高性能 XML 解析应用由 Python 编写的 lxml 实现高性能 XML 解析应用由 Python 编写的 lxml 实现高性能 XML 解析 平均分 (共 4 个评分 )XML 库方法平均时间,单位(秒)cElementTreeIterparse32lxml目标解析器54lxml优化后的 iterparse25

?

它的伸缩性如何?

对 Open Directory 数据使用 清单 4 中的 iterparse 方法,每次运行耗时 122 秒,约是解析版权数据所用时间的 5 倍。由于 Open Directory 数据的数量也约是版权数据的 5 倍(1.9 GB),这种方法应该表现出非常好性能,对特别大的文件尤其如此。

序列化

如果对 XML 文件所做的全部操作只是从单个节点获取一些文本,可以使用一个简单的正则表达式,其处理速度可能会比任何 XML 解析器都快。但是在实践中,如果数据非常复杂,则几乎不可能完成任务,因此不推荐使用这种方法。在需要真正的数据操作时,XML 库的价值是不可估量的。

将 XML 序列化为一个字符串或文件是 lxml 的长项,因为它依赖于 libxml2 C 代码库。如果要执行要求序列化的任务,lxml 无疑是最佳选择,但是需要使用一些技巧来获得最佳性能。

在序列化子树时使用 deepcopy

lxml 保持子节点及其父节点之间的引用。该特性的一个特点就是 lxml 中的节点有且仅有一个父节点(cElementTree 没有父节点)。

清单 6 包含版权文件中的所有 <Record>,并写入了一条只包含标题和版权信息的简化记录。


清单 6. 序列化元素的子节点

from lxml import etreeimport deepcopy def serialize(elem):    # Output a new tree like:    # <SimplerRecord>    #   <Title>This title</Title>    #   <Copyright><Date>date</Date><Id>id</Id></Copyright>    # </SimplerRecord>        # Create a new root node    r = etree.Element('SimplerRecord')    # Create a new child    t = etree.SubElement(r, 'Title')    # Set this child's text attribute to the original text contents of <Title>    t.text = elem.iterchildren(tag='Title').next().text    # Deep copy a descendant tree    for c in elem.iterchildren(tag='Copyright'):        r.append( deepcopy(c) )    return rout = open('titles.xml', 'w')context = etree.iterparse('copyright.xml', events=('end',), tag='Record')# Iterate through each of the <Record> nodes using our fast iteration methodfast_iter(context,           # For each <Record>, serialize a simplified version and write it          # to the output file          lambda elem:               out.write(                 etree.tostring(serialize(elem), encoding='utf-8')))

?

不要使用 deepcopy 复制单个节点的文本。手动创建新节点、填充文本属性并进行序列化,这样做的速度更快。在我的测试中,对 <Title><Copyright> 调用 deepcopy 要比 清单 6 中的代码慢 15%。在序列化大型后代树(descendant trees)时,会看到 deepcopy 将使性能得到巨大的提升。

在使用 清单 7 中的代码对 cElementTree 进行基准测试时,lxml 的序列化程序的速度几乎提高了两倍(50% 和 95%):


清单 7. 使用 cElementTree 序列化

def serialize_cet(elem):    r = cet.Element('Record')    # Create a new element with the same text child    t = cet.SubElement(r, 'Title')    t.text = elem.find('Title').text    # ElementTree does not store parent references -- an element can    # exist in multiple trees. It's not necessary to use deepcopy here.    for c in elem.findall('Copyright'):       r.append(h)    return rcontext = cet.iterparse('copyright.xml', events=('end','start'))context = iter(context)event, root = context.next()for event, elem in context:    if elem.tag == 'Record' and event =='end':        result = serialize_cet(elem)        out.write(cet.tostring(result, encoding='utf-8'))        root.clear()

?

有关迭代模式的更多信息,请参阅 ElementTree 文档 “Incremental Parsing”(参见 参考资料 获得链接)。

快速查找元素

完成解析后,最常见的 XML 任务是在解析后的树中查找特定的数据。lxml 提供了简化的搜索语法和完整的 XPath 1.0 等各种方法。作为用户,您应当了解每种方法的性能特征和优化技巧。

避免使用 findfindall

findfindall 方法继承自 ElementTree API,可使用简化的类似 XPath 的表达式语言(称为 ElementPath)查找一个或多个后代节点。从 ElementTree 迁移过来的用户可以继续使用 find/ElementPath 语法。

lxml 提供了另外两种查找子节点的选项:iterchildren/iterdescendants 方法和真正的 XPath。如果表达式需要匹配一个节点名,那么使用 iterchildreniterdescendants 方法以及其可选的标记参数,这要比使用 ElementPath 表达式快很多(有时速度会快上两倍)。

对于更复杂的模式,可以使用 XPath 类预编译搜索模式。使用标记参数(例如 etree.XPath("child::Title"))模拟 iterchildren 行为的简单模式的执行时间与 iterchildren 是相同的。但是,预编译仍然非常重要。在每次执行循环时编译模式或对元素使用 xpath() 方法(参见 参考资料 中 lxml 文档的描述),几乎比与只编译一次然后反复使用模式慢 2 倍。

lxml 中的 XPath 计算非常。如果只需要对一部分节点进行序列化,那么在检查所有节点之前使用精确的 XPath 表达式限制条件,这样效果会好很多。例如,限制示例序列化使其只包括含有 night 单词的标题,如 清单 8 所示,这只需序列化完整数据所用的时间的 60%。


清单 8. 使用 XPath 类进行有条件的序列化

def write_if_node(out, node):    if node is not None:        out.write(etree.tostring(node, encoding='utf-8'))def serialize_with_xpath(elem, xp1, xp2):    '''Take our source <Record> element and apply two pre-compiled XPath classes.    Return a node only if the first expression matches.    '''    r = etree.Element('Record')    t = etree.SubElement(r, 'Title')    x = xp1(elem)    if x:        t.text = x[0].text        for c in xp2(elem):            r.append(deepcopy(c))        return rxp1 = etree.XPath("child::Title[contains(text(), 'night')]")xp2 = etree.XPath("child::Copyright")out = open('out.xml', 'w')context = etree.iterparse('copyright.xml', events=('end',), tag='Record')fast_iter(context,    lambda elem: write_if_node(out, serialize_with_xpath(elem, xp1, xp2)))

?

在文档的其他部分查找节点

注意,即使使用了 iterparse,仍然可以根据当前的节点 使用 XPath 谓词。要查找后面紧跟一个记录(记录的标题包含单词 night)的所有 <Record> 节点,则执行以下操作:

etree.XPath("Title[contains(../Record/following::Record[1]/Title/text(), 'night')]")

?

然而,如果使用 清单 4 描述的节省内存的迭代策略,该命令将不会返回任何内容,因为解析完整个文档时将删除前面的节点:

etree.XPath("Title[contains(../Record/preceding::Record[1]/Title/text(), 'night')]")

?

虽然可以编写有效的算法来解决这一问题,但是对于那些需要跨节点进行分析的任务(特别是那些随机分布在文档中的节点),使用使用 XQuery(比如 eXist)的 XML 数据库更加适合。

提高性能的其他方法

除了使用 lxml 内部 的特定方法外,还可以通过库以外的方法提高执行速度。其中一些方法只需要修改一下代码;而另一些方法则需要重新考虑如何处理大型数据。

Psyco

Psyco 模块常常被忽略,但是它可以通过较少的工作提高 Python 应用程序的速度。一个纯 Python 程序的典型性能收益是普通程序的 2 至 4 倍,但是 lxml 使用 C 语言完成了大部分工作,因此它们之间的差别非常小。当我在启用 Psyco 的情况下运行 清单 4 时,运行时间仅仅减少了 3 秒(43.9 秒对 47.3 秒)。Psyco 需要很大的内存开销,如果机器进入交换,它甚至会抵销 Python 获得的任何性能。

如果由 lxml 驱动的应用程序包含频繁执行的核心纯 Python 代码(可能是对文本节点执行的大量字符串操作),那么仅对这些方法启用 Psyco 可能会有好处。有关 Psyco 的更多信息,参见 参考资料。

线程化

相反,如果应用程序主要依赖内部的、C 驱动的 lxml 特性,那么可能适合将它作为多处理环境下的线程化应用程序运行。关于如何启动线程有很多限制 — 对 XSLT 而言尤其如此。要了解更多内容,可参考 lxml 文档中有关线程的 FAQ 部分。

拆分解决

如果可以将特别大的文档分解为单个的、可分析的子树,那么就可以在子树级别上分解文档(使用 lxml 的快速序列化),并将工作分布到位于多台计算机中的这些文件。对于执行 CPU 密集型的脱机任务,使用随需应变的虚拟服务器正成为一种日益流行的解决方案。可以获得 Python 程序员用于设置和管理 Amazon 虚拟 Elastic Compute Cloud (EC2) 集群的详细指南。参见 参考资料 了解更多信息。

适合大型 XML 任务的一般策略

本文给出的具体代码示例可能并不适合您的项目,但是对于 GB 级或以上的 XML 数据,请考虑以下的原则(已通过测试和 lxml 文档的验证):

使用迭代解析策略,渐进式地处理大型文档。 如果需要随机地搜索整个文档,那么使用索引式 XML 数据库。只选择需要的数据。如果只对特定的节点感兴趣,使用按名字选择的方法。如果需要谓词语法,那么尝试可用的 XPath 类和方法。 考虑手头的任务和开发人员的舒适程度。如果不需要考虑速度的话,lxml 的对象化或 Amara 等对象模型对于 Python 开发人员来说可能更自然。cElementTree 在只需要进行解析时才会体现出速度优势。 花些时间做些非常简单的基准测试。在处理数百万条记录时,细微的差别就会累积起来,但是并不能总是很明显地看出哪种方法最有效。

结束语

很多软件产品都附带了 pick-two 警告,表示在速度、灵活性或可读性之间只能选择其中两种。然而,如果得到合理使用,lxml 可以满足全部三个要求。那些希望提高 DOM 性能或使用 SAX 事件驱动模型的 XML 开发人员现在有机会获得更高级的 Python 库。拥有 Python 背景的开发人员在刚开始接触 XML 时也可以轻松地利用 XPath 和 XSLT 的表达能力。这两种编程风格可以在一个基于 lxml 的应用程序中和谐共存。

本文只介绍了 lxml 的一小部分功能。请查看 lxml.objectify 模块,它主要针对那些较小的数据集或对 XML 的依赖不是强的应用程序。对于不具备良好格式的 HTML 内容,lxml 提供了两个有用的包:lxml.html 模块和 BeautifulSoup 解析器。如果要编写能够从 XSLT 调用的 Python 模块,或创建定制的 Python 或 C 扩展,还可以扩展 lxml。可以从 参考资料 中的 lxml 文档中找到有关所有这些内容的信息。

?

?

http://www.ibm.com/developerworks/cn/xml/x-hiperfparse/index.html?ca=drs-cn-0105

读书人网 >XML SOAP

热点推荐