bash, Linux: Unterschied zwischen zwei Textdateien einstellen


71

Ich habe zwei Dateien A- nodes_to_deleteund B- nodes_to_keep. Jede Datei hat viele Zeilen mit numerischen IDs.

Ich möchte die Liste der numerischen IDs haben, die in, nodes_to_deleteaber NICHT in sind nodes_to_keep, z Alt-Text .

Dies in einer PostgreSQL-Datenbank zu tun, ist unangemessen langsam. Gibt es eine gute Möglichkeit, dies mit Linux CLI-Tools in Bash zu tun?

UPDATE: Dies scheint ein Pythonic-Job zu sein, aber die Dateien sind wirklich sehr, sehr groß. Ich habe einige ähnliche Probleme gelöst mit uniq, sortund einige Mengenlehre Techniken. Dies war ungefähr zwei oder drei Größenordnungen schneller als die Datenbankäquivalente.


Ich bin gespannt, welche Antworten kommen werden. Bash ist ein bisschen mehr Segphault, Systemadministrator glaube ich. Wenn Sie "in Python" oder "in PHP" gesagt hätten oder was auch immer Ihre Chancen besser gewesen wären :)
Extraneon

Ich sah den Titel und war bereit, UI-Inkonsistenzen und Hilfforen, die heiliger sind als du, zu verprügeln. Dies hat mich enttäuscht, als ich die eigentliche Frage gelesen habe. :(
aehiilrs

Antworten:


111

Der Befehl comm macht das.


10
Und wenn die Dateien noch nicht sortiert sind, sortzuerst.
extraneon

2
+1 Erleuchtetes, großartiges Werkzeug, das ich dumm finde, nicht gewusst zu haben. Vielen Dank!
Adam Matan

9
@ Nur wird hier keinen Flammenkrieg beginnen, aber Ihr Kommentar ist einfach unhöflich.
Adam Matan

4
@Adam: Ironischerweise stammt dieses "Comm" -Bit von Arcana aus einer Zeit, in der Sie den gesamten Inhalt von / bin und / usr / bin in Ihrem Kopf behalten konnten, vor all diesen ausgefallenen Perls, Pythons und MySQLs. In diesen einfacheren V7-Tagen mussten Sie alle Werkzeuge verwenden oder (nach Luft schnappen!) Ihre eigenen mit ed (1) im Schnee schreiben, in beide Richtungen bergauf, und es hat uns gefallen! ;) Ich würde wahrscheinlich nie von Comm erfahren, wenn ich später angefangen hätte.
Msw

4
@ Adam Matan: Es tut mir leid, Unhöflichkeit war definitiv nicht meine Absicht. Tatsächlich ist der Befehl, den ich gepostet habe, eine gute Möglichkeit, viel über das System zu lernen, und ich habe solche Dinge getan, um mich selbst aufzuklären. Sonst join(1)wäre mir zB unbekannt geblieben.
Nur jemand

43

Jemand hat mir vor ein paar Monaten gezeigt, wie man genau das macht, und dann konnte ich es eine Weile nicht finden ... und beim Schauen bin ich auf Ihre Frage gestoßen. Hier ist es :

set_union () {
   sort $1 $2 | uniq
}

set_difference () {
   sort $1 $2 $2 | uniq -u
}

set_symmetric_difference() {
   sort $1 $2 | uniq -u
}

1
Ich denke, das ist besser als die akzeptierte Antwort ... commist nicht in allen Umgebungen verfügbar.
Danwyand

3
Das ist ein symmetrischer Unterschied, kein normaler Satzunterschied.
Tgr

@ Tgr ziemlich sicher, dass es normaler Satzunterschied ist.
Orip

@ wieczorek1990 Ich bin mir nicht sicher, welche Beispiele mit stdin für die sort + uniq-Lösungen funktionieren, die nicht für comm geeignet sind, aber auf jeden Fall gewinnt dieser Ansatz - sowohl für comm als auch für sort + uniq - normalerweise (zeigt Peteris Krumins 'comm-Beispiel für set Unterschied) 'cmd -23 <(Sortierdatei1) <(Sortierdatei2)' Siehe catonmat.net/blog/set-operations-in-unix-shell-simplified
orip

4
set_differenceund set_symmetric_differencefunktioniert nicht immer richtig - sie löschen Zeilen, die für die erste Eingabedatei eindeutig sind, wenn diese Zeilen in dieser Datei nicht eindeutig sind.
Leon

9

Verwendung comm- Es werden zwei sortierte Dateien Zeile für Zeile verglichen.

Die kurze Antwort auf Ihre Frage

Dieser Befehl gibt Zeilen zurück, die nur für deleteNodes und nicht für keepNodes gelten.

comm -1 -3 <(sort keepNodes) <(sort deleteNodes)

Beispieleinrichtung

Erstellen wir die Dateien mit dem Namen keepNodesund deleteNodesund verwenden sie als unsortierte Eingabe für den commBefehl.

$ cat > keepNodes <(echo bob; echo amber;)
$ cat > deleteNodes <(echo bob; echo ann;)

Wenn Sie comm ohne Argumente ausführen, werden standardmäßig 3 Spalten mit diesem Layout gedruckt:

lines_unique_to_FILE1
    lines_unique_to_FILE2
        lines_which_appear_in_both

Führen Sie in unseren obigen Beispieldateien comm ohne Argumente aus. Beachten Sie die drei Spalten.

$ comm <(sort keepNodes) <(sort deleteNodes)
amber
    ann
        bob

Spaltenausgabe unterdrücken

Unterdrücke Spalte 1, 2 oder 3 mit -N; Beachten Sie, dass beim Ausblenden einer Spalte das Leerzeichen kleiner wird.

$ comm -1 <(sort keepNodes) <(sort deleteNodes)
ann
    bob
$ comm -2 <(sort keepNodes) <(sort deleteNodes)
amber
    bob
$ comm -3 <(sort keepNodes) <(sort deleteNodes)
amber
    ann
$ comm -1 -3 <(sort keepNodes) <(sort deleteNodes)
ann
$ comm -2 -3 <(sort keepNodes) <(sort deleteNodes)
amber
$ comm -1 -2 <(sort keepNodes) <(sort deleteNodes)
bob

Sortieren ist wichtig!

Wenn Sie comm ausführen, ohne die Datei zuerst zu sortieren, schlägt dies ordnungsgemäß mit einer Meldung fehl, welche Datei nicht sortiert ist.

comm: file 1 is not in sorted order


1
+1 für korrekte Beispiele, die die Antwort auf die spezifische Frage des OP enthalten (Ausgabezeilen deleteNodes, die nicht vorhanden sind keepNodes), wären jedoch besser, wenn die richtige Lösung hervorgehoben würde : comm -1 -3 <(sort keepNodes) <(sort deleteNodes).
John B

4

comm wurde speziell für diese Art von Anwendungsfall entwickelt, erfordert jedoch sortierte Eingaben.

awkist wohl ein besseres Werkzeug dafür, da es ziemlich einfach ist, Mengenunterschiede zu finden, dies nicht erfordert sortund zusätzliche Flexibilität bietet.

awk 'NR == FNR { a[$0]; next } !($0 in a)' nodes_to_keep nodes_to_delete

Vielleicht möchten Sie zum Beispiel nur den Unterschied in Zeilen finden, die nicht negative Zahlen darstellen:

awk -v r='^[0-9]+$' 'NR == FNR && $0 ~ r {
    a[$0]
    next
} $0 ~ r && !($0 in a)' nodes_to_keep nodes_to_delete

1

Vielleicht brauchen Sie einen besseren Weg, um es in Postgres zu tun. Ich kann ziemlich wetten, dass Sie keinen schnelleren Weg finden, dies mit Flatfiles zu tun. Sie sollten in der Lage sein, einen einfachen inneren Join durchzuführen und davon auszugehen, dass beide ID-Spalten indiziert sind, was sehr schnell sein sollte.


Sie sind technisch korrekt und das explainunterstützt Ihre Behauptung, aber es funktioniert einfach nicht für sehr große (~ zig Millionen) Tabellen.
Adam Matan

1
Ja, es würde durch Ihr Gedächtnis eingeschränkt sein, anders als bei einer sortierten Kommunikation, aber ich würde denken, wenn Sie zwei Tabellen mit nur einem int-ID-Feld haben, könnten Sie ohne Probleme in die 10er Millionen gelangen.
Dark Castle

Das stimmt theoretisch, funktioniert aber aus irgendeinem Grund einfach nicht.
Adam Matan

0

Dies unterscheidet sich also geringfügig von den anderen Antworten. Ich kann nicht sagen, dass ein C ++ - Compiler genau ein "Linux CLI-Tool" ist, aber das Ausführen g++ -O3 -march=native -o set_diff main.cpp(mit dem folgenden Code main.cppkann den Trick machen):

#include<algorithm>
#include<iostream>
#include<iterator>
#include<fstream>
#include<string>
#include<unordered_set>

using namespace std;

int main(int argc, char** argv) {
    ifstream keep_file(argv[1]), del_file(argv[2]);
    unordered_multiset<string> init_lines{istream_iterator<string>(keep_file), istream_iterator<string>()};
    string line;
    while (getline(del_file, line)) {
        init_lines.erase(line);
    }
    copy(init_lines.begin(),init_lines.end(), ostream_iterator<string>(cout, "\n"));
}

Zur Verwendung einfach ausführen set_diff B A( nicht A B , da Bist nodes_to_keep) und der resultierende Unterschied wird auf stdout gedruckt.

Beachten Sie, dass ich auf einige bewährte Methoden für C ++ verzichtet habe, um den Code einfacher zu halten.

Es könnten viele zusätzliche Geschwindigkeitsoptimierungen vorgenommen werden (zum Preis von mehr Speicher). mmapwäre auch besonders nützlich für große Datenmengen, aber das würde den Code viel komplizierter machen.

Da Sie erwähnt haben, dass die Datenmengen groß sind, hielt ich das Lesen nodes_to_deleteeiner Zeile gleichzeitig für eine gute Idee, um den Speicherverbrauch zu reduzieren. Der im obigen Code verfolgte Ansatz ist nicht besonders effizient, wenn sich viele Dupes in Ihrem befinden nodes_to_delete. Auch die Ordnung bleibt nicht erhalten.


Etwas, das einfacher zu kopieren und einzufügen ist bash(dh die Erstellung von überspringt main.cpp):

g++ -O3 -march=native -xc++ -o set_diff - <<EOF
#include<algorithm>
#include<iostream>
#include<iterator>
#include<fstream>
#include<string>
#include<unordered_set>

using namespace std;

int main(int argc, char** argv) {
        ifstream keep_file(argv[1]), del_file(argv[2]);
        unordered_multiset<string> init_lines{istream_iterator<string>(keep_file), istream_iterator<string>()};
        string line;
        while (getline(del_file, line)) {
                init_lines.erase(line);
        }
        copy(init_lines.begin(),init_lines.end(), ostream_iterator<string>(cout, "\n"));
}
EOF
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.