3

我是 xml 解析的新手,我一直试图找出一种跳过父元素内容的方法,因为有一个嵌套元素在其文本属性中包含大量数据(我无法更改此文件的方式生成)。这是 xml 的示例:

<root>
    <Parent>
        <thing_1>
            <a>I need this</a>
        </thing_1>
        <thing_2>
            <a>I need this</a>
        </thing_2>
        <thing_3>
            <subgroup>
                <huge_thing>enormous string here</huge_thing>
            </subgroup>
        </thing_3>
    </Parent>
    <Parent>
        <thing_1>
            <a>I need this</a>
        </thing_1>
        <thing_2>
            <a>I need this</a>
        </thing_2>
        <thing_3>
            <subgroup>
                <huge_thing>enormous string here</huge_thing>
            </subgroup>
        </thing_3>
    </Parent>
</root>

我已经尝试了 lxml.iterparse 和 xml.sax 实现来尝试解决这个问题,但没有骰子。这些是我在搜索中找到的大部分答案:

  1. 在 iterparse 中使用 tag 关键字。

    这不起作用,因为尽管 lxml 在后台清理了元素,但元素中的大文本仍然被解析到内存中,所以我得到了很大的内存峰值。

  2. 如果找到该元素的开始事件,则创建一个将其设置为 True 的标志,然后在解析中忽略该元素。

    这不起作用,因为元素在结束事件时仍被解析到内存中。

  3. 在到达特定元素的结束事件之前中断。

    当我到达元素时,我不能只是打破,因为有多个这些元素需要特定的子数据。

  4. 这是不可能的,因为流解析器仍然有一个结束事件并生成完整的元素。

    ... 好的。

我目前正在尝试直接编辑 GzipFile 发送到 iterparse 的流数据,希望它甚至不知道该元素存在,但我遇到了问题。任何方向将不胜感激。

4

2 回答 2

2

我认为您不能让解析器选择性地忽略它正在解析的 XML 的某些部分。这是我使用 SAX 解析器的发现...

我获取了您的示例 XML,将其放大到不到 400MB,创建了一个 SAX 解析器,并以两种不同的方式针对我的 big.xml 文件运行它。

  • 对于直接的方法,sax.parse('big.xml', MyHandler())内存达到 12M 的峰值。
  • 对于缓冲文件读取器方法,使用 4K 块parser.feed(chunk),内存峰值为 10M。

然后我将大小翻了一番,对于一个 800M 的文件,双向重新运行,峰值内存使用量没有改变,~10M。SAX 解析器似乎非常有效。

我针对您的示例 XML 运行此脚本以创建一些非常大的文本节点,每个节点 400M。

with open('input.xml') as f:
    data = f.read()

with open('big.xml', 'w') as f:
    f.write(data.replace('enormous string here', 'a'*400_000_000))

这是 big.xml 的大小(以 MB 为单位):

du -ms big.xml 
763     big.xml

这是我的 SAX ContentHandler,它仅在数据父级的路径以结尾thing_*/a(根据您的示例不合格huge_thing)时才处理字符数据...

顺便说一句,非常感谢l4mpi这个答案,展示了如何缓冲你想要的字符数据:

from xml import sax

class MyHandler(sax.handler.ContentHandler):
    def __init__(self):
        self._charBuffer = []

        self._path = []

    def _getCharacterData(self):
        data = ''.join(self._charBuffer).strip()
        self._charBuffer = []
        return data.strip()  # remove strip() if whitespace is important

    def characters(self, data):
        if len(self._path) < 2:
            return

        if self._path[-1] == 'a' and self._path[-2].startswith('thing_'):
            self._charBuffer.append(data)

    def startElement(self, name, attrs):
        self._path.append(name)

    def endElement(self, name):
        self._path.pop()

        if len(self._path) == 0:
            return

        if self._path[-1].startswith('thing_'):
            print(self._path[-1])
            print(self._getCharacterData())

对于整个文件解析方法和分块阅读器,我得到:

thing_1
I need this
thing_2
I need this
thing_3

thing_1
I need this
thing_2
I need this
thing_3

thing_3由于我的简单逻辑,它正在打印,但其中的数据subgroup/huge_thing被忽略了。

以下是我使用直接方法调用处理程序的parse()方法:

handler = MyHandler()
sax.parse('big.xml', handler)

当我使用 Unix/BSD time运行它时,我得到:

/usr/bin/time -l ./main.py
...
        1.45 real         0.64 user         0.11 sys
...
            11027456  peak memory footprint

下面是我如何使用更复杂的分块读取器调用处理程序,使用 4K 块大小:

handler = MyHandler()
parser = sax.make_parser()
parser.setContentHandler(handler)

Chunk_Sz = 4096
with open('big.xml') as f:
    chunk = f.read(Chunk_Sz)
    while chunk != '':
        parser.feed(chunk)
        chunk = f.read(Chunk_Sz)
/usr/bin/time -l ./main.py
...
        1.85 real         1.65 user         0.19 sys
...
            10453952  peak memory footprint

即使使用 512B 的块大小,它也不会低于 10M,但运行时间翻了一番。

我很想知道你得到了什么样的表现。

于 2022-01-25T08:34:58.260 回答
0

您不能使用 DOM 解析器,因为每个定义都会将整个文档填充到 RAM 中。但基本上 DOM 解析器只是一个 SAX 解析器,它在通过 SAX 事件时创建一个 DOM。

在创建自定义 SAX 解析器时,您实际上不仅可以创建 DOM(或您喜欢的任何其他内存表示),还可以开始忽略与文档中某些特定位置相关的事件。

请注意解析需要继续,以便您知道何时停止忽略事件。但是解析器的输出不会包含这个不需要的大块数据。

于 2022-01-26T12:09:21.633 回答