it-swarm.com.de

Schnelle Möglichkeit, Zeilen in einer Datei zu finden, die sich nicht in einer anderen befinden?

Ich habe zwei große Dateien (Sätze von Dateinamen). Ungefähr 30.000 Zeilen in jeder Datei. Ich versuche, einen schnellen Weg zu finden, um Zeilen in Datei1 zu finden, die in Datei2 nicht vorhanden sind.

Wenn dies beispielsweise file1 ist:

line1
line2
line3

Und das ist file2:

line1
line4
line5

Dann sollte mein Ergebnis/Output sein:

line2
line3

Das funktioniert:

grep -v -f file2 file1

Aber es ist sehr, sehr langsam, wenn es für meine großen Dateien verwendet wird.

Ich vermute, es gibt eine gute Möglichkeit, dies mit diff () zu tun, aber die Ausgabe sollte nur die Zeilen sein, sonst nichts, und ich kann keinen Schalter dafür finden.

Kann mir jemand helfen, einen schnellen Weg zu finden, dies mit bash und grundlegenden Linux-Binärdateien zu tun?

EDIT: Um meine eigene Frage zu beantworten, ist dies der beste Weg, den ich bisher mit diff () gefunden habe:

diff file2 file1 | grep '^>' | sed 's/^>\ //'

Es muss doch einen besseren Weg geben?

190
Niels2000

Sie können dies erreichen, indem Sie die Formatierung der alten/neuen/unveränderten Zeilen in der Ausgabe GNU diff steuern:

diff --new-line-format="" --unchanged-line-format=""  file1 file2

Die Eingabedateien sollten sortiert werden damit dies funktioniert. Mit bash (und zsh) können Sie mit Prozessersetzung <( ) direkt sortieren:

diff --new-line-format="" --unchanged-line-format="" <(sort file1) <(sort file2)

Im obigen Beispiel werden neu und unverändert Zeilen unterdrückt, sodass nur geändert (dh in Ihrem Fall entfernte Zeilen) ausgegeben werden . Sie können auch einige diff Optionen verwenden, die andere Lösungen nicht bieten, z. B. -i, Um Groß-/Kleinschreibung zu ignorieren, oder verschiedene Whitespace-Optionen (-E, -b, -v Usw.) für weniger strenge Übereinstimmung.


Erklärung

Mit den Optionen --new-line-format, --old-line-format Und --unchanged-line-format Können Sie steuern, wie diff die Unterschiede formatiert, ähnlich wie bei printf Formatspezifizierern. Diese Optionen formatieren neu (hinzugefügt), alt (entfernt) bzw. unverändert Zeilen. Wenn Sie eins auf "" leer setzen, wird die Ausgabe dieser Art von Zeile verhindert.

Wenn Sie mit dem Format Unified Diff vertraut sind, können Sie es teilweise neu erstellen mit:

diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2

Der %L - Bezeichner ist die betreffende Zeile, und wir stellen jedem "+" "-" oder "" voran, wie diff -u (Beachten Sie, dass nur Differenzen ausgegeben werden, das ---+++ Und @@ Am oberen Rand jeder gruppierten Änderung). Sie können dies auch verwenden, um andere nützliche Dinge wie Nummerieren jeder Zeile mit %dn Zu tun.


Die Methode diff (zusammen mit anderen Vorschlägen comm und join) erzeugt nur die erwartete Ausgabe mit der Eingabe sortiert kann mit <(sort ...) an Ort und Stelle sortiert werden. Hier ist ein einfaches awk (nawk) Skript (inspiriert von den Skripten, die in der Antwort von Konsolebox verlinkt sind), das willkürlich geordnete Eingabedateien akzeptiert. und gibt die fehlenden Zeilen in der Reihenfolge aus Sie treten in file1 auf.

# output lines in file1 that are not in file2
BEGIN { FS="" }                         # preserve whitespace
(NR==FNR) { ll1[FNR]=$0; nl1=FNR; }     # file1, index by lineno
(NR!=FNR) { ss2[$0]++; }                # file2, index by string
END {
    for (ll=1; ll<=nl1; ll++) if (!(ll1[ll] in ss2)) print ll1[ll]
}

Dadurch wird der gesamte Inhalt von Datei1 zeilenweise in einem zeilennummernindizierten Array ll1[] Und der gesamte Inhalt von Datei2 zeilenweise in einem zeileninhaltsindizierten assoziativen Array ss2[] Gespeichert. Nachdem beide Dateien gelesen wurden, iterieren Sie über ll1 Und verwenden Sie den Operator in, um festzustellen, ob die Zeile in Datei1 in Datei2 vorhanden ist. (Dies hat eine andere Ausgabe als die diff -Methode, wenn Duplikate vorhanden sind.)

Für den Fall, dass die Dateien so groß sind, dass das Speichern beider Dateien ein Speicherproblem verursacht, können Sie die CPU gegen Speicher austauschen, indem Sie nur Datei1 speichern und Übereinstimmungen während des Lesens von Datei2 löschen.

BEGIN { FS="" }
(NR==FNR) {  # file1, index by lineno and string
  ll1[FNR]=$0; ss1[$0]=FNR; nl1=FNR;
}
(NR!=FNR) {  # file2
  if ($0 in ss1) { delete ll1[ss1[$0]]; delete ss1[$0]; }
}
END {
  for (ll=1; ll<=nl1; ll++) if (ll in ll1) print ll1[ll]
}

Das Obige speichert den gesamten Inhalt von Datei1 in zwei Arrays, eines indiziert nach Zeilennummer ll1[], Eines indiziert nach Zeileninhalt ss1[]. Beim Lesen von Datei2 wird jede übereinstimmende Zeile aus ll1[] Und ss1[] Gelöscht. Am Ende werden die verbleibenden Zeilen aus Datei1 ausgegeben, wobei die ursprüngliche Reihenfolge beibehalten wird.

In diesem Fall können Sie bei dem angegebenen Problem auch dividieren und erobern mit GNU split (die Filterung ist eine GNU - Erweiterung), Wiederholte Läufe mit Teilen von Datei1 und Lesen von Datei2 jedes Mal komplett:

split -l 20000 --filter='gawk -f linesnotin.awk - file2' < file1

Beachten Sie die Verwendung und Platzierung von - Für stdin in der gawk-Befehlszeile. Dies wird von split aus Datei1 in Blöcken von 20000 Zeilen pro Aufruf bereitgestellt.

Für Benutzer von Nicht-GNU-Systemen gibt es mit ziemlicher Sicherheit ein Coreutils-Paket GNU, das Sie unter OSX als Teil der Tools Apple Xcode erhalten, die GNU diff, awk, obwohl nur eine POSIX/BSD split und keine GNU - Version.

190
mr.spuratic

Der comm Befehl (kurz für "common") kann nützlich sein comm - compare two sorted files line by line

#find lines only in file1
comm -23 file1 file2 

#find lines only in file2
comm -13 file1 file2 

#find lines common to both files
comm -12 file1 file2 

Die man -Datei ist dafür eigentlich recht lesbar.

193
JnBrymn

Wie von konsolebox vorgeschlagen, grep die Poster-Lösung

grep -v -f file2 file1

funktioniert sehr gut (schnell), wenn Sie einfach die Option -F hinzufügen, um die Muster als feste Zeichenfolgen anstatt als reguläre Ausdrücke zu behandeln. Ich habe dies an einem Paar von ~ 1000 Zeilendateilisten überprüft, die ich vergleichen musste. Mit -F Dauerte es 0,031 s (real), während es ohne 2,278 s (real) dauerte, wenn die grep-Ausgabe auf wc -l Umgeleitet wurde.

Diese Tests umfassten auch den Schalter -x, Der Teil der Lösung ist, um die vollständige Genauigkeit in Fällen zu gewährleisten, in denen Datei2 Zeilen enthält, die mit einem Teil einer oder mehrerer Zeilen in Datei1, jedoch nicht mit allen übereinstimmen.

Eine Lösung, bei der die Eingaben nicht sortiert werden müssen, die schnell und flexibel ist (Groß-/Kleinschreibung beachten usw.) und die (meiner Meinung nach) auf jedem POSIX-System funktioniert, ist:

grep -F -x -v -f file2 file1
21
pbz

was ist die Geschwindigkeit von so sort und diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted
10
Puggan Se

Wenn Ihnen "ausgefallene Werkzeuge" fehlen, z. In einigen minimalen Linux-Distributionen gibt es eine Lösung mit nur cat, sort und uniq:

cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

Prüfung:

seq 1 1 7 | sort --random-sort > includes.txt
seq 3 1 9 | sort --random-sort > excludes.txt
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

# Output:
1
2    

Dies ist auch relativ schnell, verglichen mit grep.

7
Ondra Žižka
$ join -v 1 -t '' file1 file2
line2
line3

Das -t stellt sicher, dass die gesamte Zeile verglichen wird, wenn Sie in einigen Zeilen ein Leerzeichen hatten.

5
Steven Penny

Sie können Python verwenden:

python -c '
lines_to_remove = set()
with open("file2", "r") as f:
    for line in f.readlines():
        lines_to_remove.add(line.strip())

with open("f1", "r") as f:
    for line in f.readlines():
        if line.strip() not in lines_to_remove:
            print(line.strip())
'
2
HelloGoodbye

Die Verwendung von fgrep oder das Hinzufügen der Option -F zu grep könnte Abhilfe schaffen. Für schnellere Berechnungen können Sie jedoch Awk verwenden.

Sie können eine dieser Awk-Methoden ausprobieren:

http://www.linuxquestions.org/questions/programming-9/grep-for-huge-files-826030/#post4066219

2
konsolebox

Die Art, wie ich das normalerweise mache, ist die Verwendung von --suppress-common-lines flag, beachte jedoch, dass dies nur funktioniert, wenn du es im Side-by-Side-Format machst.

diff -y --suppress-common-lines file1.txt file2.txt

1
BAustin