Pre-Processing von großen XML-Quelldateien vor dem Transformieren - Best Practice?

Hi zusammen,
wie schon in meinem anderen Thread angeschnitten, bin ich gerade am Transformieren von XML zu JSON mittels pyMARC.

Mir liegt dabei ein großer Datenbankabzug als .xml vor: 1 Mio+ Datensätze, Größe: mehrere GB. Die Verarbeitung als Einzeldatei ist mir nicht möglich, weil sie die gängigen RAM-Kapazitäten der mir zur Verfügung stehenden Geräte sprengt.

Es braucht also eine Prozessierung in kleineren Chunks, um den Arbeitsspeicher zu schonen. Dabei auf gängige XML-Parser in Python zurückzugreifen, um damit die XML-Datei zu splitten (Stichwort: XML-Splitter) ist leider ebenso ineffizient, weil es bei dieser Größe zu lange braucht. Spezielle und vor allem proprietäre Software schließe ich aus, weil der Prozess der Vor-Verarbeitung nahtlos an ein Python-Skript angebunden sein soll, das dann die Transformation leistet. Bleiben also die Linux/Unix-Kommandozeilen-Tools, die mit dem Rohtext der XML-Datei arbeiten, ohne diesen zu parsen.

Hier noch mal die Struktur der XML-Quelldatei:

<?xml version="1.0" encoding="ISO-8859-1"?>
<collection xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="abc.xsd">
<record identifier="xyz" status="e" level="p">
[...]
</record>

<record identifier="hij" status="e" level="p">
[...]
</record>

<record identifier="klm" status="e" level="p">
[...]
</record>
</collection>

Mit Anlehnung an eine Herangehensweise aus dem Data Wrangler Handbuch bin ich so vorgegangen:

  1. Entferne alle unnötigen Umbrüche zwischen den Datensätzen
    sed -i '/<\/record>/,/<record/{//!d}' bigdatadump.xml
  2. Ersetze alle Zeilenumbrüche mit einem ungenutzten Zeichen (notwendig für das spätere korrekte Splitten) und speichere als Arbeitskopie
    cat bigdatadump.xml | tr '\n' '\007' > workingfile
  3. Setze einen Zeilenumbruch nach jeden </record> - Ab dann steht jeder Datensatz auf jeweils einer Zeile und lässt sich so später korrekt splitten
    sed -i 's/<\/record>/\0\n/g' workingfile
  4. Entferne alles, was am Anfang der Datei vor dem ersten <record> steht (XML-Header etc.)
    sed -i 's/^<?xml.*<record/<record/' workingfile
  5. Entferne alle Leerzeichen zwischen den Elementen
    sed -i 's/>[\t \x07]*</></g' workingfile
  6. Teile die Datei in 100MB Chunks
    split --line-bytes=100MB workingfile workingfile.
  7. Abschließend: Mache die Zeilenumbruch-Ersetzungen von Schritt 2 überall wieder rückgängig, damit die Dateien wieder normale Zeilenumbrüche haben
for i in workingfile.*; do
    cat $i | tr '\007' '\n' > $i.xml
done

Im Resultat sollte man nun lauter 100MB große XML-Dateien (workingfile.aa.xml, workingfile.ab.xml, workingfile.ac.xml etc.) haben, die insich geschlossene Datensätze enthalten und dann Stück für Stück transformiert werden können. Würde man nur Schritt 6 (split) direkt auf die XML-Quelldatei anwenden, wäre die Datei zwar auch in Chunks gesplittet, jedoch an willkürlichen Stellen, wodruch die XML-Struktur gebrochen und damit fehlerhaft ist.

:warning: Leider scheitert der oben von mir beschriebene Ansatz in meinem Fall immer noch an der Größe der XML-Datei. Bei Schritt 3 wird der folgende Fehler geworfen: „sed: regex input buffer length larger than INT_MAX“

:grey_question: Daher die Frage, an alle ETL-Erfahrenen hier: Wie geht ihr mit großen XML-Datenabzügen um, wenn ihr diese Transformieren wollt? Wie komme ich hier bei extrem großen Quelldateien mit Kommadozeilen-Tools weiter?

Vielen Dank im Voraus.

Ich würde anstelle von sed et al. das Splitten in Python mit Hilfe eines SAX-Parsers oder StAX-Parsers implementieren.

1 „Gefällt mir“

yaz-marcdump hat eine split-Funktion, vielleicht ein Versuch wert? Habe ich vor vielen Jahren viel verwendet, so in der Art:
yaz-marcdump -v -s chunk_ -C 10000 bigmarcfile

2 „Gefällt mir“

@dmaus
Wie schon im Eingangspost erwähnt, scheiden in meinem Fall sämtliche XML-Parser mit Python aus, weil die zu lange (= Wochen) beim Splitten/Parsen eines solchen großen Datenbankabzugs brauchen. Zumindest ist das bisher meine Einschätzung nach einem Text mit lxml. Habe dafür auf split_xml_file_by_tags.py von ChameleonTartu zurückgegriffen:

from lxml import etree

XML_FILE = 'bigdatadump.xml'

# Change your tag here by which to split
SPLIT_BY_TAG = 'record'

# Add folder where to store splitted files
FOLDER_FOR_SPLIT_FILES = 'sliced/el_{0}.xml'

doc = etree.parse(XML_FILE)
tags = len([_ for _ in doc.findall(SPLIT_BY_TAG)])

for tag_index in range(tags):
    xml_doc = etree.parse(XML_FILE)
    for index, elem in enumerate(xml_doc.findall(SPLIT_BY_TAG)):
        if tag_index != index:
            elem.getparent().remove(elem)

    with open(FOLDER_FOR_SPLIT_FILES.format(tag_index), 'w') as f:
        f.write(etree.tostring(xml_doc).decode())

Falls du ein performantes Python-Skript für das Splitten von einer MARCXML-Datei mit Millionen Datensätzen hast, bitte gerne teilen :smile:

@phu
Danke, das hört sich gut an, leider wirft yaz-marcdump beim Chunken immer nur diesen selben Fehler, ohne das weiter etwas passiert:

yaz-marcdump -v -s chunk_ -C 1000 bigdatadump.xml
<!-- Skipping bad byte 60 (0x3C) at offset 0 (0x0) -->
<!-- Skipping bad byte 63 (0x3F) at offset 1 (0x1) -->
<!-- Skipping bad byte 120 (0x78) at offset 2 (0x2) -->
<!-- Skipping bad byte 109 (0x6D) at offset 3 (0x3) -->
<!-- Skipping bad byte 108 (0x6C) at offset 4 (0x4) -->
<!-- Skipping bad byte 32 (0x20) at offset 5 (0x5) -->
<!-- Skipping bad byte 118 (0x76) at offset 6 (0x6) -->
<!-- Skipping bad byte 101 (0x65) at offset 7 (0x7) -->
<!-- Skipping bad byte 114 (0x72) at offset 8 (0x8) -->
<!-- Skipping bad byte 115 (0x73) at offset 9 (0x9) -->
<!-- Skipping bad byte 105 (0x69) at offset 10 (0xa) -->
<!-- Skipping bad byte 111 (0x6F) at offset 11 (0xb) -->
<!-- Skipping bad byte 110 (0x6E) at offset 12 (0xc) -->
<!-- Skipping bad byte 61 (0x3D) at offset 13 (0xd) -->
<!-- Skipping bad byte 34 (0x22) at offset 14 (0xe) -->
<!-- Bad Length 10 read at offset 15 (f) -->

Habe nun auf zwei verschiedenen Ubuntu-Instanzen (Ubuntu 22.04 LTS nativ und auf WSL) schon verschiedene XML-Dateien, meine und auch MARCXML-Beispieldateien von LoC und DNB ausprobiert, mit verschiedenen Encodings (UTF-8 und ISO-8859-1) und Zeilenenden (Windows, Unix, Mac).

YAZ version: 5.31.1 c3cea881e3e7e80b069ddd1429994e58841acb14

Weißt du da weiter oder hat sonst wer eine Ahnung?

Wie schon im Eingangspost erwähnt, scheiden in meinem Fall sämtliche XML-Parser mit Python aus, weil die zu lange (= Wochen) beim Splitten/Parsen eines solchen großen Datenbankabzugs brauchen. Zumindest ist das bisher meine Einschätzung nach einem Text mit lxml. Habe dafür auf split_xml_file_by_tags.py von ChameleonTartu zurückgegriffen:

Das verwendete etree erzeugt eine Baumstruktur für das gesamte XML-Dokument. Klar, dass ist bei sehr großen Dateien keine gute Idee und ein klarer Performancekiller.

Ein SAX-Parser funktioniert anders. Er liest das XML-Dokument als Datenstrom und erzeugt Parserereignisse. Die lassen sich wieder in die lexikalische Form von XML und in eine Ausgabedatei schreiben. Für den Anwendungsfall Splitten wird auch kaum eine eigene Datenhaltung benötigt, so dass für Speicherverbrauch und Geschwindigkeit maßgeblich das Lesen und Schreiben von Bytes verantwortlich sein wird.

1 „Gefällt mir“

Ah, das geht nur mit mrc, nicht mit marcxml. Ich hatte es nicht mehr so richtig in Erinnerung.
Du müsstest erstmal konvertieren:
yaz-marcdump -v -i marcxml -o marc bigdatadump.xml > bigdatadump.mrc

Das Splitten der mrc habe ich eben mit einer ca. 7 GB großen GND-Datei mit ca 6 Mio. Datensätzen ausprobiert, es hat etwa 8 Minuten gedauert.
Siehe https://data.dnb.de/GND/ (die authorities-gnd-person_dnbmarc_20240213.mrc.gz entpackt)

1 „Gefällt mir“

@dmaus
Danke, das war richtig. Nachdem ich es mal in Python mit der SAX Library ausprobiert habe, hat das sich als sehr perfomant herausgestellt. For future reference kann ich xml_split.py von benallard & scnctech empfehlen. Das funktioniert out of the box mit folgendem Command
python xml_split.py bigdatadump.xml -M <split file size in kb> -o <output path>
Der einzige Nachtteil dieses Skripts: Beim Splitten kann es passieren, dass ein <record> Datensatz auf zwei XML-Dateien aufgesplittet wird. Alle XML-Header und -Tags sind korrekt geöffnet und geschlossen, nur dass sich eben ein paar Felder und Unterfelder in der einen und die restlichen in der anderen XML-Datei aufteilen. In meinem Use Case muss ich das noch anpassen.

@phu
Danke auch dir für den Hinweis. Das war der Fehler.
Mit der Abfolge XML (.xml) zu MARC (.mrc) zu MARC chunks und dann wieder zu MARCXML hat es geklappt. Im Detail funktioniert es folgendermaßen:

# Create working directory for chunking
mkdir ~/chunks

# 1. Convert to MARC files
yaz-marcdump -v -i marcxml -o marc ~/bigdatadump.xml > ~/bigdatadump.mrc

# 2. Split MARC files into chunks (10000 records per file)
yaz-marcdump -v -s ~/chunks/bigdatadump_ -C 10000 ~/bigdatadump.mrc

# 3. Convert all chunked files back to MARCXML
for i in ~/chunks/bigdatadump_*; do
    yaz-marcdump -v -i marc -o marcxml $i > $i.xml
done

Noch zum Vergleich der Performance: Im Fall meines riesigen Datenbankabzugs hat das Splitten mit SAX/Python 5min und mit yaz-marcdump 25min gedauert.

2 „Gefällt mir“