Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Vom einfachen Programm zum fertigen Debian-Paket, Fragen rund um Programmiersprachen, Scripting und Lizenzierung.
Antworten
buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 13.09.2023 10:22:39

Mir fehlt es ein wenig an Wissen darüber, wie Python und das OS mit Speicher umgeht, während Dateien gelesen werden. Der hier präsentierte Code funktioniert und verarbeitet nur eine kleine Beispieldatei. Im Original geht es hier aber um CSV-Dateien mit 100 bis 500 Millionen Zeilen und bis zu 30 Spalten. Das funktioniert auch.

Aber hier geht es um ein zusätzliches Feature und ich bin daran interessiert, ob ihr Ideen zur Optimierung habt.
Das "neue" Feature hier ist, dass während dem Einlesen einzelne Zeilen der CSV-Datei ersetzt werden. Nach diversen Einlese versuchen, weiß ich, welche Zeilen "kaputt" sind. Diese ersetze ich während dem Einlesen. Das erspart es mir, an den gelieferten Rohdaten manuell herum editieren zu müssen.

Eine CSV Datei wird über mehrere Zwischenschritte eingelesen und am Ende in ein Pandas DataFrame verwandelt. Die Zwischenschritte dienen der Validierung (im Beispiel-Code nur rudimentär abgebildet). Ein weiterer Schritt ist eben auch, dass eine der Zeilen ersetzt wird. Zeile 3 (mit OID 02) soll ersetzt werden, weil die Anführungszeichen falsch sind, weil diese den zweiten Feldtrenner "verdecken" und das csv Modul in dieser Zeile nur 3 Felder liest.

Code: Alles auswählen

OID;Foo;Bar;Count
01;Suse;Miller;3
02;"Invalid;Name";1
03;Peter;Blow;9
Hier der Code, der mit den 500 Mio. Zeilen wohl etwas "schwierig" wäre.

Code: Alles auswählen

#!/usr/bin/env python3
import io
import csv
from pathlib import Path
import pandas


def get_stream_from_file(fp, replace):
    stream = io.StringIO()

    with fp.open('r') as handle:
        for line in handle:
            print(f'{line=}')
            try:
                line = replace[line]
            except KeyError:
                pass
        
            stream.write(line)

    stream.seek(0)
    return stream


if __name__ ==  "__main__":
    replace = {
        '02;"Invalid;Name";1\n': '02;Invalid;Name;1\n'
    }
 
    stream = get_stream_from_file(Path.cwd() / 'data.csv', replace)


    # Validate the CSV
    for row in csv.reader(stream, delimiter=';'):
        print(f'{row=}')

        # validate row
        if len(row) != 4:
            raise TypeException

    # Get CSV as data
    stream.seek(0)
    data = pandas.read_csv(stream, sep=';')

    print(data.to_markdown())
Die Ausgabe sieht derzeit so aus:

Code: Alles auswählen

line='OID;Foo;Bar;Count\n'
line='01;Suse;Miller;3\n'
line='02;"Invalid;Name";1\n'
line='03;Peter;Blow;9\n'
row=['OID', 'Foo', 'Bar', 'Count']
row=['01', 'Suse', 'Miller', '3']
row=['02', 'Invalid', 'Name', '1']
row=['03', 'Peter', 'Blow', '9']
|    |   OID | Foo     | Bar    |   Count |
|---:|------:|:--------|:-------|--------:|
|  0 |     1 | Suse    | Miller |       3 |
|  1 |     2 | Invalid | Name   |       1 |
|  2 |     3 | Peter   | Blow   |       9 |
Es geht hier nicht um Ausführungsgeschwindigkeit. Dass dies ein paar Stunden dauert, ist nicht mein Problem.
Problematisch wird es nur, wenn das Programm am Ende so viel RAM benötigt, dass es anfängt zu swappen.

Ein Splitten der CSV Dateien ist natürlich eine Option. Aber diese möchte ich vermeiden, weil sie extra Code und Handling erfordert.

Eure Gedanken und Idee dazu würden mich interessieren.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

reox
Beiträge: 2464
Registriert: 06.06.2006 22:09:47
Lizenz eigener Beiträge: MIT Lizenz

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von reox » 13.09.2023 10:32:33

Was du derzeit machst ist
1. Die Datei read-only zu öffnen
2. Zeilenweise einzulesen
3. Die Zeile zu ersetzen und in ein StringIO zu schreiben
4. den StringIO in Pandas zu lesen

Das braucht natürlich genau so viel RAM wie die Datei groß ist, da sie einmal komplett im Speicher landet.

Du hast eigentlich nur zwei Möglichkeiten: Entweder du schreibst eine temporäre Datei oder du editierst die Datei In-Place. Zweiteres ist natürlich schick aber auch gefährlich und wesentlich aufwändiger in der Implementierung.
Nichtdestotrotz: Wenn du die Datei dann in pandas einlesen willst, muss sie in den RAM geladen werden - keine Ahnung ob pandas memmappen kann?

joka63
Beiträge: 24
Registriert: 20.07.2023 23:18:50

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von joka63 » 13.09.2023 21:28:19

reox hat geschrieben: ↑ zum Beitrag ↑
13.09.2023 10:32:33
Was du derzeit machst ist
1. Die Datei read-only zu öffnen
2. Zeilenweise einzulesen
3. Die Zeile zu ersetzen und in ein StringIO zu schreiben
4. den StringIO in Pandas zu lesen

Das braucht natürlich genau so viel RAM wie die Datei groß ist, da sie einmal komplett im Speicher landet.

Du hast eigentlich nur zwei Möglichkeiten: Entweder du schreibst eine temporäre Datei oder du editierst die Datei In-Place. Zweiteres ist natürlich schick aber auch gefährlich und wesentlich aufwändiger in der Implementierung.
Nichtdestotrotz: Wenn du die Datei dann in pandas einlesen willst, muss sie in den RAM geladen werden - keine Ahnung ob pandas memmappen kann?
Es ist möglich, mit einer eigenen Stream-Klasse das Einlesen der gesamten csv-Datei mit den 500 Millionen Zeilen in einen StringIO-Puffer oder in eine temporäre Datei zu vermeiden.
Es gibt auf Stackoverflow einen Thread "Using a custom object in pandas.read_csv()" dazu: https://stackoverflow.com/a/46310416

Die verlinkte Antwort enthält einen Beispiel-Code. Dort die Klasse DataStream an die hier beschriebene Aufgabe anpassen, was ungefähr so aussehen könnte:

Code: Alles auswählen

class DataFile(object):
    def __init__(self, file, replace):
        self.file = file
        self.replace = replace

    def read(self):
        with open(file, 'r') as f:
            for line in f:
                yield self.replace[line}
Zotac ZBox ID91: Debian 12 mit GNOME
Geekom Mini IT11: Fedora 38 Silverblue (GNOME)

Benutzeravatar
TRex
Moderator
Beiträge: 8086
Registriert: 23.11.2006 12:23:54
Wohnort: KA

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von TRex » 13.09.2023 23:47:49

Würde es funktionieren, eine Zeile zu lesen, sich den Offset zu merken und die Datei mit einem Offset zu öffnen? Ich weiß ehrlich gesagt nicht, ob und wie letzteres in python tut ohne nachzuschauen.

(Und das dann bis zum Ende, also solange Text gelesen wird)
Jesus saves. Buddha does incremental backups.
Windows ist doof, Linux funktioniert nichtDon't break debian!Wie man widerspricht

Benutzeravatar
GregorS
Beiträge: 2628
Registriert: 05.06.2008 09:36:37
Wohnort: Freiburg
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von GregorS » 14.09.2023 04:51:56

reox hat geschrieben: ↑ zum Beitrag ↑
13.09.2023 10:32:33
Das braucht natürlich genau so viel RAM wie die Datei groß ist, da sie einmal komplett im Speicher landet.
Mir kommt da das Stichwort „named pipes“ in den Sinn. Genau damit kann man doch gerade derlei Probleme umgehen, oder nicht?

Gruß

Gregor
Wenn man keine Probleme hat, kann man sich welche machen. ("Großes Lötauge", Medizinmann der M3-Hopi [und sog. Maker])

Benutzeravatar
GregorS
Beiträge: 2628
Registriert: 05.06.2008 09:36:37
Wohnort: Freiburg
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von GregorS » 14.09.2023 06:17:18

buhtz hat geschrieben: ↑ zum Beitrag ↑
13.09.2023 10:22:39
... Ein weiterer Schritt ist eben auch, dass eine der Zeilen ersetzt wird. Zeile 3 (mit OID 02) soll ersetzt werden, weil die Anführungszeichen falsch sind, weil diese den zweiten Feldtrenner "verdecken" und das csv Modul in dieser Zeile nur 3 Felder liest. ...
Eure Gedanken und Idee dazu würden mich interessieren.
Mich würde v.A. interessieren, ob Du die Kriterien, nach denen eine Zeile ersetzt* wird, in klare/eindeutige Regeln gießen kannst. Wenn es einfach nur um's Ersetzen oder Löschen ungültiger Zeichen geht, könntest Du z.B. tr bemühen.

Gruß

Gregor

*wieso eigentlich „ersetzt“? Im Beispiel genügt ja eine ziemlich simple Korrektur.
Wenn man keine Probleme hat, kann man sich welche machen. ("Großes Lötauge", Medizinmann der M3-Hopi [und sog. Maker])

reox
Beiträge: 2464
Registriert: 06.06.2006 22:09:47
Lizenz eigener Beiträge: MIT Lizenz

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von reox » 14.09.2023 07:19:50

joka63 hat geschrieben: ↑ zum Beitrag ↑
13.09.2023 21:28:19
Es ist möglich, mit einer eigenen Stream-Klasse das Einlesen der gesamten csv-Datei mit den 500 Millionen Zeilen in einen StringIO-Puffer oder in eine temporäre Datei zu vermeiden.
Es gibt auf Stackoverflow einen Thread "Using a custom object in pandas.read_csv()" dazu: https://stackoverflow.com/a/46310416
Ja das wäre eine Option aber damit wird die Datei ja auch in den RAM gelesen.

Mir ist von der Aufgabenstellung noch nicht 100% klar was getan werden soll. Will man die Datei verändern UND mit pandas einlesen oder geht es nur darum die Datei zu verändern?
Ersteres geht mMn nicht ohne die Datei komplett im RAM zu halten oder per memmap. Zweiteres geht indem man eine Zeile im RAM hält aber auf der Platte eine Temporäre Datei anlegt (Die Alternative das in-place zu machen verursacht evt eine hohe Schreiblast - außer man kann zB Leerzeichen erlauben und die ersetzen Texte sind immer kürzer als das Original). Zweiteres wäre auch mit sed möglich

joka63
Beiträge: 24
Registriert: 20.07.2023 23:18:50

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von joka63 » 14.09.2023 19:59:17

reox hat geschrieben: ↑ zum Beitrag ↑
14.09.2023 07:19:50
joka63 hat geschrieben: ↑ zum Beitrag ↑
13.09.2023 21:28:19
Es ist möglich, mit einer eigenen Stream-Klasse das Einlesen der gesamten csv-Datei mit den 500 Millionen Zeilen in einen StringIO-Puffer oder in eine temporäre Datei zu vermeiden.
Es gibt auf Stackoverflow einen Thread "Using a custom object in pandas.read_csv()" dazu: https://stackoverflow.com/a/46310416
Ja das wäre eine Option aber damit wird die Datei ja auch in den RAM gelesen.

Mir ist von der Aufgabenstellung noch nicht 100% klar was getan werden soll. Will man die Datei verändern UND mit pandas einlesen oder geht es nur darum die Datei zu verändern?
Ersteres geht mMn nicht ohne die Datei komplett im RAM zu halten oder per memmap. Zweiteres geht indem man eine Zeile im RAM hält aber auf der Platte eine Temporäre Datei anlegt (Die Alternative das in-place zu machen verursacht evt eine hohe Schreiblast - außer man kann zB Leerzeichen erlauben und die ersetzen Texte sind immer kürzer als das Original). Zweiteres wäre auch mit sed möglich
Die Aufgabenbeschreibung finde ich auch missverständlich. Deswegen habe ich mich an den vom TE geposteten Python-Code gehalten, den du mit deinen 4 Punkten gut zusammengefasst hast.
Mit meinem Vorschlag würde man sich das Einlesen der gesamten Datei in einen Extra-StringIO-Buffer zusätzlich zu dem "Pandas DataFrame" sparen.

Eine einfache Alternative ist noch, einfach mit os.popen eine Pipe zu einem sed-Befehl zu öffnen. Aber ich nehme an, dass die Ersetzungsregeln im realen Programm deutlich komplexer sind. self.replace wird wohl ein größeres Dictionary sein.
Zotac ZBox ID91: Debian 12 mit GNOME
Geekom Mini IT11: Fedora 38 Silverblue (GNOME)

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 15.09.2023 11:44:44

Ich danke euch für eure Ideen und Gedanken. Tatsächlich ist mir selbst die Aufgabenstellung noch nicht in allen Details klar.
Ich nehme das erstmal so und werde wohl diverse Varianten mit meinen realen Daten probieren.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

Benutzeravatar
GregorS
Beiträge: 2628
Registriert: 05.06.2008 09:36:37
Wohnort: Freiburg
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von GregorS » 15.09.2023 11:54:32

buhtz hat geschrieben: ↑ zum Beitrag ↑
15.09.2023 11:44:44
Ich danke euch für eure Ideen und Gedanken. Tatsächlich ist mir selbst die Aufgabenstellung noch nicht in allen Details klar.
Ich nehme das erstmal so und werde wohl diverse Varianten mit meinen realen Daten probieren.
Mich interessiert, wie es damit weitergeht. Die Aufgabenstellung – so unklar sie bislang auch sein mag – klingt irgendwie spannend. Lass gelegentlich mal was lesen ...

Gruß

Gregor
Wenn man keine Probleme hat, kann man sich welche machen. ("Großes Lötauge", Medizinmann der M3-Hopi [und sog. Maker])

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 15.09.2023 12:24:40

GregorS hat geschrieben: ↑ zum Beitrag ↑
15.09.2023 11:54:32
Mich interessiert, wie es damit weitergeht. Die Aufgabenstellung – so unklar sie bislang auch sein mag – klingt irgendwie spannend. Lass gelegentlich mal was lesen ...
OK, ich werde berichten.

Die Original-Funktion ist übrigens hier dokumentiert und auch mit dem Code verlinkt. Da passiert natürlich noch ein bisschen mehr.

Am Ende geht es bei den Paket buhtzology.bandas nur darum, mir widerkehrende Arbeit beim Validieren und Einlesen von CVS Dateien nicht nur zu automatisieren, sondern auch abzusichern (d.h. bei Fehlern anschlagen!).
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 19.09.2023 15:22:18

Hatte jetzt nochmal etwas Zeit, darüber nachzudenken. Kurz formuliert: Ich befürchte, dass ich doppelt so viel RAM verbrauche, wie evtl. nötig.

Im einfachsten Fall, lese ich mit pandas.read_csv() eine Datei von der Festplatte. Ich weiß nicht, wie pandas das intern regelt, aber ich gehe davon aus, das diese Datei bzw. der DataFrame nur einmal im RAM liegt. Nehmen wir mal an das sind 10 GB. Natürlich sehen 10 GB Text (CSV Datei) und die selben Daten als pandas.DataFrame im RAM "etwas anders" aus. Aber auch der DataFrame dürfte trotz Optimierung noch einige GB groß sein.

Nun kommt der Schritt mit dem Ersetzen von Datei-Teilen vorher.
Ich lese die Datei ins RAM und ersetze dabei einzelne Zeilen. Am Ende dieses Vorgangs habe ich ein io.StringIO Objekt (also ein Buffer) im RAM von ca 10 GB.
Dann lese ich mit pandas diese Daten ein, eben nicht von der Festplatte, sondern gleich aus dem Buffer im RAM. Danach habe ich 10 GB Buffer und knapp 10 GB pandas.DataFrame im RAM; also knapp 20 GB RAM, die doppelte Größe der ursprünglichen Daten an RAM verbraucht, "nur" weil ich vorher diesen Zeilen-Ersetzen-Schritt drin habe.

So in etwa ist meine Überlegung.

EDIT: Anekdote zum Größenunterschied zwischen CSV und pandas.DataFrame. Aus einer 2,7 GB großen CSV Datei macht pandas eine 1,7 GB großen Dataframe (als pickle Datei auf der Festplatte). Laut sys.getsizeof() ist das DateFrame Objekt im RAM aber 8,4 GB groß. Merkwürdig.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

Benutzeravatar
heisenberg
Beiträge: 3567
Registriert: 04.06.2015 01:17:27
Lizenz eigener Beiträge: MIT Lizenz

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von heisenberg » 19.09.2023 16:36:20

Bis jetzt habe ich den Eindruck, dass hier ein zeilenweises Vorgehen wesentlich sinnvoller ist, als alles in den RAM zu holen. Solange Du nur zeileninterne Transformation/Analyse durchführst, brauchst Du nicht mehr als eine Zeile gleichzeitig im RAM zu haben.

Den Vorteil, den Du wohl haben willst, ist die to_markdown() - Funktion? Das müsstest Du Dir bei Verzicht auf Pandas wohl selbst schreiben. Aber das scheint mir im Zweifelsfall auch auf eine Bildschirmseite zu passen.
Jede Rohheit hat ihren Ursprung in einer Schwäche.

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 19.09.2023 17:20:12

Ne, mit to_markdown() hat das hier nichts zu tun. Die brauche ich nicht.

Wo packe ich den die Zeilen hin, nach dem ich sie geändert habe? Soll ich sie zurückschreiben?
Puh... Dann muss ich ja 10 GB lesen, zurückschreiben, und für pandas wieder lesen. Das ist mir dann doch ein bisschen zu viel Ineffizienz.
Dann kann ich auch 20 GB (oder eben mehr) im RAM halten und die Auslagerungsdatei das erledigen lassen.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

Benutzeravatar
heisenberg
Beiträge: 3567
Registriert: 04.06.2015 01:17:27
Lizenz eigener Beiträge: MIT Lizenz

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von heisenberg » 19.09.2023 17:24:59

buhtz hat geschrieben: ↑ zum Beitrag ↑
19.09.2023 17:20:12
Wo packe ich den die Zeilen hin, nach dem ich sie geändert habe? Soll ich sie zurückschreiben?
Puh... Dann muss ich ja 10 GB lesen, zurückschreiben, und für pandas wieder lesen. Das ist mir dann doch ein bisschen zu viel Ineffizienz.
Dann kann ich auch 20 GB (oder eben mehr) im RAM halten und die Auslagerungsdatei das erledigen lassen.
Da würde ich dann sagen: Ab in eine Datenbank damit (performant mit mariadb oder portabel als Einzeldatei mit SQLite)! Da kann man dann effizient mit SQL-Abfragen die Daten bearbeiten. Vorher hast Du ein schnelles, zeilenbasiert sequentiell arbeitendes Ladeprogramm und danach nur noch SQL.

Keine Ahnung, was Du eigentlich machen möchtest. Sag' doch mal: Was hast Du eigentlich genau vor? Bzw. was erwartest Du, dass da noch kommen wird?

Nachtrag

Bzgl. des Arbeitsspeicherverbrauches verarbeitest Du mit der DB-Variante dann einzelne Datensätze, die bei Bedarf aus der DB-geholt werden bzw. dann geschrieben werden. Das dauert bei der riesigen Menge an Datensätzen seine Zeit, aber - sofern Du keine ungeschickten SQL-Abfragen verwendest - ist der Arbeitsspeicherverbrauch niedrig.

Die Performance wird vermutlich um Größenordnungen besser sein, wenn Du das nativ mit SQL erledigst, als wenn Du da einzelne Datensätze einliest und die programmatisch sequentiell verarbeitest.
Jede Rohheit hat ihren Ursprung in einer Schwäche.

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 20.09.2023 14:31:55

Für eine DB ist es "zu früh".
Hier im Beispiel landen die Daten am Ende(!) ja in einem pandas DataFrame. Alternativ dazu könnte man natürlich auch eine Datenbank nehmen.
Das ist für die Frage hier egal.

Der reale Anwendungsfall für das Beispiel hier ist das Einlesen, gleichzeitiges Validieren und grobes Korrigieren (mit Zeilen ersetzen) von Rohdaten eines dritten Datengebers. Gewohnheitsgemäß sind die immer totaler Mist und können nicht 1 zu 1 in eine Datenbank oder andere "feste" Datenstruktur überführt werden. Für das Einlesen und Validieren habe ich schon eine automatisierte Lösung, die in meinen Anwendungsfällen jedenfalls gut funktioniert. Nur für das Korrigieren hatte ich bisher noch keine Lösung.

Aktuell habe ich eben den Prototyp einer Lösung (noch nicht gemerged). In meiner weiteren Arbeit wird sich zeigen, ob dass auf die Dauer praktikabel ist.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

Benutzeravatar
heisenberg
Beiträge: 3567
Registriert: 04.06.2015 01:17:27
Lizenz eigener Beiträge: MIT Lizenz

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von heisenberg » 20.09.2023 15:04:55

So wie ich verstanden habe, heisst pandas DataFrame immer: Daten mindestens 1 x komplett im RAM. Und Du schreibst, dass Dir das aber um die Ohren fliegt, weil das halt mit der Datenmenge nicht skaliert und irgendwann der RAM voll ist, dass Programm abbricht oder dass System wegen Swapping so langsam ist, dass es mit der Aufgabe niemals fertig wird. Damit würde für mich doch die Lösung pandas DataFrame wegfallen.
Für eine DB ist es "zu früh".
Das wäre dann ja auch keine richtige Datenbank. Eher eine Datensatzsammlung.
Gewohnheitsgemäß sind die immer totaler Mist und können nicht 1 zu 1 in eine Datenbank oder andere "feste" Datenstruktur überführt werden.
Das mag ja sein. Aber mit Deinem Programm validierst Du ja jetzt auch auch schon und findest dann heraus, welche Datensätze richtig sind, welche falsch sind. Wenn Datensätze falsch sind findest Du heraus, was falsch ist und findest anhand der falschen Datensätze die Art der Fehler heraus und baust dann programmatisch Transformationen, die die Daten in ein gewünschtes Format bringen.

Meiner Ansicht nach, kann man das alles wunderbar zeilenweise erledigen. Mit dem iterativen Ansatz, den ich hier geschildert habe, muss man dann den Prozess immer wieder erneut anstarten. Das ist aber ohnehin sowieso der Fall.

Wenn ich Dich so richtig verstanden habe ist Dein iterativer Ansatz aber, dass Du möglichst mehrere Stufen haben willst: Du führst die 1. Validierung durch und bekommst ein Zwischenergebnis. Dann bemerkst Du ein paar Fehler und arbeitest erneut mit den Daten. Dann führst Du die 2. Validierung durch auf den Daten des Zwischenergebnisses, was dann Zwischenergebnis-2 gibt. etc.

Mein Ansatz wäre im Vergleich dazu: Ich starte die Validierung, bemerke Fehler. Dann baue ich die benötigten Transformationen und starte wieder mit den Ausgangsdaten. Dann entdecke ich weitere Fehler, baue weitere Transformationen und starte wieder mit den Originaldaten. etc. Evtl. breche ich die Validierung zwischendrin auch ab, weil es ja nicht notwendig ist jedes mal die 500 Mio Datensätze zu verarbeiten.

Denkbar wäre auch, dass man die fehlerhaften Zeilen separat wegschreibt, um dann eine Restdatenmenge zu haben, um die man sich in weiteren Schritten kümmert bis die Restdatenmenge irgendwann leer ist.

So wie es jetzt aussieht braucht es für mich da nicht unbedingt eine Datenbank. Aber eine Datenbank könnte hier schon gut helfen. Da könnte man z. B. Fehlerdatensätze als Rohdaten-Zeichenkette in eine separate Tabelle packen, die man dann schrittweise nachverarbeitet und validiert. Bzw. man hat erst einen DB-Loader, der alle Zeilen als Zeichenkette direkt in die Datenbank lädt und führt dann die Validierung und Transformation nur in der Datenbank aus.

Die paar Befehle für eine einfache Tabellenstruktur sollten recht schnell erledigt sein.
Aktuell habe ich eben den Prototyp einer Lösung
Ja, dann erzähl' mal. Deinen Python-Code möchte mich mir jetzt nicht unbedingt anschauen. Da habe ich zu wenig Ahnung von Python. Ich würde lieber direkt von Dir lesen, was Du Dir so dabei denkst.

Nachtrag

Ich verstehe jetzt, dass Du die Daten wohl möglichst im Rohdatenformat erhalten möchtest um Zeichenkettentransformationen möglichst auf der ganzen Zeile durchführen zu können ohne die irgendwie aufteilen zu müssen. In dem Fall, könnte man die Zeilen grundsätzlich im Rohdatenformat in die Datenbank laden, dort die Validierung durchführen und nur eine oder mehrere Indextabellen haben, die alle Datensätze mit einem Flag versehen (z. B. "1 - valide Daten", "2 - Fehler erkannt"). Weiterhin könnte man dann die Rohdatenstrings dort bearbeiten.
Jede Rohheit hat ihren Ursprung in einer Schwäche.

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 21.09.2023 10:05:07

heisenberg hat geschrieben: ↑ zum Beitrag ↑
20.09.2023 15:04:55
Und Du schreibst, dass Dir das aber um die Ohren fliegt, weil das halt mit der Datenmenge nicht skaliert
Da habe ich mich falsch ausgedrückt. Der DataFrame ist nicht das Problem. Wenn das dort erst einmal drin ist, ist alles gut. In der Realität sind es ja sogar mehrere DataFrames, weil ich mehrere solcher CSV Dateien in einem Rutsch einlese.
heisenberg hat geschrieben: ↑ zum Beitrag ↑
20.09.2023 15:04:55
Meiner Ansicht nach, kann man das alles wunderbar zeilenweise erledigen. Mit dem iterativen Ansatz, den ich hier geschildert habe, muss man dann den Prozess immer wieder erneut anstarten. Das ist aber ohnehin sowieso der Fall.
Ja, so sehe ich das auch. Da hast du schon Recht.

heisenberg hat geschrieben: ↑ zum Beitrag ↑
20.09.2023 15:04:55
Wenn ich Dich so richtig verstanden habe ist Dein iterativer Ansatz aber, dass Du möglichst mehrere Stufen haben willst
Nicht in dem Sinne, dass das alles in einem Run durchläuft. Mehrere Schritte heißt eben auch, dass Script mehrmals starten. So wie du es beschrieben hast, mit dem immer wieder Starten.
Denkbar wäre auch, dass man die fehlerhaften Zeilen separat wegschreibt, um dann eine Restdatenmenge zu haben, um die man sich in weiteren Schritten kümmert bis die Restdatenmenge irgendwann leer ist.
Das ist auch eine schöne Idee! Tatsächlich protokoliere ich die fehlerhaften Zeilen schon in einer separaten Datei.
Deinen Python-Code möchte mich mir jetzt nicht unbedingt anschauen. Da habe ich zu wenig Ahnung von Python. Ich würde lieber direkt von Dir lesen, was Du Dir so dabei denkst.
Verständlich. :D

Aktuell habe ich keine elegante Lösung, sondern habe die Daten wirklich doppelt im RAM. Einmal lese ich sie in einen Buffer und führe dabei die Ersetzungen durch. Dann liest pandas aus diesem Buffer. Danach wird der Buffer natürlich verworfen, aber für einen "kurzen Moment" sind die Daten halt doppelt da. Das ist halt jetzt die schnelle pragmatische "Lösung".
Langfristig finde ich ein eigenes Stream Object wie hier von joka63 vorgeschlagen eine gute Idee. Das würde den Code besser isolieren und das maintaining der unit tests auch erleichtern.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

reox
Beiträge: 2464
Registriert: 06.06.2006 22:09:47
Lizenz eigener Beiträge: MIT Lizenz

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von reox » 22.09.2023 07:22:36

buhtz hat geschrieben: ↑ zum Beitrag ↑
20.09.2023 14:31:55
Der reale Anwendungsfall für das Beispiel hier ist das Einlesen, gleichzeitiges Validieren und grobes Korrigieren (mit Zeilen ersetzen) von Rohdaten eines dritten Datengebers. Gewohnheitsgemäß sind die immer totaler Mist und können nicht 1 zu 1 in eine Datenbank oder andere "feste" Datenstruktur überführt werden. Für das Einlesen und Validieren habe ich schon eine automatisierte Lösung, die in meinen Anwendungsfällen jedenfalls gut funktioniert. Nur für das Korrigieren hatte ich bisher noch keine Lösung.
aha! In dem Fall würde ich zeilenweise lesen und eine temporäre Datei schreiben. Wenn du es klug anlegst, kannst du sogar nur ein Patch File schreiben.
Der Leseaufwand liegt dann bei O(N) und der Speicheraufwand ist in der Größenordnung einer Zeile.

joka63
Beiträge: 24
Registriert: 20.07.2023 23:18:50

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von joka63 » 24.09.2023 23:08:03

buhtz hat geschrieben: ↑ zum Beitrag ↑
21.09.2023 10:05:07
Langfristig finde ich ein eigenes Stream Object wie hier von joka63 vorgeschlagen eine gute Idee. Das würde den Code besser isolieren und das maintaining der unit tests auch erleichtern.
Ich hatte letzte Woche den Python-Code aus dem ersten Beitrag heruntergeladen und um das Stream-Object "DataFile" erweitert (und den dann überflüssig gewordenen Code entfernt) und das in meiner PyCharm-Entwicklungsumgebung getestet. Ich hoffe, es ist verständlich und hilfreich:

Code: Alles auswählen

#!/usr/bin/env python3
import io
import sys
import csv
from pathlib import Path
import pandas


replace: dict = {}


def iterstream(iterable, buffer_size=io.DEFAULT_BUFFER_SIZE):
    """
    See https://stackoverflow.com/questions/46310030/using-a-custom-object-in-pandas-read-csv/46310416#46310416
    for explanation of function iterstream and class DataFile.

    http://stackoverflow.com/a/20260030/190597 (Mechanical snail)
    Lets you use an iterable (e.g. a generator) that yields bytestrings as a
    read-only input stream.

    The stream implements Python 3's newer I/O API (available in Python 2's io
    module).

    For efficiency, the stream is buffered.
    """
    class IterStream(io.RawIOBase):
        def __init__(self):
            self.leftover = None
        def readable(self):
            return True
        def readinto(self, b):
            try:
                l = len(b)  # We're supposed to return at most this much
                chunk = self.leftover or next(iterable)
                output, self.leftover = chunk[:l], chunk[l:]
                b[:len(output)] = output
                return len(output)
            except StopIteration:
                return 0    # indicate EOF
    return io.BufferedReader(IterStream(), buffer_size=buffer_size)


class DataFile(object):
    def __init__(self, file, replace, as_bytes=True):
        self.file = file
        self.replace = replace
        self.as_bytes = as_bytes

    def read(self):
        with open(self.file, 'r') as stream:
            for line in stream:
                # Replace source line if necessary:
                if line in self.replace:
                    row = self.replace[line]
                else:
                    row = line
                if self.as_bytes:
                    yield row.encode('utf-8')
                else:
                    yield row


def check_data_file():
    """
    Validate the CSV, but don't store its contents anywhere
    """
    def check_data_stream(stream):
        for row in csv.reader(stream, delimiter=';'):
            print(f'{row=}')
            # validate row
            if len(row) != 4:
                raise RuntimeError(f"CSV format error in line: {row}")

    try:
        data_stream = DataFile(Path.cwd() / 'data.csv', replace, as_bytes=False)
        check_data_stream(data_stream.read())
    except Exception as e:
        print(f"Cannot read data.csv: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ ==  "__main__":
    replace = {
        '02;"Invalid;Name";1\n': '02;Invalid;Name;1\n'
    }

    check_data_file()
    data_stream = DataFile(Path.cwd() / 'data.csv', replace, as_bytes=True)
    data = pandas.read_csv(iterstream(data_stream.read()), sep=';')

    print(data.to_markdown())
Das generiert die gleiche Ausgabe wie das Beispiel aus dem ersten Beitrag.
Ich hätte mir noch gewünscht, dass ich zeilenweise Ersetzen, Verifizieren und Weitergabe an Pandas in einem Rutsch demonstrieren kann. Das scheitert aber daran, dass zur Verifikation csv.reader verwendet wird, das eine (oder mehrere?) Zeilen von einem Stream einliest und es in ein Array umwandelt, dass dann aber nicht mehr durch pandas.read_csv weiterverarbeitet werden kann.
Deshalb bleiben Ersetzen+Verifizieren und dann Ersetzen und Einlesen in Pandas zwei separate Schritte, d.h. die csv-Datei muss zweimal gelesen werden. Beim Verifizieren in check_data_file() werden aber keine Daten im Hauptspeicher zwischengespeichert, so dass der doppelte Speicherbedarf entfällt.
In der realen Anwendung würde man wohl nicht schon bei der ersten fehlerhaften Zeile abbrechen, sondern alle fehlerhaften Zeile loggen.
Zotac ZBox ID91: Debian 12 mit GNOME
Geekom Mini IT11: Fedora 38 Silverblue (GNOME)

buhtz
Beiträge: 1106
Registriert: 04.12.2015 17:54:49
Kontaktdaten:

Re: Python Optimierung: Teile einer Text-/CSV-Datei während dem Einlesen ersetzen

Beitrag von buhtz » 25.09.2023 16:09:24

Vielen herzlichen Dank.
Debian 11 & 12; Desktop-PC, Headless-NAS, Raspberry Pi 4
Teil des Upstream Betreuer Teams von Back In Time (Debianbackintime)

Antworten