Zum Hauptinhalt springen

HOWTO: Große Dateien verarbeiten mit Standard-Python

 

Vorgefertigte Datensätze, die den Rahmen sprengen

Häufig werde ich mit bereitgestellten Rohdaten für Analysen konfrontiert, welche sich unkomprimiert durchaus auf Dateien von einem halben Gigabyte oder mehr erstrecken. Ab einem Gigabyte kommen die Desktop-gestützten Statistik-Tools langsam ins Schwitzen. Es gibt natürlich je nach Tool Möglichkeiten, nur einen Teil der Spalten zu selektieren oder nur die ersten 10.000 Zeilen zu laden usw.

Aber was macht man, wenn man aus der Datenlieferung nur eine zufällige Stichprobe ziehen möchte? Man darf sich nie darauf verlassen, dass die Datei zufällig sortiert ist. Sie kann durch Prozesse im Datenbankexport bereits systematische Reihenfolgeeffekte beinhalten. Es kann aber auch vorkommen, dass man z.B. nur ein Zehntel einer Gruppierung analysieren möchte, wie etwa die Einkäufe jedes zehnten Kunden. Dazu muss die komplette Datei gelesen werden, sonst kann man nie sicherstellen, dass alle Einkäufe der gefilterten Kunden berücksichtigt wurden.

Zerlegen von Dateien mit Python-Scripts

 

Probleme herkömmlicher Tools

Nun, das Öffnen solcher Dateien unter Windows mit diversen Text-Editoren oder gar Excel ist selten von richtigem Erfolg gekrönt, da die komplette Datei erstmal in das Tool geladen werden muss. Das Programm verweigert dabei irgendwann den Dienst, schneidet die Datei ab oder braucht zumindest sehr lange.

 

Naive Lösung mit Python-Script

Ich habe es zuletzt mal wieder mit einem kurzen Python-Script gelöst. Nehmen wir mal den Versuch, die Datei einfach stumpf in 3 große Partitionen aufzuspalten.

Zunächst müssen wir die Datei öffnen. Das passiert über einen sogenannten Filehandle, mit dem wir die Datei ansprechen können. Die simple Herangehensweise, die häufig auch funktioniert, ist, den Inhalt zu lesen, ihn in Zeilen zu trennen, diese aufzuteilen und ihn dann wieder zu speichern. Etwa so:

fileHandle = open('datensatz.csv', 'r')
content = fileHandle.read()
content = content.split('\n')
newFile01 = []
newFile02 = []
newFile03 = []

Jetzt haben wir erstmal eine Liste mit allen Zeilen und drei leere Listen für die Zieldateien. Ich nehme mal die einfache Herangehensweise und teile die Zeilen gleichmäßig auf die neuen Dateien auf:

counter=0
for line in content:
    if counter % 3 == 0:
        newFile01.append(line)
    elif counter % 3 == 1:
        newFile02.append(line)
    elif counter % 3 == 2:
        newFile03.append(line)
    counter+=1
 file = open('newFile01.csv', 'w')
 file.write('\n'.join(newFile01))
 file.close()
 file = open('newFile02.csv', 'w')
 file.write('\n'.join(newFile02))
 file.close()
 file = open('newFile03.csv', 'w')
 file.write('\n'.join(newFile03))
 file.close()

 

Siehe da, es hat geklappt!

Der Code ist jedoch extrem holzig. Zunächst sollten wir das "with"-Statement verwenden, dann müssen wir die Dateien nicht mehr explizit schließen. Dieses Literal bewirkt, dass bei getaner Arbeit ein Aufräumprozess gestartet wird, vor allem auch, wenn ein Fehler auftritt im With-Bereich. Im Falle der Filehandles gehört zum Aufräumen das Schließen der Dateien. Das schützt davor, dass Prozesse abbrechen, aber die Dateien trotzdem als "in Arbeit" gesperrt sind. Viel hässlicher ist noch, dass wir mit read() die komplette Datei in den Arbeitsspeicher lesen! Die Testdatei für dieses Script hatte 650MB und der Arbeitsspeicher wurde mit etwa 1,6 Gigabyte belastet!! Hier kann man auch einfach die Datei Zeile für Zeile lesen und verarbeiten! Nächster Anlauf:

with open('datensatz.csv', 'r') as fileHandle: 
    counter=0
    newFile01, newFile02, newFile03 = [], [], []
    for line in fileHandle:
        if counter % 3 == 0:
            newFile01.append(line)
        elif counter % 3 == 1:
            newFile02.append(line)
        elif counter % 3 == 2:
            newFile03.append(line)
        counter += 1

with open('newFile01.csv', 'w') as file01:
    file01.write('\n'.join(newFile01))
with open('newFile02.csv', 'w') as file02:
    file02.write('\n'.join(newFile02))
with open('newFile03.csv', 'w') as file03:
    file03.write('\n'.join(newFile03))

Das sieht schon besser aus, ist insgesamt kürzer und läuft schmäler im Arbeitsspeicher, braucht aber immer noch knapp einen Gigabyte. Kein Wunder, in den Listen newFile01, newFile02 und newFile03 steckt ja wieder die gesamte Datei für einen gewissen Zeitraum. Das geht noch besser, indem wir direkt in die Dateien schreiben. Es ist übrigens ein oft übersehenes Feature der Print-Funktion in Python3, dass man das Ausgabeziel wählen kann. Per default ist es immer die console, es kann aber auch eine Datei sein.

with open('datensatz.csv', 'r') as fileHandle, open('newFile01.csv', 'w') as file01, open('newFile02.csv', 'w') as file02, open('newFile03.csv', 'w') as file03:
    counter = 0 
    for line in fileHandle: 
        if counter % 3 == 0: 
            print(line, file = file01) 
        if counter % 3 == 1: 
            print(line, file = file02) 
        if counter % 3 == 2: 
            print(line, file = file03) 
        counter+=1

So, das ganze ist noch kürzer und überstieg im Lauf nie 16MB im Arbeitsspeicher! D.h. hier kann man auch bedenkenlos Dateien verarbeiten, welche den Arbeitsspeicher schnell sprengen würden! Ich hätte hier jetzt keine Angst, Files mit hunderten von Gigabyte zu verarbeiten.

Jetzt haben wir immer noch nicht das Szenario gelöst, wenn wir z.B. nach Kundennummern trennen wollen, das behandle ich vielleicht in einem separaten Post. Genauso das Problem, dass der Datenheader jetzt nur in der ersten Datei zu finden ist.

PS: Es geht übrigens noch kürzer, aber dann ist wohl Schluss:

with open('datensatz.csv', 'r') as fileHandle, open('newFile01.csv', 'w') as file01, open('newFile02.csv', 'w') as file02, open('newFile03.csv', 'w') as file03: 
    files, counter = (file01, file02, file03), 0 
    for line in fileHandle: 
        print(line, file = files[counter % 3]) 
        counter+=1

 

Stefan Seltmann
Dein Ansprechpartner
Stefan Seltmann
Lead Expert
Stefan liebt das Programmieren, vor allem rund um Data Engineering und Data Science, und arbeitet quasi in seinem Hobby. Gerade für Softwareentwicklung mit Python und/oder Spark punktet er als b.telligents Telefonjoker.
#CodeFirst, #TestMore, #CodeDoctor