Vergleich zweier CSV-Files

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Corpa
User
Beiträge: 3
Registriert: Sonntag 25. Februar 2018, 11:55

Sonntag 25. Februar 2018, 13:03

Hallo Leute!

für einen, für mich als Python-Anfänger, "kniffligen Fall" benötige ich einmal Eure Ideen..

Ausgangspunkt sind zwei unsortierte CSV Files mit je ca. 1000k Lines, die Referenzierung erfolgt durch den Abgleich von Typ und Charge (siehe file_3.csv)..

Code: Alles auswählen

file_1.csv

i;Datum;Typ;Anzahl;Charge
1;2018-01-01;Mutter;3,666
2;2018-01-01;Schraube;3,0815
3;2018-01-01;Nagel;3,0816
4;2018-03-05;Schraubenzieher;1;3698
...

Code: Alles auswählen

file_2.csv

i;Datum;Typ;Anzahl;Charge
1;2018-01-01;Nagel;4,0816
2;2018-01-02;Schraube;3,0815
3;2018-01-01;Mutter;3,666
4;2018-01-01;Mutter;3,666
...
Nun möchte ich diese Listen vergleichen und eine Änderungsdatei erstellen:

Code: Alles auswählen

file_3.csv

i;Typ;Charge;Datum_file_1;Datum_file_2;Anzahl_file_1;Anzahl_file_2;Diff
1;Mutter;666;"";2018-01-01;"";3;"Added"
2;Schraube;0815;2018-01-01;2018-01-02;3;3;"Date changed"
3;Nagel;0816;2018-01-01;2018-01-01;3;4;"QTY changed"
4;Schraubenzieher;3698;2018-03-05;"";1;"";"Deleted"
Meine Ideen bisher:
A) zwei for-Schleifen und mehreren If Abfragen, welche bei der Größe der Dateien und der benötigten Zeit nicht Zielführend war. :lol:
B) numpy where() Funktion, gibt mir bei 2/4 Fällen ein False aus. Bisher beste Methode.
C) difflib, schöne Lib, brachte bisher aber auch keinen Durchbruch..

Hoffe die Infos sind ausreichend und verständlich, falls nicht, reiche/bessere ich gerne nach :idea:

Wie würdet ihr an den Fall rangehen? Primär geht es mir nicht um einen fertigen Code, sondern, wie oben beschrieben, um Ideen bzw. Lösungsansätze.

Vorab Vielen Dank :)
Sirius3
User
Beiträge: 8805
Registriert: Sonntag 21. Oktober 2012, 17:20

Sonntag 25. Februar 2018, 13:28

@Corpa: Deine Beispieldateien sind kaputt, so dass sie nur mehr Verwirrung stiften, als helfen, Dein Problem zu verstehen. Es scheinen manche Zeilen doppelt vorzukommen, wie geht man damit um; es gibt ein `i` das nicht relevant zu sein scheint, aber trotzdem mitgeschleppt wird.

Wie immer bei solchen Problemen, liest man die Daten erst in passende Datenstrukturen (hier Wörterbücher und evtl. Listen) verarbeitet die Daten dann und schreibt das Ergebnis in eine neue Datei. Bei der Wahl der Datenstruktur mußt Du Dir fragen, „wie will ich auf die Daten zugreifen“.
Benutzeravatar
kbr
User
Beiträge: 957
Registriert: Mittwoch 15. Oktober 2008, 09:27

Sonntag 25. Februar 2018, 13:50

@Corpa: Ein Lösungsansatz könnte sein, file_1.csv komplett einzulesen und als Dictionary vorzuhalten. Mit der Zeile als Value und Typ und Charge als Key. Dann könntest Du die zweite Datei zeilenweise lesen und abgleichen, ob die Zeile bereits in der ersten Datei enthalten war und ob die Daten übereinstimmen. Falls die Daten nicht übereinstimmen, dann schreibst Du einen Änderungseintrag in die Ausgabedatei. In beiden Fällen löschst Du anschließend den Eintrag aus dem Dictionary. Wenn eine Zeile aus Datei 2 nicht im Dictionary ist, dann schreibst Du eine "Added" message in die Ausgabedatei.
Wenn die Datei 2 durch ist, kannst Du alle im Dictionary verbliebenen Einträge als "Deleted" ausgeben.
Vielleicht geht es auch eleganter, aber das wäre jetzt meine erste Idee.

Und, wie Sirius3 bereits schrieb, wird das i nicht sinnvoll verwendet. Wenn dies eine id wäre und die Daten nach dieser abgeglichen werden könnten, wäre die Aufgabe wesentlich leichter und weniger fehleranfällig zu lösen. Falls ich sie überhaupt richtig verstanden habe.
Corpa
User
Beiträge: 3
Registriert: Sonntag 25. Februar 2018, 11:55

Montag 26. Februar 2018, 19:45

@kbr, Sirius
Vielen Dank für die schnelle Antwort.
Die Spalte i habe ich rein zur Übersicht eingefügt. In der realen Datei gibt es diese natürlich nicht.

Daher einmal die Übersicht angepasst.

Code: Alles auswählen

file_1.csv

Datum;Typ;Anzahl;Charge
2018-01-01;Mutter;3;666
2018-01-01;Schraube;3;0815
2018-01-01;Nagel;3;0816
2018-03-05;Schraubenzieher;1;3698
...

Code: Alles auswählen

file_2.csv

Datum;Typ;Anzahl;Charge
2018-01-01;Nagel;4;0816
2018-01-02;Schraube;3;0815
2018-01-01;Mutter;3;666
2018-01-01;Mutter;3;666
...

Code: Alles auswählen

file_3.csv

Typ;Charge;Datum_file_1;Datum_file_2;Anzahl_file_1;Anzahl_file_2;Diff
Mutter;666;"";2018-01-01;"";3;"Added"
Schraube;0815;2018-01-01;2018-01-02;3;3;"Date changed"
Nagel;0816;2018-01-01;2018-01-01;3;4;"QTY changed"
Schraubenzieher;3698;2018-03-05;"";1;"";"Deleted"
@kbr
Deine Idee mit dem Dict ist simpel und hat mich bereits beschäftigt. Das einlesen via csv.DictReader klappt jedoch fehlt es mir noch an der Umsetzungsidee der Abfrage im Dict...

1) Lese erste line in der CSV und selektieren von Typ und Charge #kein Thema
2) Suche/Filter Typ, Charge.. (prüfen ob gleiches Datum, Anzahl usw..) #Filtern mit multiplen values in den keys?
3) schreiben in den Report #sollte kein Thema werden

Der Punkt ist halt die Suche im Dict. Wie setzte ich eine multiple Suche um?? Siehe Code ##Entwurf##

Code: Alles auswählen

import csv

#Read File 1
file1 = open("csv1.csv", "r")
rfile1 = csv.reader(file1, delimiter=";")

#File 2 to DICT
file2 = open("csv2.csv", "r")
dictfile2 = csv.DictReader(file2)

#create report
#report = {}

for line in rfile1:
    Datum = line[0]
    Typ = line[1]
    Anzahl = line[2]
    Charge = line[3]
    
##ENTWURF##
    if Typ and Charge and Anzahl and Datum in dictfile2:
        #Werte aus Dict löschen
    elif Typ and Charge and Anzahl in dictfile2:
        #Datum change in Report schreiben
        #Werte aus Dict löschen
    elif Typ and Charge and Datum in dictfile2:
        #QTY change in Report schreiben
        #Werte aus Dict löschen
    elif Typ and Charge in dictfile2:
        #Added in Report schreiben
        #Werte aus Dict löschen
    else:
        #Added in Report schreiben
      
Benutzeravatar
kbr
User
Beiträge: 957
Registriert: Mittwoch 15. Oktober 2008, 09:27

Montag 26. Februar 2018, 20:15

@Corpa: versuch mal den folgenden, mäßig eleganten Ansatz (ungetestet):

Code: Alles auswählen

with open('file_1.csv') as fobj:
    data = {line.split(';')[1]+line.split(';')[-1]: line for line in fobj}
    
with open('file_2.csv') as fobj:
    for line in fobj:
        parts = line.split(';')
        key = parts[1] + parts[-1]
        if key in data:
            if data[key] == line:
                # nothing changed
            else:
                # something has changed
            del data[key]
        else:
            # new entry in file_2

# everything left in data has not been in file_2
Sirius3
User
Beiträge: 8805
Registriert: Sonntag 21. Oktober 2012, 17:20

Dienstag 27. Februar 2018, 08:23

@Corpa: nicht die einzelnen Zeile in je einem Wörterbuch (obwohl das auch ein guter Schritt ist), sondern die gesamten Daten in ein Wörterbuch stecken, denn das ist ja genau die Zugriffsmethode, die Du brauchst: über Typ und Charge auf einen Datensatz zuzugreifen.

Code: Alles auswählen

def read_data(filename):
    with open(filename) as lines:
        return { (entry['Typ'], entry['Charge']): entry
            for entry in csv.reader(lines, delimiter=';') }

def main():
    data1 = read_data('csv1.csv')
    data2 = read_data('csv2.csv')
    only_in_data1 = set(data1) - set(data2)
    only_in_data2 = set(data2) - set(data1)
    in_both = set(data1) & set(data2)
    for key in in_body:
        entry1 = data1[key]
        entry2 = data2[key]
        if entry1['Anzahl'] != entry2['Anzahl']:
            print("Anzahl verscheiden")
    usw.

if __name__ == '__main__':
    main()
Es bleibt noch die Frage, wie mit doppelten Einträgen in einer Datei umzugehen ist.

@kbr: für das Lesen von CSV-Dateien gibt es das CSV-Modul. Dann muß man eine Zeile auch nicht vielfach splitten. Du solltest immer Einlesen von Verarbeiten möglichst klar trennen, und zum Einlesen gehört eben auch, die Zeilen aufzuteilen. Statt Typ und Charge als String zusammenzustecken solltest Du ein Tupel als Wörterbuchschlüssel verwenden, sonst ist Schraube1 mit Chargennummer 285 das gleiche wie Schraube12 mit Chargennummer 85. Aus Daten etwas zu löschen ist eine schlechte Idee, denn dann sind sie für einen eventuellen weiteren Schritt kaputt.
Benutzeravatar
kbr
User
Beiträge: 957
Registriert: Mittwoch 15. Oktober 2008, 09:27

Dienstag 27. Februar 2018, 09:56

@Sirius3: Mein Ansatz war schnell dahin geschrieben und ein Tupel als Key ist definitiv die bessere Idee. Das Löschen von Einträgen aus dem Dictionary allerdings ist Absicht, denn nach den Beispielen gilt ein Eintrag auch dann als neu hinzugekommen, wenn er in der ersten Datei einmal, in der zweiten aber mehrmals vorkommt. Diese Unterscheidung geht bei Deinen Set-Operationen verloren. Weiter ging ich davon aus, dass die Dateien möglicherweise zu groß sind, um beide im Speicher Platz zu finden (zugegeben unwahrscheinlich, da Laptops heute oft schon mit 16 GB ausgestattet sind, und das für viele Anforderungen völlig ausreicht), so dass nur die erste Datei komplett eingelesen, und die zweite sequentiell behandelt wurde.
Corpa
User
Beiträge: 3
Registriert: Sonntag 25. Februar 2018, 11:55

Donnerstag 1. März 2018, 18:14

@sirius, kbr

Vielen lieben Dank! Auf Eure eingebrachten Ideen bin ich so erstmal nicht gekommen.. Liegt vielleicht auch an der mangelnden Erfahrung in diesem Fall. Die Set Lösung passt, auch bei der zu berechnenden Menge an Daten.

Den Code von Sirius habe ich einmal angepasst und zwei kleine Fehler ausgemerzt. Läuft super!

Code: Alles auswählen

import csv

def read_data(filename):
    with open(filename) as lines:
        return { (entry[1], entry[3]): entry
            for entry in csv.reader(lines, delimiter=';') }
 
def main():

    data1 = read_data('csv1.csv')
    data2 = read_data('csv2.csv')
    
    only_in_data1 = set(data1) - set(data2)
    only_in_data2 = set(data2) - set(data1)
    in_both = set(data1) & set(data2)
    
    for key in in_both:
    
        entry1 = data1[key]
        entry2 = data2[key]
        
        #Anzahl und Datum
        if entry1[2] != entry2[2] and entry1[0] != entry2[0] :
            print("QTY & Datum")
            print(str(entry1) + str(entry2))
        #Anzahl <>
        elif entry1[2] != entry2[2]:
            print("QTY")
            print(str(entry1) + str(entry2))
        #Datum <>
        elif entry1[0] != entry2[0]:
            print("Datum")
            print(str(entry1) + str(entry2))
        else:
            print("same")
            #print(str(entry1) + str(entry2))
    
    #Deleted
    print(only_in_data1)
    for key2 in only_in_data1: #Entwurf
        #print(str(entry1) + str(entry2))
        pass 
    
    #Added
    print(only_in_data2)
    for key3 in only_in_data1:
        print(str(entry1) + str(entry2))
    
if __name__ == '__main__':
    main()

Code: Alles auswählen

CSV1
Datum;Typ;Anzahl;Charge
2018-01-01;Mutter;3;666
2018-01-01;Schraube;3;0815
2018-01-01;Nagel;3;0816
2018-03-05;Schraubenzieher;1;3698

Code: Alles auswählen

CSV2
Datum;Typ;Anzahl;Charge
2018-01-01;Nagel;4;0816
2018-01-02;Schraube;3;0815
2018-01-01;Mutter;3;666
2018-01-01;Mutter;3;666
2018-01-05;Stift;8;1675
Wenn man den Code nun mit den zwei CSV Dateien abspielt, fehlen mir nur noch die Daten (Anzahl und Datum) bei Schraubenzieher und Stift...

Code: Alles auswählen

{('Schraubenzieher', '3698')} <-- Deleted
{('Stift', '1675')} <--- Added
['2018-01-01', 'Mutter', '3', '666'] <--- Added
Habe einen halben Tag rumprobiert aber finde keine passende Möglichkeit Added und Deleted auszugeben :evil: . Die zweite und dritte for-Schleife ist eine Notlösung (Entwurf) und gibt leider auch nicht alle benötigten Daten wieder (Anzahl & Datum). Eine Ausgabe wie bei Artikel "Mutter" wäre perfekt. Diese kann ich weiter verwerten. Habt ihr eine Idee?
Sirius3
User
Beiträge: 8805
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 2. März 2018, 08:24

@Corpa: Variablennamen kann man recyceln, wenn es sich um unabhängige Schleifen handelt, es besteht also kein Grund, key2 und key3 statt einfach nur key zu nehmen. In der ersten Schleife steht schon, wie man auf vom Schlüssel auf die Daten zugreifen kann.

Übersichtlicher wird es, wenn man die Daten entpackt:

Code: Alles auswählen

    for key in in_both:
        date1, type1, amount1, charge1 = data1[key]
        date2, type2, amount2, charge2 = data2[key]

        if date1 == date2 and amount1 == amount2:        
            state = "same"
        elif data1 == date2:
            state = "QTY"
        elif amount1 == amount2:
            state = "date"
        else:
            state = "date & QTY"

        print(type1, charge1, date1, date2, amount1, amount2, state)
Antworten