使用SAX

时间:2019-10-28 12:05:06

标签: python xml sax

我需要解析非常大的XML文件(3-5GB范围),该文件必须根据XML节点中包含的数据分成几个较小的XML文件。

每个输入文件都包含数十万个<measure>元素,就像这个(非常)简化的片段中一样。

    <items>
        <measure code="0810">
            <condition sequ="001" SID="-5041162"/>
            <footnote Id="00550"/>
            <footnote Id="00735"/>
        </measure>
        <measure code="6304">
            <component Id="01" national="1"/>
            <footnote Id="00001"/>
        </measure>
        <measure code="0811">
            <condition sequ="002" SID="-5041356"/>
            <footnote Id="00555"/>
        </measure>
        <measure code="2915">
            <component Id="01" national="0"/>
            <certif SID="-737740"/>
            <certif SID="-737780"/>
        </measure>
    </items>

实际<measure>元素的内容几乎可以是任何格式正确的XML。

我在解析这些文件时需要执行两个过程:

  1. <measure>元素的内容中提取信息,然后转储 它到MongoDB数据库(此部分已解决...)
  2. 基于以下内容将原始XML文件分区为100个XML子文件: 每个<measure>的“ code”属性的前两位数字 节点。也就是说,新的100个XML文件(名为“ part_00.xml” 需要创建“ part_99.xml”),并且每个<measure>元素都必须是 附加到相应的子文件。 即示例中的<measure>块1和3应该复制到'part_08.xml',块2应该复制到'part_63.xml'...

我正在使用SAX解析原始文件,并且上面的进程1运行良好。 SAX流程的纯框架是:

    import sys
    from xml.sax import ContentHandler
    from xml.sax import make_parser

    class ParseMeasures(ContentHandler):
        code = ''

        def startElement(self, name, attrs):
            if name == 'measure':
                self.code = attrs.get('code')

        def endElement(self, name):
            if name == 'measure':
                print('***Must append <measure> block to file part_{0}.xml'.format(self.code[:2]))

    def main(args):
        handler = ParseMeasures()
        sax_parser = make_parser()
        sax_parser.setContentHandler(handler)
        sax_parser.parse('my_large_xml.file.xml')
        print('Ended')

    if __name__ == '__main__':
        main(sys.argv[1:])

我需要的是能够访问'endElement()'中的整个<measure> XML元素,以将其附加到相应的子文件中。

是否可以将SAX与其他XML解析功能结合使用,从而可以在'endElement()'中获取整个<measure> XML元素? (我可以处理子文件的创建和管理……这不是问题!)

首先,也许在这种情况下SAX方法不是最合适的吗?

“唯一”的警告是该进程应处理3-5GB范围内的输入文件...

2 个答案:

答案 0 :(得分:2)

以下是一种混合解决方案,该解决方案使用内置的SAX解析器生成解析事件,并使用lxml生成部分树(仅<measure>元素,一次仅包含一个)。

一旦构建了元素,lxml的API for incremental XML generation就会根据@code的值将其序列化为不同的文件。

此代码可处理<measure>元素内的任何嵌套级别以及包括空格在内的文本值。它目前不处理注释,处理指令或名称空间,但可以添加对它们的支持。

即使输入文件很大,内存消耗也应保持较低水平。 lxml将增加一些开销,但是迭代编写支持非常方便。总体上来说,完全手动完成此操作会更快,但也会更加复杂。

from xml.sax import ContentHandler, make_parser
from lxml import etree

class ParseMeasures(ContentHandler):
    def __init__(self):
        self.stack = []
        self.open = False
        self.elem = None
        self.writers = {}
        self.text = []

    def _get_writer(self, filename):
        with etree.xmlfile(filename) as xf:
            with xf.element('items'):
                while True:
                    el = (yield)
                    xf.write(el)
                    xf.flush()  # maybe don't flush *every* write

    def _write(self):
        grp = self.elem.attrib['code'][0:2]

        if grp in self.writers:
            writer = self.writers[grp]
        else:
            writer = self.writers[grp] = self._get_writer('part_%s.xml' % grp)
            next(writer)        # run up to `yield` and wait

        writer.send(self.elem)  # write out current `<measure>`
        self.elem = None

    def _add_text(self):
        if self.elem is not None and self.text:
            if self.open:
                self.elem.text = ''.join(self.text)
            else:
                self.elem.tail = ''.join(self.text)
            self.text = []

    def startElement(self, name, attrib):
        if self.stack or name == 'measure':
            self._add_text()
            self.open = True
            self.elem = etree.Element(name, attrib)
            self.stack.append(self.elem)
            if len(self.stack) > 1:
                self.stack[-2].append(self.elem)

    def characters(self, content):
        if self.elem is not None:
            self.text.append(content)

    def endElement(self, name):
        if self.stack:
            self._add_text()
            self.open = False
            self.elem = self.stack.pop()            
            if not self.stack:
                self._write()

    def endDocument(self):
        # clean up
        for writer in self.writers:
            self.writers[writer].close()


def main():
    sax_parser = make_parser()
    sax_parser.setContentHandler(ParseMeasures())
    sax_parser.parse(r'test.xml')

if __name__ == '__main__':
    main()

这会生成part_08.xml

<items>
    <measure code="0810">
        <condition sequ="001" SID="-5041162"/>
        <footnote Id="00550"/>
        <footnote Id="00735"/>
    </measure>
    <measure code="0811">
        <condition sequ="002" SID="-5041356"/>
        <footnote Id="00555"/>
    </measure>
</items>

part_29.xml

<items>
    <measure code="2915">
        <component Id="01" national="0"/>
        <certif SID="-737740"/>
        <certif SID="-737780"/>
    </measure>
</items>

part_63.xml

<items>
    <measure code="6304">
        <component Id="01" national="1"/>
        <footnote Id="00001"/>
    </measure>
</items>

答案 1 :(得分:1)

下面(输出的是3个文件名为“ part_zz.xml”的文件)

请注意,下面的解决方案不使用任何外部库。

import sys
import xml.etree.ElementTree as ET
from collections import defaultdict
from xml.sax import ContentHandler
from xml.sax import make_parser


class ParseMeasures(ContentHandler):
    def __init__(self):
        self.data = defaultdict(list)
        self.code = None

    def startElement(self, name, attrs):
        if name == 'measure':
            self.code = attrs.get('code')[:2]
        if self.code:
            self.data[self.code].append((name, attrs._attrs))

    def endDocument(self):
        for k, v in self.data.items():
            root = None
            for entry in v:
                if entry[0] == 'measure':
                    if not root:
                        root = ET.Element('items')
                    measure = ET.SubElement(root, 'measure')
                temp = ET.SubElement(measure, entry[0])
                temp.attrib = entry[1]
            tree = ET.ElementTree(root)
            tree.write('part_{}.xml'.format(k), method='xml')


def main(args):
    handler = ParseMeasures()
    sax_parser = make_parser()
    sax_parser.setContentHandler(handler)
    sax_parser.parse('my_large_xml.file.xml')


if __name__ == '__main__':
    main(sys.argv[1:])

输出示例('part_08.xml')

<items>
    <measure>
        <measure code="0810"/>
        <condition SID="-5041162" sequ="001"/>
        <footnote Id="00550"/>
        <footnote Id="00735"/>
    </measure>
    <measure>
        <measure code="0811"/>
        <condition SID="-5041356" sequ="002"/>
        <footnote Id="00555"/>
    </measure>
</items>