如何释放由lxml.etree使用的内存?

6

我正在使用 lxml.etree 从许多 XML 文件中加载数据,但是在完成初始解析后,我希望关闭它们。目前,在下面的代码中,XML_FILES 列表占用了程序使用的 400 MiB 内存的 350 MiB。我尝试过 del XML_FILESdel XML_FILES[:]XML_FILES = Nonefor etree in XML_FILES: etree = None 等等一些方法,但这些似乎都没有起作用。我也找不到任何关闭 lxml 文件的方法。下面是执行解析的代码:

def open_xml_files():
    return [etree.parse(filename) for filename in paths]

def load_location_data(xml_files):
    location_data = {}

    for xml_file in xml_files:
        for city in xml_file.findall('City'):
            code = city.findtext('CityCode')
            name = city.findtext('CityName')
            location_data['city'][code] = name

        # [A few more like the one above]    

    return location_data

XML_FILES = utils.open_xml_files()
LOCATION_DATA = load_location_data(XML_FILES)
# XML_FILES never used again from this point on

现在,我该如何摆脱这里的XML_FILES?

你是如何确定内存使用量的?也许del确实释放了结构体,但内存由malloc在进程中保留。 - Fred Foo
@larsmans 我使用了memory_profiler。这是一个示例输出:http://hastebin.com/wigevoyafu.py - Underyx
memory_profiler据我所知,存在一个问题,即根据内核而不是malloc来测量进程的内存使用情况。该进程可能会保留内存以供稍后重用。尝试加载XML,然后使用del删除,再次加载并检查内存使用情况是否实际上翻倍。 - Fred Foo
@larsmans 它不会加倍:http://hastebin.com/tageriwuba.py现在,如果您不介意的话,能否稍微详细解释一下这告诉我什么? - Underyx
4个回答

4
我找到的其他解决方案非常低效,但这个方法对我很有效:
def destroy_tree(tree):
    root = tree.getroot()

    node_tracker = {root: [0, None]}

    for node in root.iterdescendants():
        parent = node.getparent()
        node_tracker[node] = [node_tracker[parent][0] + 1, parent]

    node_tracker = sorted([(depth, parent, child) for child, (depth, parent)
                           in node_tracker.items()], key=lambda x: x[0], reverse=True)

    for _, parent, child in node_tracker:
        if parent is None:
            break
        parent.remove(child)

    del tree

4
考虑到第二次解析文件时,如果在解析之间删除了结构(请参见评论),并且内存使用量没有加倍,这里是正在发生的事情:
  • LXML需要内存,因此调用 malloc
  • malloc 需要内存,因此从操作系统请求该内存。
  • del 从Python和LXML的角度来看删除了该结构。但是, malloc 的伙伴 free 实际上没有将内存返还给操作系统。相反,它保留它以服务于未来的请求。
  • 下一次当LXML请求内存时, malloc 会从先前从操作系统获取的同一区域提供内存。

这对于 malloc 实现来说是相当典型的行为。 memory_profiler 仅检查进程的总内存,包括为 malloc 重用而保留的部分。对于使用大的、连续的内存块(例如大的NumPy数组)的应用程序,这是可以的,因为它们实际上会返回给操作系统(*)。但是对于像LXML这样请求大量较小的分配的库, memory_profiler 将给出一个上限,而不是确切的数字。

(*)至少在具有Glibc的Linux上是如此。我不确定MacOS和Windows会做什么。


400MB的内存是很大的,没有一个明智的malloc实现会这样做。 - tbodt

3
你可以考虑使用 etree.iterparse,它使用生成器而不是内存列表。结合生成器表达式,这可能会节省你的程序一些内存。
def open_xml_files():
    return (etree.iterparse(filename) for filename in paths)
iterparse创建一个文件内容解析的生成器,而parse立即解析文件并将内容加载到内存中。内存使用差异是因为iterparse实际上直到调用它的next()方法(在本例中通过for循环)才执行任何操作。 编辑: 显然,iterparse确实逐步工作,但在解析时不释放内存。您可以使用此答案中的解决方案,在遍历xml文档时释放内存。

我曾经认为这在lxml的情况下主要只是语法糖,因为我记得它仍然首先遍历整个文件。你确定这方面它比较好用吗?(很有可能我只是记错了。) - Underyx
1
好的,显然iterparse可以逐步工作,但在解析时不会释放内存。您可能想查看此答案 - Emmett Butler
太好了,谢谢,我会去查看的。不过,我在想:完成解析后清除内存是否更简单?在解析进行时,我可以接受使用这么多内存,即直到我提供的代码中的最后一行。如果我能释放XML_FILES所使用的所有内存,那对我来说就足够了。 - Underyx
iterparse 逐步构建解析树,但除非每次处理部分后都将其修剪,否则它不会在最后生成完整的树。 - Fred Foo
@EmmettJ.Butler 经过大量的重构,我最终使用了您在评论中提供的链接,并且它运行得非常好。请问您是否可以编辑您的答案以包含该链接,这样我就可以接受它了吗?谢谢! - Underyx
我已经编辑了我的答案,包括你使用的解决方案。 - Emmett Butler

1
如何将占用大量内存的代码作为一个独立进程运行,并由操作系统负责释放内存?在您的情况下,可以尝试以下解决方案:
from multiprocessing import Process, Queue

def get_location_data(q):
    XML_FILES = utils.open_xml_files()
    q.put(load_location_data(XML_FILES))

q = Queue()
p = Process(target=get_location_data, args=((q,)))
p.start()
result = q.get() # your location data
if p.is_alive():
    p.terminate()

没有泄漏。分析器只是给出了一个上限。 - Fred Foo
这个方法确实很有效,但似乎有点不太正规,所以我会继续寻找更好的替代方案。无论如何,还是谢谢! - Underyx
祝你好运,尽管我怀疑你会找到任何东西 :) 但如果你找到了,请分享! - Wojciech Walczak

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