lxml解析 python
Python从未遭受过XML库不足的困扰。 从2.0版开始,它包含了熟悉的xml.dom.minidom
以及相关的pulldom和XML的简单API(SAX)模型。 从2.4开始,它包含了流行的ElementTree API。 另外,总是有第三方库提供更高级别或更多的pythonic接口。
尽管任何XML库都足以用于简单的文档对象模型(DOM)或小型文件的SAX解析,但是开发人员越来越面临更大的数据集,并且需要在Web服务上下文中实时解析XML。 同时,经验丰富的XML开发人员可能会更喜欢XML本机语言,例如XPath或XSLT,因为它们具有紧凑性和表达能力。 可以访问XPath的声明性语法,同时保留Python中可用的通用功能,这是理想的选择。
lxml是第一个展示高性能特性的Python XML库,它包括对XPath 1.0,XSLT 1.0,自定义元素类甚至Pythonic数据绑定接口的原生支持。 它基于两个C库建立: libxml2
和libxslt
。 它们提供了解析,序列化和转换这些核心任务背后的大部分功能。
您在代码中使用lxml的哪些部分取决于您的需求:您对XPath满意吗? 您喜欢使用类似Python的对象吗? 您在系统上有多少内存可用于保留大树?
本文不介绍lxml的全部内容,而是演示了有效处理超大型XML文件,针对高速和低内存使用进行优化的技术。 使用了两个免费的示例文档:Google转换为XML的美国版权续订数据和Open Directory RDF内容。
在这里,仅将lxml与cElementTree进行比较,而不与其他数十个可用的Python库进行比较。 我选择cElementTree是因为它是Python 2.5的本机部分,并且像lxml一样是基于C库构建的。
XML库通常是为小样本文件设计的,并在其中进行了测试。 实际上,许多现实世界的项目是在没有完整数据可用的情况下开始的。 程序员使用示例内容并编写如清单1所示的代码,努力工作数周或数月。
from lxml import etree
doc = etree.parse('content-sample.xml')
lxml parse
方法读取整个文档并构建一个内存树。 与cElementTree相比,lxml树要昂贵得多,因为它保留了有关节点上下文的更多信息,包括对父节点的引用。 以这种方式解析2G文档会立即将具有2G RAM的计算机进行交换,从而带来灾难性的性能影响。 如果在假定该数据将在内存中可用的情况下编写整个应用程序,则将进行主要重构。
当不希望或不希望构建内存树时,请使用不依赖于读取整个源文件的迭代解析技术。 lxml提供了两种方法:
iterparse
方法 目标解析器方法是熟悉SAX事件驱动代码的开发人员所熟悉的。 目标解析器是实现以下方法的类:
start
射击。 元素的数据和子元素尚不可用。 end
射击。 元素的所有子节点(包括文本节点)现在都可用。 data
在子文本上触发,并有权访问该文本。 close
射击。 清单2演示了创建一个实现所需方法的目标解析器类(这里称为TitleTarget
)。 该解析器在内部列表( self.text
)中收集Title
元素的文本子级,并在到达close()
方法后返回该列表。
Title
标记的所有文本子代的列表 class TitleTarget(object):
def __init__(self):
self.text = []
def start(self, tag, attrib):
self.is_title = True if tag == 'Title' else False
def end(self, tag):
pass
def data(self, data):
if self.is_title:
self.text.append(data.encode('utf-8'))
def close(self):
return self.text
parser = etree.XMLParser(target = TitleTarget())
# This and most other samples read in the Google copyright data
infile = 'copyright.xml'
results = etree.parse(infile, parser)
# When iterated over, 'results' will contain the output from
# target parser's close() method
out = open('titles.txt', 'w')
out.write('\n'.join(results))
out.close()
当对照版权数据运行时,此代码的时间为54秒。 目标解析可以相当快,并且不会生成消耗内存的解析树,但是所有事件都会触发数据中的所有元素。 对于非常大的文档,例如在本示例中,当仅关注几个元素时,这可能是不希望的。 是否可以将处理限制为选定的标签并获得更好的性能?
iterparse
方法 lxml的iterparse
方法是ElementTree API的扩展。 iterparse
返回用于所选元素上下文的Python迭代器。 它接受两个有用的参数:要监视的事件的元组和标记名。 在这种情况下,我只对
的文本内容感兴趣(在到达end
事件时可用)。 从输出清单3将是相同的,在目标解析器方法的清单2但应该是更快,因为LXML可以优化事件处理内部。 它也减少了很多代码行。
context = etree.iterparse(infile, events=('end',), tag='Title')
for event, elem in context:
out.write('%s\n' % elem.text.encode('utf-8'))
如果运行此代码并监视输出,您会看到它首先非常快速地添加标题,但很快就会缓慢地进行爬网。 快速检查top
发现计算机已更换:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
170 root 15 -5 0 0 0 D 3.9 0.0 0:01.32 kswapd0
这是怎么回事? 尽管iterparse
并不会消耗整个文件,但它不会从每次迭代中释放对节点的引用。 当将重复访问整个文档时,这是一项功能。 但是,在这种情况下,我宁愿在每个循环结束时回收该内存。 这既包括对已经处理的子节点或文本节点的引用,也包括当前节点的先前同级,它们对根节点的引用也被隐式保留,如清单4所示 :
for event, elem in context:
out.write('%s\n' % elem.text.encode('utf-8'))
# It's safe to call clear() here because no descendants will be accessed
elem.clear()
# Also eliminate now-empty references from the root node to
while elem.getprevious() is not None:
del elem.getparent()[0]
为了方便起见,我将清单4重构为一个函数,该函数采用可调用func
对当前节点执行操作,如清单5所示 。 我将在后续示例中使用此方法。
func
然后清理不需要的引用的func
def fast_iter(context, func):
for event, elem in context:
func(elem)
elem.clear()
while elem.getprevious() is not None:
del elem.getparent()[0]
del context
清单4中的这种优化的iterparse
方法产生的输出与清单2中的目标解析器产生的输出相同,但时间却减少了一半。 当任务仅限于特定事件和标记名时,它甚至比cElementTree还要快,如此处所示。 (不过,在大多数情况下,当解析是主要活动时,cElementTree的性能将优于lxml。)
表1显示了针对基准侧边栏中描述的计算机上的版权数据衡量的各种解析器技术的时间。
提取text()
XML库 | 方法 | 平均时间,以秒为单位 |
---|---|---|
cElementTree | Iterparse | 32 |
xml文件 | 目标解析器 | 54 |
xml文件 | 优化的iterparse | 25 |
在Open Directory数据上运行清单4中相同的iterparse
方法,每次运行花费122秒,比解析版权数据花费的时间长五倍。 由于Open Directory数据的大小也略微超过其五倍(为1.9 GB),因此即使在非常大的文件上,您也应该从这种方法获得大致线性的时间性能。
如果您对XML文件所做的全部工作是从单个节点中获取一些文本,则可以使用简单的正则表达式,该正则表达式的运行速度可能比任何XML解析器都要快。 但是,实际上,当数据非常复杂时,这几乎是不可能实现的,我不建议这样做。 当需要真正的数据操作时,XML库是无价的。
lxml擅长将XML序列化为字符串或文件,因为它直接依赖于libxml2
C代码。 如果您的任务根本不需要任何序列化,则lxml是一个明确的选择,但是有一些技巧可以使库发挥最佳性能。
deepcopy
lxml保留子节点与其父节点之间的引用。 这样的效果是lxml中的一个节点可以只有一个父节点。 (cElementTree没有父节点的概念。)
清单6取得了版权文件中的每个
,并编写了一个仅包含标题和版权信息的简化记录。
from lxml import etree
import deepcopy
def serialize(elem):
# Output a new tree like:
#
# This title
# date id
#
# 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
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 r
out = open('titles.xml', 'w')
context = etree.iterparse('copyright.xml', events=('end',), tag='Record')
# Iterate through each of the nodes using our fast iteration method
fast_iter(context,
# For each , serialize a simplified version and write it
# to the output file
lambda elem:
out.write(
etree.tostring(serialize(elem), encoding='utf-8')))
不要使用deepcopy
来简单地复制单个节点的文本。 创建一个新节点,手动填充其text属性,然后对其进行序列化,速度更快。 在我的测试中,为
和
调用deepcopy
速度比清单6中的代码慢15%。 序列化大型后代树时,您将看到deepcopy
的最大性能提升。
使用清单7中的代码对cElementTree进行基准测试时,lxml的序列化器几乎快了一倍(50秒对95秒):
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 r
context = 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文档的“增量解析”。 (请参阅相关主题中的链接。)
解析之后,最常见的XML任务是在解析的树中定位感兴趣的特定数据。 lxml提供了几种方法,从简化的搜索语法到完整的XPath 1.0。 作为用户,您应该了解每种方法的性能特征和优化技术。
find
和findall
从ElementTree API继承的find
和findall
方法使用一种称为ElementPath的简化的类似于XPath的表达式语言来查找一个或多个后代节点。 从ElementTree迁移到lxml的用户自然可以继续使用find / ElementPath语法。
lxml提供了两个用于发现子节点的选项: iterchildren
/ iterdescendants
方法和真实的XPath。 在表达式应该与节点名称匹配的情况下,与等效的ElementPath表达式相比,使用iterchildren
或iterdescendants
方法及其可选的tag参数要快得多(在某些情况下是两倍)。
对于更复杂的模式,请使用XPath
类预编译搜索模式。 简单的模式,模仿的行为iterchildren
与标签的参数(例如, etree.XPath("child::Title")
执行的有效,同时作为他们iterchildren
等价物。 不过,进行预编译很重要。 在循环的每次执行中或在元素上使用xpath()
方法(在lxml文档中描述,请参阅参考资料 )编译模式的速度几乎是一次编译然后重复使用该模式的速度的两倍。
lxml中的XPath评估速度很快 。 如果只需要序列化节点的一个子集,则最好是预先限制精确的XPath表达式,而不是以后检查所有节点。 例如,将示例序列化限制为仅包含包含单词night
标题,如清单8所示 ,需要60%的时间来序列化整个集合。
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 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 r
xp1 = 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
的记录的所有
节点,请执行以下操作:
etree.XPath("Title[contains(../Record/following::Record[1]/Title/text(), 'night')]")
但是,当使用清单4中描述的内存有效的迭代策略时,此命令将不返回任何内容,因为在进行文档解析时会清除前面的节点:
etree.XPath("Title[contains(../Record/preceding::Record[1]/Title/text(), 'night')]")
尽管可以编写一种有效的算法来解决此特定问题,但涉及跨节点分析的任务(尤其是那些可能随机分布在文档中的任务)通常更适合使用XQuery的XML数据库,例如eXist。
除了在 lxml中使用特定方法外,您还可以使用库外的方法来影响执行速度。 其中一些是简单的代码更改。 其他人则需要有关如何处理大数据问题的新思路。
Psyco模块是通过最少的工作来提高Python应用程序速度的常用方法。 一个纯Python程序的典型收益是两倍到四倍,但是lxml在C语言中完成了大部分工作,因此差异很小。 在启用Psyco的情况下运行清单4时,我仅将运行时间减少了三秒钟(43.9秒对47.3秒)。 Psyco的内存开销很大,如果计算机必须进行交换,它甚至可能抵消任何收益。
如果您的lxml驱动的应用程序具有经常执行的核心纯Python代码(也许在文本节点上进行了广泛的字符串操作),则仅对这些方法启用Psyco可能会有所帮助。 有关Psyco的更多信息,请参阅相关主题 。
相反,如果您的应用程序主要依赖于内部C驱动的lxml功能,那么在多处理器环境中将其作为线程应用程序运行可能对您有利。 在如何启动线程方面存在一些限制,尤其是使用XSLT时。 有关更多信息,请查阅lxml文档中有关线程的FAQ部分。
如果可以将非常大的文档划分为可单独分析的子树,则可以在子树级别拆分文档(使用lxml的快速序列化),然后将这些文件上的工作分配到多台计算机上,这是可行的。 使用按需虚拟服务器是一种越来越流行的解决方案,用于执行中央处理器(CPU)绑定的脱机任务。
此处呈现的特定代码示例可能不适用于您的项目,但是当面对以GB或更大为单位的XML数据时,请考虑一些原则(由测试和lxml文档证明):
许多软件产品附带两个选择警告,这意味着您只能选择两个:速度,灵活性或可读性。 如果仔细使用,lxml可以提供全部三个。 那些在DOM性能或事件驱动的SAX模型上苦苦挣扎的XML开发人员现在有机会使用更高级别的pythonic库。 来自XML的Python背景的程序员可以轻松地探索XPath和XSLT的表现力。 两种编码样式都可以在基于lxml的应用程序中愉快地共存。
lxml提供的功能比这里探讨的更多。 确保研究lxml.objectify
模块,尤其是对于较小的数据集或主要不是基于XML的应用程序。 对于可能格式不正确HTML内容,lxml提供了两个有用的程序包: lxml.html
模块和BeautifulSoup解析器。 如果编写可从XSLT调用的Python模块或创建自定义Python或C扩展,也可以扩展lxml本身。 查找中提到的LXML文件中有关所有这些信息相关主题 。
翻译自: https://www.ibm.com/developerworks/opensource/library/x-hiperfparse/index.html
lxml解析 python