Riesige Text- oder XML-Dateien zerteilen mit Python

Manchmal ist es einfacher, große Datenmengen nicht auf einmal zu verarbeiten, sondern sie zuerst in kleine Stücke zu zerteilen. Ein Beispiel, das eine Teilung notwendig macht, ist das Umwandeln von XML mit XSLT, das bei sehr großen Dateien zu viel Speicher beansprucht.

Das Unix-Kommando split kann große Textdateien in kleine zerlegen, aber für manche Anwendungsfälle wie die Teilung von sehr großen XML-Dateien ist das nicht ausreichend. Es gelten dabei folgende Anforderungen:

  • Die Trennung darf nur an bestimmten Stellen erfolgen (d.h. zumeist hinter bestimmten schließenden XML-Tags).
  • Aber nicht an jeder dieser möglichen Trennstellen muss auch tatsächlich getrennt werden.
  • Ein Header/Footer (mit äußeren XML-Tags) muss in jedem Teil erhalten bleiben bzw. kopiert werden.

Das split-Kommando ist dafür zu unflexibel, da es entweder nach einer bestimmten Anzahl Zeilen oder an definierten Trennstellen teilt, aber keine Kombination aus beidem zulässt und außerdem die automatische Übernahme von Kopf- und Fußzeilen nicht zulässt.

Folgendes Python-Skript erledigt den Job (getestet mit Python 3.6):

import os,sys,getopt

def usage():
    print("Usage: zrhkstk.py -i inputfile -h headlines -t taillines -p pattern -n lines")
    

def tail_file(qfile, nlines):
    qfile.seek(0, os.SEEK_END)
    endf = position = qfile.tell()
    linecnt = 0
    while position >= 0:
        qfile.seek(position)
        next_char = qfile.read(1)
        if next_char == "\n" and position != endf-1:
            linecnt += 1

        if linecnt == nlines:
            break
        position -= 1

    if position < 0:
        qfile.seek(0)

    return qfile.read(), position

try:
   opts, args = getopt.getopt(sys.argv[1:],"i:h:t:p:n:")
except getopt.GetoptError:
   usage()
   sys.exit(2)

infilename = ''
headlines = 1
taillines = 1
pattern = ''
nlines = 100000
prefix = 'part'

try:
    for opt, arg in opts:
        if (opt == '-i'):
            infilename = arg
        elif (opt == '-h'):
            headlines = int(arg)
        elif (opt == '-t'):
            taillines = int(arg)
        elif (opt == '-p'):
            pattern = arg
        elif (opt == '-n'):
            nlines = int(arg)
except:
    usage()
    sys.exit(2)

infile = open(infilename, 'r')
tail, tailpos = tail_file(infile, taillines)
infile.seek(0,0)
head = ''

for n in range(0,headlines):
    line = infile.readline()
    head = head + line

nfile = 1

while True:
    outfilename = "part"+str(nfile)+"-"+infilename
    with open(outfilename, 'w') as outfile:
        outfile.write(head)
        n = 0
        while True:
            line = infile.readline()
            if (line==''):
                # EOF
                infile.close()
                outfile.close()
                sys.exit()
            outfile.write(line)
            n = n + 1
            if (n > nlines and pattern in line):
                break
        outfile.write(tail)
    nfile = nfile + 1

Wenn z.B. ein riesiges XML-File nur hinter schließenden </record>-Tags getrennt werden darf, frühestens nach 100000 Zeilen getrennt werden soll und 2 Kopf- und 1 Fußzeile in jeden Teil übernommen werden sollen, reicht ein Aufruf von:

python3 zrhkstk.py -i input.xml -p "</record>" -h 2

Die Namen der Ausgabedateien beginnen mit „part“ gefolgt von einer Nummer, einem Trennstrich und anschließend dem Namen der Eingabedatei.

Für die Funktion tail_file, die ähnlich wie das Unix-Kommando tail die letzten n Zeilen einer Datei findet, habe ich mich bei einem Skript bedient, das in einem Kommentar auf github gepostet wurde.