Как итеративно анализировать огромный XML-файл с помощью Python и XSLT-файла и записывать в CSV - PullRequest
0 голосов
/ 15 февраля 2019

Я не могу сгладить и преобразовать файл XML в CSV с помощью XSLT при работе с большими файлами XML.

В настоящее время я анализирую вложенный файл XML с помощью lxml, используя файл XSLчтобы сгладить вывод, а затем я записываю вывод в файл CSV.

Мой XML выглядит примерно так:

<root>
    <level1>
        <level2>
            <topid>1</topid>
            <level3>
                <subtopid>1</topid>
                <level4>
                    <subid>1</id>
                    <descr>test</descr>
                </level4>
                <level4>
                    <subid>2</id>
                    <descr>test2</descr>
                </level4>
                ...
            </level3>
            ...
        </level2>
    </level1>
</root>

Я хочу получить следующий файл CSV:

topid,subtopid,subid,descr
1,1,1,test
1,1,2,test2
....

Мой XSLT:

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="utf-8" use-character-maps="map"/>
<xsl:character-map name="map">
    <xsl:output-character character="," string=" "/>
</xsl:character-map>

<xsl:strip-space elements="*"/>
<xsl:variable name="delimiter" select="','"/>
<xsl:variable name="newline" select="'&#xd;'" />

<xsl:template match="/root">
    <xsl:text>topid,subtopid,subid,descr</xsl:text>
    <xsl:value-of select="$newline" />

    <xsl:for-each select="level1/level2/level3/level4">
        <xsl:value-of select="ancestor::root/level1/level2/topid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="ancestor::root/level1/level2/level3/subtopid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="subid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="descr" />
        <xsl:value-of select="$newline" />
    </xsl:for-each>
</xsl:template>

Мой код Python:

import lxml.etree as ET

xsltfile = ET.XSLT(ET.parse('transactions.xsl'))
xmlfile  = ET.parse('myxmlfile.xml')
output   = xsltfile(xmlfile).write_output('output.csv')

Это прекрасно работает для небольших файлов, но теперь я хочусделать то же самое с XML-файлом + - 2,5 ГБ.Использование etree.parse загрузит его в память, которая, очевидно, не будет работать с большими файлами.

Я хочу где-то выполнить итерацию, поэтому я не загружаю файл XML в память и пишу в строку CSV для строки, все еще используя XSLT для преобразования.Я использую файл XSLT, потому что это единственный способ, которым я знаю (сейчас), как сгладить вложенный файл XML.

Ответы [ 3 ]

0 голосов
/ 15 февраля 2019

Я бы предпочел использовать XSLT 3.0 (или даже 2.0!) В Python, но у меня не было времени выяснить, как использовать Saxon / C.

Другой вариант - использовать iterparse().

Пример ...

XML Input (исправлено, чтобы быть правильно сформированным и добавить секунду level3 для тестирования)

<root>
    <level1>
        <level2>
            <topid>1</topid>
            <level3>
                <subtopid>1</subtopid>
                <level4>
                    <subid>1</subid>
                    <descr>test</descr>
                </level4>
                <level4>
                    <subid>2</subid>
                    <descr>test2</descr>
                </level4>
            </level3>
            <level3>
                <subtopid>2</subtopid>
                <level4>
                    <subid>1</subid>
                    <descr>test</descr>
                </level4>
                <level4>
                    <subid>2</subid>
                    <descr>test2</descr>
                </level4>
            </level3>
        </level2>
    </level1>
</root>

Python

from lxml import etree
import csv

context = etree.iterparse("test.xml", events=("start", "end"))

fields = ("topid", "subtopid", "subid", "descr")

with open("test.csv", "w", newline="", encoding="utf8") as xml_data_to_csv:

    csv_writer = csv.DictWriter(xml_data_to_csv, fieldnames=fields,
                                delimiter=",", quoting=csv.QUOTE_MINIMAL)

    csv_writer.writeheader()

    topid = None
    subtopid = None
    values = {}

    for event, elem in context:
        tag = elem.tag
        text = elem.text

        if tag == "topid" and text:
            topid = text

        if tag == "subtopid" and text:
            subtopid = text

        if tag == "subid" and text:
            values["subid"] = text

        if tag == "descr" and text:
            values["descr"] = text

        if event == "start" and tag == "level4":
            # Build a dict containing all of the "fields" with default values of "Unknown".
            values = {key: "Unknown" for key in fields}

        if event == "end" and tag == "level4":
            values["topid"] = topid
            values["subtopid"] = subtopid
            csv_writer.writerow(values)

        elem.clear()

Выход CSV

topid,subtopid,subid,descr
1,1,1,test
1,1,2,test2
1,2,1,test
1,2,2,test2
0 голосов
/ 27 февраля 2019

Saxon / C и python могут работать:

Один пользователь успешно использовал Boost.Python для взаимодействия с библиотекой C ++.

Другой пользователь сделал взаимодействие по-другому: https://github.com/ajelenak/pysaxon

0 голосов
/ 15 февраля 2019

Одной из возможностей является использование потоковой передачи XSLT 3.0.Здесь есть две проблемы:

(а) сделать ваш код пригодным для обработки.Мы не можем судить, насколько это сложно, не увидев код таблицы стилей.

(b) установка и запуск потокового процессора XSLT 3.0.Это зависит от того, насколько вы привязаны к среде Python.Если это нужно сделать в Python, вы можете попробовать установить Saxon / C.Альтернативой является обращение к другой среде, в этом случае у вас есть больше возможностей, например, вы можете запустить Saxon-EE на Java.

ПОЗЖЕ

Глядя накод, который вы опубликовали, довольно странный

<xsl:for-each select="level1/level2/level3/level4">
    <xsl:value-of select="ancestor::root/level1/level2/topid" />

Я подозреваю, что вы хотите вывести topid "текущего" элемента level2, но это не то, что он делает (в XSLT 1.0 этобудет печатать значение первого level2/topic, в XSLT 2.0+ - будет печатать значения всех элементов level2/topic. Я подозреваю, что вы действительно хотите что-то вроде этого:

    <xsl:for-each select="level1/level2/level3/level4">
        <xsl:value-of select="ancestor::level2/topid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="ancestor::level3/subtopid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="subid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="descr" />
        <xsl:value-of select="$newline" />
    </xsl:for-each>

Это почти что потокно не совсем. Потоковая передача не позволяет вам вернуться к элементам topid и subtopid. Самый простой способ сделать его пригодным для обработки - это сохранить самые последние значения этих элементов в аккумуляторах:

<xsl:accumulator name="topid" as="xs:string" initial-value="''">
  <xsl:accumulator-rule match="topid/text()" select="string(.)"/>
</xsl:accumulator>

<xsl:accumulator name="subtopid" as="xs:string" initial-value="''">
  <xsl:accumulator-rule match="subtopid/text()" select="string(.)"/>
</xsl:accumulator>

и затем получить доступ к значениям как:

    <xsl:for-each select="level1/level2/level3/level4">
        <xsl:value-of select="accumulator-before('topid')" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="accumulator-before('subtopid')" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="subid" />
        <xsl:value-of select="$delimiter" />
        <xsl:value-of select="descr" />
        <xsl:value-of select="$newline" />
    </xsl:for-each>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...