使用 Python Iterparse 处理大型 XML 文件
- 2025-02-27 09:05:00
- admin 原创
- 54
问题描述:
我需要用 Python 编写一个解析器,可以在没有太多内存(只有 2 GB)的计算机上处理一些非常大的文件(> 2 GB)。我想在 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”后,我需要做一些事情来清理空子项。有人能给我一些建议吗?在处理完数据后,我可以做些什么来正确清理?
解决方案 1:
尝试一下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('''\n <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()
解决方案 2:
iterparse()
让您在构建树的同时执行一些操作,这意味着除非您删除不再需要的内容,否则最终您仍然会得到整棵树。
更多信息请阅读ElementTree 原始实现的作者撰写的文章(但也适用于 lxml)
解决方案 3:
为什么不使用sax的“回调”方法?
解决方案 4:
请注意,iterparse 仍会构建一棵树,就像 parse 一样,但您可以在解析时安全地重新排列或删除树的某些部分。例如,要解析大型文件,您可以在处理完元素后立即删除它们:
`for event, elem in iterparse(source):
if elem.tag == "record":
... process record elements ...
elem.clear()`
上述模式有一个缺点;它不会清除根元素,因此最终会得到一个带有大量空子元素的单个元素。如果您的文件很大,而不仅仅是很大,这可能是一个问题。要解决这个问题,您需要掌握根元素。最简单的方法是启用启动事件,并将对第一个元素的引用保存在变量中:
获得一个可迭代对象
context = iterparse(source, events=("start", "end"))
将其变成迭代器
context = iter(context)
获取根元素
event, root = context.next()
for event, elem in context:
if event == "end" and elem.tag == "record":
... process record elements ...
root.clear()
所以这是一个增量解析的问题,这个链接可以给你详细的答案,总结一下答案,你可以参考上面的内容
解决方案 5:
根据我的经验,无论是否使用 iterparse element.clear
(参见F. Lundh和 L. Daly),都无法始终处理非常大的 XML 文件:一段时间内运行良好,突然内存消耗急剧上升,出现内存错误或系统崩溃。如果您遇到同样的问题,也许可以使用相同的解决方案:expat 解析器。另请参阅F. Lundh或以下使用 OP 的 XML 片段的示例(加上两个变音符以检查是否存在编码问题):
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')
outfile.write(data + '
')
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')
输入.xml:
<root>
<item>
<title>Item 1</title>
<desc>Description 1ä</desc>
</item>
<item>
<title>Item 2</title>
<desc>Description 2ü</desc>
</item>
</root>
输出.txt:
Description 1ä
Description 2ü
解决方案 6:
root.clear() 方法的唯一问题是它返回 NoneTypes。这意味着您无法编辑使用 replace() 或 title() 等字符串方法解析的数据。也就是说,如果您只是按原样解析数据,这是一种最佳方法。
扫码咨询,免费领取项目管理大礼包!