Python中解析大型XML文档的最快方法是什么?

75

我目前正在运行基于《Python Cookbook》第12.5章的以下代码:

from xml.parsers import expat

class Element(object):
    def __init__(self, name, attributes):
        self.name = name
        self.attributes = attributes
        self.cdata = ''
        self.children = []
    def addChild(self, element):
        self.children.append(element)
    def getAttribute(self,key):
        return self.attributes.get(key)
    def getData(self):
        return self.cdata
    def getElements(self, name=''):
        if name:
            return [c for c in self.children if c.name == name]
        else:
            return list(self.children)

class Xml2Obj(object):
    def __init__(self):
        self.root = None
        self.nodeStack = []
    def StartElement(self, name, attributes):
        element = Element(name.encode(), attributes)
        if self.nodeStack:
            parent = self.nodeStack[-1]
            parent.addChild(element)
        else:
            self.root = element
        self.nodeStack.append(element)
    def EndElement(self, name):
        self.nodeStack.pop()
    def CharacterData(self,data):
        if data.strip():
            data = data.encode()
            element = self.nodeStack[-1]
            element.cdata += data
    def Parse(self, filename):
        Parser = expat.ParserCreate()
        Parser.StartElementHandler = self.StartElement
        Parser.EndElementHandler = self.EndElement
        Parser.CharacterDataHandler = self.CharacterData
        ParserStatus = Parser.Parse(open(filename).read(),1)
        return self.root

我正在处理大约1 GB大小的XML文档。 有人知道更快的解析方法吗?


2
你的问题过于模糊,难以得到有用的答案。请考虑回答以下问题:
  • 您需要使用这个1GB的XML文档做什么?
  • 您需要这个解析器有多快的速度?
  • 您是否可以懒惰地迭代文档,而不是一开始就把所有内容加载到内存中?
- Matt
2
我需要将所有数据加载到内存中,对数据进行索引,然后“浏览”和处理它。 - Jeroen Dirks
显然,PyRXP非常快。他们声称它是最快的解析器,但cElementTree不在他们的统计列表中。 - Matthew Schinckel
8个回答

79

看起来你的程序并不需要使用 DOM 功能。我建议你使用 (c)ElementTree 库。如果你使用 cElementTree 模块的 iterparse 函数,你可以逐步遍历 XML 并在事件发生时处理它们。

不过,请注意 Fredrik 的建议,使用 cElementTree 的 iterparse 函数

对于解析大文件,你可以在处理完元素后立即将其丢弃:

for event, elem in iterparse(source):
    if elem.tag == "record":
        ... process record elements ...
        elem.clear()

上述模式有一个缺点,它不清除根元素,因此您最终将得到一个只有许多空子元素的单个元素。如果您的文件非常大,而不仅仅是大,这可能会成为一个问题。要解决这个问题,您需要获取根元素。最简单的方法是启用开始事件,并将对第一个元素的引用保存在变量中。
# get an iterable
context = iterparse(source, events=("start", "end"))

# turn it into an iterator
context = iter(context)

# get the root element
event, root = context.next()

for event, elem in context:
    if event == "end" and elem.tag == "record":
        ... process record elements ...
        root.clear()

lxml.iterparse()不支持这样做。

在Python 3.7上无法使用上述方法,请考虑以下获取第一个元素的方式。

import xml.etree.ElementTree as ET

# Get an iterable.
context = ET.iterparse(source, events=("start", "end"))
    
for index, (event, elem) in enumerate(context):
    # Get the root element.
    if index == 0:
        root = elem
    if event == "end" and elem.tag == "record":
        # ... process record elements ...
        root.clear()

2
context.next() 更改为 context.__next__() 可以解决第二个示例的问题(Python 3)。 - Eric Reed
不明白为什么在第三个例子中需要执行root.clear(),因为它在任何地方都没有被使用? - soulrider
以下代码适用于Python 3.9: parser = ET.iterparse(stream, events=("end",)) event, root = next(parser) result.handleElement(root) for event, element in parser: # .. 在此处处理元素 ... root.clear() - J. Beattie
在这些代码片段中,source是什么?所有链接都已经失效了。 - MikeB
@MikeB:刚得知Fredrik Lundh去年去世了,所以他的网站已经不存在了:( 我已经从WebArchive更新了链接。 - Steen
显示剩余4条评论

17

你尝试过使用cElementTree模块吗?

cElementTree在Python 2.5及以后版本中包含在xml.etree.cElementTree中。请参考基准测试

需要注意的是,自从Python 3.3起,cElementTree已被用作默认实现,因此在Python版本3.3及以上版本中不需要进行此更改。

移除了失效的ImageShack链接


11

我建议你使用lxml,它是一个针对libxml2库的Python绑定,速度非常快。

根据我的经验,libxml2和expat的性能非常相似。但是我更喜欢libxml2(以及Python中的lxml),因为它似乎正在更积极地开发和测试。此外,libxml2具有更多的功能。

lxml在很大程度上与xml.etree.ElementTree兼容。并且其网站上有良好的文档。


7
注册回调函数会极大地减缓解析速度。[编辑]这是因为(快速的)C代码必须调用Python解释器,而Python解释器不像C那样快速。基本上,您使用C代码来读取文件(快速),然后在Python中构建DOM(慢)。[/编辑]
尝试使用xml.etree.ElementTree,它完全由C实现,可以在不使用任何回调到Python代码的情况下解析XML。
文档解析完成后,您可以对其进行过滤以获取所需内容。
如果仍然太慢,并且您不需要DOM,则另一种选择是将文件读入字符串,然后使用简单的字符串操作进行处理。

这是非常误导性的建议。基于回调的XML解析器本质上并不慢。此外,OP已经在使用Python的expat绑定,这些绑定也是本地C编写的。 - Matt
2
Python解释器始终比本地编译的C代码慢。正如您在问题中清楚地看到的那样,它正在注册Python代码以便为每个元素调用!而且这段代码还要做很多工作! - Aaron Digulla
2
这个应该被提高,Python中的回调非常慢,你要避免这种情况,尽可能多地在C语言环境中完成。 - Johan Dahlin

5
如果您的应用程序对性能敏感,并且可能会遇到大文件(如您所说,> 1GB),那么基于您在问题中展示的代码,我强烈建议不要使用它,因为它将整个文档加载到内存中。如果有可能,我建议您重新考虑设计,以避免一次性将整个文档树保存在RAM中。我不知道您的应用程序需求是什么,因此无法适当地建议任何特定方法,除了通用的建议尝试使用“基于事件”设计。

谢谢!我正在尝试做类似的事情,但是想使用ElementTree的iterparse方法,但是内存仍然在增加,你知道为什么吗? - Alon Samuel

1

expat ParseFile 如果您不需要将整个树存储在内存中,那么它可以很好地工作,否则对于大文件,它迟早会消耗您的RAM:

import xml.parsers.expat
parser = xml.parsers.expat.ParserCreate()
parser.ParseFile(open('path.xml', 'r'))

它将文件分块读取,并在不耗尽内存的情况下将它们提供给解析器。
文档:https://docs.python.org/2/library/pyexpat.html#xml.parsers.expat.xmlparser.ParseFile

1

我花了相当长的时间尝试了一些方法,发现使用lxml和iterparse是最快且最少占用内存的方法,但需要确保释放不必要的内存。在我的例子中,解析arXiv转储文件:

from lxml import etree

context = etree.iterparse('path/to/file', events=('end',), tag='Record')

for event, element in context:
    record_id = element.findtext('.//{http://arxiv.org/OAI/arXiv/}id')
    created = element.findtext('.//{http://arxiv.org/OAI/arXiv/}created')

    print(record_id, created)

    # Free memory.
    element.clear()
    while element.getprevious() is not None:
        del element.getparent()[0]

所以element.clear并不足够,还需要删除任何与先前元素的链接。

0
在 Python3 中,您应该更改语法
而不是这样
# get the root element
event, root = context.next()

尝试这个(就像在Iterparse对象没有next属性中推荐的那样)

# get the root element
event, root = next(context)

这一行是不必要的

# turn it into an iterator
context = iter(context)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接