使用Python Iterparse处理大型XML文件

43
我需要在内存只有 2 GB 的计算机上编写一个 Python 解析器,能够处理一些非常大的文件(>2GB)。我想使用 lxml 中的 iterparse 来完成这个任务。
我的文件格式如下:
<item>
  <title>Item 1</title>
  <desc>Description 1</desc>
</item>
<item>
  <title>Item 2</title>
  <desc>Description 2</desc>
</item>

到目前为止,我的解决方案是:

from lxml import etree

context = etree.iterparse( MYFILE, tag='item' )

for event, elem in context :
      print elem.xpath( 'description/text( )' )

del context

不幸的是,这个解决方案仍然消耗了大量的内存。我认为问题在于在处理每个“ITEM”后,我需要做一些清理空子项的操作。有人能提供一些建议,告诉我在处理完我的数据后应该怎样正确地清理?


作为补充说明,我的描述打印输出只是一个简单的例子,我实际上需要对处理的每个项目内容进行更多的工作,但我想简化它。 - Dave Johnshon
1
在处理单个迭代元素后,您是否尝试调用elem.clear()来回收迭代元素的内存? - Uku Loskit
6个回答

64

尝试使用Liza Daly的fast_iter。在处理元素elem后,它调用elem.clear()来清除子代,并且还会删除前面的兄弟节点。

def fast_iter(context, func, *args, **kwargs):
    """
    http://lxml.de/parsing.html#modifying-the-tree
    Based on Liza Daly's fast_iter
    http://www.ibm.com/developerworks/xml/library/x-hiperfparse/
    See also http://effbot.org/zone/element-iterparse.htm
    """
    for event, elem in context:
        func(elem, *args, **kwargs)
        # 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 elem
        for ancestor in elem.xpath('ancestor-or-self::*'):
            while ancestor.getprevious() is not None:
                del ancestor.getparent()[0]
    del context


def process_element(elem):
    print elem.xpath( 'description/text( )' )

context = etree.iterparse( MYFILE, tag='item' )
fast_iter(context,process_element)

Daly的文章非常值得一读,特别是当你处理大型XML文件时。


编辑:上面发布的fast_iter是Daly的fast_iter的修改版本。在处理一个元素后,它更积极地删除不再需要的其他元素。

下面的脚本展示了这种行为上的差异。要特别注意的是,orig_fast_iter没有删除A1元素,而mod_fast_iter删除了它,从而节省了更多内存。

import lxml.etree as ET
import textwrap
import io

def setup_ABC():
    content = textwrap.dedent('''\
      <root>
        <A1>
          <B1></B1>
          <C>1<D1></D1></C>
          <E1></E1>
        </A1>
        <A2>
          <B2></B2>
          <C>2<D></D></C>
          <E2></E2>
        </A2>
      </root>
        ''')
    return content


def study_fast_iter():
    def orig_fast_iter(context, func, *args, **kwargs):
        for event, elem in context:
            print('Processing {e}'.format(e=ET.tostring(elem)))
            func(elem, *args, **kwargs)
            print('Clearing {e}'.format(e=ET.tostring(elem)))
            elem.clear()
            while elem.getprevious() is not None:
                print('Deleting {p}'.format(
                    p=(elem.getparent()[0]).tag))
                del elem.getparent()[0]
        del context

    def mod_fast_iter(context, func, *args, **kwargs):
        """
        http://www.ibm.com/developerworks/xml/library/x-hiperfparse/
        Author: Liza Daly
        See also http://effbot.org/zone/element-iterparse.htm
        """
        for event, elem in context:
            print('Processing {e}'.format(e=ET.tostring(elem)))
            func(elem, *args, **kwargs)
            # It's safe to call clear() here because no descendants will be
            # accessed
            print('Clearing {e}'.format(e=ET.tostring(elem)))
            elem.clear()
            # Also eliminate now-empty references from the root node to elem
            for ancestor in elem.xpath('ancestor-or-self::*'):
                print('Checking ancestor: {a}'.format(a=ancestor.tag))
                while ancestor.getprevious() is not None:
                    print(
                        'Deleting {p}'.format(p=(ancestor.getparent()[0]).tag))
                    del ancestor.getparent()[0]
        del context

    content = setup_ABC()
    context = ET.iterparse(io.BytesIO(content), events=('end', ), tag='C')
    orig_fast_iter(context, lambda elem: None)
    # Processing <C>1<D1/></C>
    # Clearing <C>1<D1/></C>
    # Deleting B1
    # Processing <C>2<D/></C>
    # Clearing <C>2<D/></C>
    # Deleting B2

    print('-' * 80)
    """
    The improved fast_iter deletes A1. The original fast_iter does not.
    """
    content = setup_ABC()
    context = ET.iterparse(io.BytesIO(content), events=('end', ), tag='C')
    mod_fast_iter(context, lambda elem: None)
    # Processing <C>1<D1/></C>
    # Clearing <C>1<D1/></C>
    # Checking ancestor: root
    # Checking ancestor: A1
    # Checking ancestor: C
    # Deleting B1
    # Processing <C>2<D/></C>
    # Clearing <C>2<D/></C>
    # Checking ancestor: root
    # Checking ancestor: A2
    # Deleting A1
    # Checking ancestor: C
    # Deleting B2

study_fast_iter()

很好。但是,如果我们指定的元素 tag='item' 不存在且 XML 文件非常大,则会出现相当大的内存积累,而这些内存并没有被释放。我猜测树形结构会不断地增长,并且由于没有触发结束事件,整个 XML 文件都会保存在内存中。有什么解决方法吗? - bioslime
@bioslime:如果你知道某些标签存在并希望清除它们以节省内存,可以使用 iterparse 迭代这些标签,然后在回调函数内调用 iterwalk 来搜索 item 标签。这样,你就可以在仍然保存一些内存的情况下搜索未知标签。但是你仍然必须知道某些标签存在。这里有一个例子,使用了这种方法。 - unutbu
@unutbu:好的,我会看一下。实际上我知道这个格式,但在某些XML中,所有元素的出现都是<item xsi:nil="true"/>而不是<item></item>。目前我做了一个简单的预检查:打开文件,迭代每一行并检查是否有<item>。如果有,就跳出循环。如果没有,我稍后会跳过for event, elem in context - bioslime
@bioslime:你尝试过不进行预检查,直接使用tag='item'吗?iterparse会以任何方式找到这些项,因此当处理item时,fast_iter将清除元素。根据命中率和失败率的比例,以这种方式处理所有XML可能比进行预检查更快。 - unutbu
这样的解决方案如何用于处理项目的子元素?它们似乎在我们使用它们之前被清除了。 - Zach Schulze
显示剩余2条评论

5
< p > iterparse() 允许您在构建树的同时进行操作,这意味着除非删除不再需要的内容,否则最终仍将得到整个树。

< p > 更多信息:阅读原始ElementTree实现的作者(但也适用于lxml)的此文


1
请注意,iterparse仍然会构建一棵树,就像parse一样,但是你可以在解析时安全地重新排列或删除树的部分。例如,为了解析大型文件,您可以在处理完它们后立即摆脱元素:
对于此模式有一个缺点; 它不清除根元素,因此您最终将得到一个具有许多空子元素的单个元素。如果您的文件非常庞大,而不仅仅是大文件,那么这可能是一个问题。要解决此问题,需要获取根元素。最简单的方法是启用开始事件,并将对第一个元素的引用保存在变量中:
获取可迭代对象
将其转换为迭代器
获取根元素
event, root = context.next()

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

这是一个关于增量解析的问题,点击此链接可以得到详细答案。以上是概括的回答。

1
为什么不使用sax的“回调”方法?

1

根据我的经验,使用iterparse时无论是否使用element.clear(参见F. Lundh和L. Daly),都不能始终处理非常大的XML文件:它可以进行一段时间,突然内存消耗量激增并出现内存错误或系统崩溃。如果您遇到相同的问题,也许您可以使用相同的解决方案:使用expat解析器。另请参见F. Lundh或使用OP的XML片段的以下示例(加上两个umlaut以检查是否存在编码问题):

import xml.parsers.expat
from collections import deque

def iter_xml(inpath: str, outpath: str) -> None:
    def handle_cdata_end():
        nonlocal in_cdata
        in_cdata = False

    def handle_cdata_start():
        nonlocal in_cdata
        in_cdata = True

    def handle_data(data: str):
        nonlocal in_cdata
        if not in_cdata and open_tags and open_tags[-1] == 'desc':
            data = data.replace('\\', '\\\\').replace('\n', '\\n')
            outfile.write(data + '\n')

    def handle_endtag(tag: str):
        while open_tags:
            open_tag = open_tags.pop()
            if open_tag == tag:
                break

    def handle_starttag(tag: str, attrs: 'Dict[str, str]'):
        open_tags.append(tag)

    open_tags = deque()
    in_cdata = False
    parser = xml.parsers.expat.ParserCreate()
    parser.CharacterDataHandler = handle_data
    parser.EndCdataSectionHandler = handle_cdata_end
    parser.EndElementHandler = handle_endtag
    parser.StartCdataSectionHandler = handle_cdata_start
    parser.StartElementHandler = handle_starttag
    with open(inpath, 'rb') as infile:
        with open(outpath, 'w', encoding = 'utf-8') as outfile:
            parser.ParseFile(infile)

iter_xml('input.xml', 'output.txt')

input.xml:

<root>
    <item>
    <title>Item 1</title>
    <desc>Description 1ä</desc>
    </item>
    <item>
    <title>Item 2</title>
    <desc>Description 2ü</desc>
    </item>
</root>

output.txt:

Description 1ä
Description 2ü

-1
root.clear() 方法唯一的问题是它返回 NoneTypes。这意味着您不能使用字符串方法(如 replace() 或 title())编辑解析的数据。尽管如此,如果您只是解析原始数据,则这是一种最佳方法。

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