Разбиение больших файлов XML на подфайлы в Python с использованием SAX - PullRequest
1 голос
/ 28 октября 2019

Мне нужно проанализировать очень большие XML-файлы (в диапазоне 3-5 ГБ), которые должны разбиваться на несколько меньших 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-подфайлов на основе первых двух цифр атрибута «code» каждого узла <measure>. То есть необходимо создать 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:])

Мне нужно было бы иметь возможность получить доступ ко всему XML-элементу <measure> в endElement (), чтобы добавить его в соответствующий субфайл.

Есть ли способ объединить SAX с другими функциями синтаксического анализа XML, который позволит получить весь элемент XML <measure> в endElement ()? (Я могу справиться с созданием и управлением подфайлами ... Это не проблема!)

Или, может быть, SAX-подход не самый адекватный в этой ситуации, для начала?

Единственное предостережение в том, что процесс должен обрабатывать входные файлы в диапазоне 3-5 Гб ...

Ответы [ 2 ]

2 голосов
/ 28 октября 2019

Ниже приведено гибридное решение, использующее встроенный синтаксический анализатор SAX для генерации событий синтаксического анализа и lxml для построения частичных деревьев (только <measure> элементов и только по одному за раз).

Один разэлемент был построен, он сериализован API-интерфейсом lxml для пошагового генерирования XML в разные файлы в зависимости от значения @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 голос
/ 28 октября 2019

Ниже (выводится 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>
...