Wie lösche ich doppelte Zeilen in einer Datei, ohne sie unter Unix zu sortieren?

136

Gibt es eine Möglichkeit, doppelte Zeilen in einer Datei unter Unix zu löschen?

Ich kann es mit sort -uund uniqBefehlen tun , aber ich möchte sedoder verwenden awk. Ist das möglich?

Vijay
quelle
11
Wenn Sie aufeinanderfolgende Duplikate meinen, uniqreicht es aus.
Michael Krelin - Hacker
und ansonsten glaube ich, dass es mit awkgrößeren Dateien möglich ist , aber bei größeren Dateien ziemlich ressourcenintensiv sein wird.
Michael Krelin - Hacker
Duplikate stackoverflow.com/q/24324350 und stackoverflow.com/q/11532157 haben interessante Antworten, die idealerweise hier migriert werden sollten.
Tripleee

Antworten:

290
awk '!seen[$0]++' file.txt

seenist ein assoziatives Array, an das Awk jede Zeile der Datei weitergibt. Wenn sich keine Zeile im Array befindet, seen[$0]wird false ausgewertet. Das !ist ein logischer NICHT-Operator und invertiert das Falsche in Wahr. Awk druckt die Zeilen, in denen der Ausdruck true ergibt. Die ++Inkremente, seenso dass seen[$0] == 1nach dem ersten Mal eine Zeile gefunden wird und dann seen[$0] == 2und so weiter.
Awk wertet alles außer 0und ""(leere Zeichenfolge) als wahr aus. Wenn eine doppelte Linie in platziert seendann !seen[$0]wird falsch bewerten und die Linie nicht in die Ausgabe geschrieben werden.

Jonas Elfström
quelle
5
Um es in einer Datei zu speichern, können wir dies tunawk '!seen[$0]++' merge_all.txt > output.txt
Akash Kandpal
5
Eine wichtige Einschränkung hier: Wenn Sie dies für mehrere Dateien tun müssen und am Ende des Befehls weitere Dateien anheften oder einen Platzhalter verwenden, wird das Array "gesehen" mit doppelten Zeilen aus ALLEN Dateien gefüllt. Wenn Sie stattdessen jede Datei unabhängig behandeln möchten, müssen Sie etwas tun wiefor f in *.txt; do gawk -i inplace '!seen[$0]++' "$f"; done
Nick K9
@ NickK9, dass das kumulative De-Duping über mehrere Dateien hinweg an sich schon fantastisch ist. Netter Tipp
sfscs
31

Von http://sed.sourceforge.net/sed1line.txt : (Bitte frag mich nicht, wie das funktioniert ;-))

 # delete duplicate, consecutive lines from a file (emulates "uniq").
 # First line in a set of duplicate lines is kept, rest are deleted.
 sed '$!N; /^\(.*\)\n\1$/!P; D'

 # delete duplicate, nonconsecutive lines from a file. Beware not to
 # overflow the buffer size of the hold space, or else use GNU sed.
 sed -n 'G; s/\n/&&/; /^\([ -~]*\n\).*\n\1/d; s/\n//; h; P'
Andre Miller
quelle
Geekery ;-) +1, aber Ressourcenverbrauch ist unvermeidlich.
Michael Krelin - Hacker
3
'$! N; /^(.*)\n\1$/!P; D 'bedeutet "Wenn Sie nicht in der letzten Zeile sind, lesen Sie in einer anderen Zeile. Schauen Sie sich jetzt an, was Sie haben, und wenn es NICHT Zeug ist, gefolgt von einer neuen Zeile und dann wieder dasselbe Zeug, drucken Sie das Zeug aus. Löschen Sie es jetzt." das Zeug (bis zur Newline). "
Beta
2
'G; s / \ n / && /; / ^ ([- ~] * \ n). * \ n \ 1 / d; s / \ n //; h; P 'bedeutet ungefähr: "Hänge den gesamten Haltebereich an diese Zeile an. Wenn du dann eine doppelte Zeile siehst, wirf das Ganze raus, andernfalls kopiere das ganze Durcheinander zurück in den Haltebereich und drucke den ersten Teil (das ist die Zeile, die du gerade hast) lesen. "
Beta
Ist das $!Teil notwendig? Tut nicht sed 'N; /^\(.*\)\n\1$/!P; D'das Gleiche? Ich kann mir kein Beispiel ausdenken, bei dem die beiden auf meinem Computer unterschiedlich sind (fwiw, ich habe am Ende mit beiden Versionen eine leere Zeile ausprobiert und beide waren in Ordnung).
Eddi
1
Fast 7 Jahre später und niemand antwortete @amichair ... <sniff> macht mich traurig. ;) Stellt jedenfalls [ -~]einen Bereich von ASCII-Zeichen von 0x20 (Leerzeichen) bis 0x7E (Tilde) dar. Diese gelten als die druckbaren ASCII - Zeichen (gelinkten Seite hat auch 0x7F / löschen , aber das scheint nicht richtig). Das macht die Lösung für jeden kaputt, der kein ASCII verwendet oder beispielsweise Tabulatorzeichen verwendet. Je portabler es ist, [^\n]desto mehr Zeichen sind enthalten ... alle außer einem.
B-Schicht
14

Perl-Einzeiler ähnlich der awk-Lösung von @ jonas:

perl -ne 'print if ! $x{$_}++' file

Diese Variante entfernt nach dem Vergleich nachgestellte Leerzeichen:

perl -lne 's/\s*$//; print if ! $x{$_}++' file

Diese Variante bearbeitet die Datei direkt:

perl -i -ne 'print if ! $x{$_}++' file

Diese Variante bearbeitet die Datei direkt und erstellt eine Sicherung file.bak

perl -i.bak -ne 'print if ! $x{$_}++' file
Chris Koknat
quelle
6

Der Einzeiler, den Andre Miller oben gepostet hat, funktioniert mit Ausnahme der neuesten Versionen von sed, wenn die Eingabedatei mit einer Leerzeile und ohne Zeichen endet. Auf meinem Mac dreht sich meine CPU nur.

Endlosschleife, wenn die letzte Zeile leer ist und keine Zeichen enthält :

sed '$!N; /^\(.*\)\n\1$/!P; D'

Hängt nicht, aber Sie verlieren die letzte Zeile

sed '$d;N; /^\(.*\)\n\1$/!P; D'

Die Erklärung befindet sich ganz am Ende der sed FAQ :

Das GNU sed Maintainer das Gefühl , dass trotz der Portabilität Probleme
würde dies dazu führen, den N - Befehl Ändern drucken (anstatt
der Musterraum konsistentere war löschen) mit einem Intuition
darüber , wie ein Befehl „fügen Sie die nächste Zeile“ soll verhalten.
Eine weitere Tatsache, die die Änderung begünstigte, war, dass "{N; Befehl;}"
die letzte Zeile löscht, wenn die Datei eine ungerade Anzahl von Zeilen enthält, aber
die letzte Zeile druckt, wenn die Datei eine gerade Anzahl von Zeilen enthält.

Ändern Sie ein einzelnes "N", um Skripte, die das frühere Verhalten von N (Löschen
des Musterbereichs beim Erreichen des EOF) verwendeten, in Skripte zu konvertieren, die mit
allen Versionen von sed kompatibel sind. zu "$ d; N;" .

Bradley Kreider
quelle
5

Eine alternative Möglichkeit mit Vim (Vi-kompatibel) :

Löschen Sie doppelte, aufeinanderfolgende Zeilen aus einer Datei:

vim -esu NONE +'g/\v^(.*)\n\1$/d' +wq

Löschen Sie doppelte, nicht aufeinanderfolgende und nicht leere Zeilen aus einer Datei:

vim -esu NONE +'g/\v^(.+)$\_.{-}^\1$/d' +wq

Bohr
quelle
4

Die erste Lösung stammt ebenfalls von http://sed.sourceforge.net/sed1line.txt

$ echo -e '1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5' |sed -nr '$!N;/^(.*)\n\1$/!P;D'
1
2
3
4
5

Die Kernidee ist:

print ONLY once of each duplicate consecutive lines at its LAST appearance and use D command to implement LOOP.

Erklärt:

  1. $!N;: Wenn die aktuelle Zeile NICHT die letzte Zeile ist, Nlesen Sie mit dem Befehl die nächste Zeile ein pattern space.
  2. /^(.*)\n\1$/!P: Wenn der Inhalt des Stroms durch pattern spacezwei duplicate stringgetrennt ist \n, was bedeutet, dass die nächste Zeile die samemit der aktuellen Zeile ist, können wir sie NICHT gemäß unserer Kernidee drucken. Andernfalls, was bedeutet, dass die aktuelle Zeile das LETZTE Erscheinungsbild aller doppelten aufeinanderfolgenden Zeilen ist, können wir jetzt den PBefehl verwenden, um die Zeichen im aktuellen pattern spaceutil \n( \nauch gedruckt) zu drucken.
  3. D: Wir verwenden den DBefehl, um die Zeichen im aktuellen pattern spaceUtil zu löschen \n( \nebenfalls gelöscht). Der Inhalt von pattern spaceist dann die nächste Zeile.
  4. und DBefehl erzwingt sed, zu seinem FIRSTBefehl zu springen, $!Nliest jedoch NICHT die nächste Zeile aus der Datei oder dem Standardeingabestream.

Die zweite Lösung ist leicht zu verstehen (von mir selbst):

$ echo -e '1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5' |sed -nr 'p;:loop;$!N;s/^(.*)\n\1$/\1/;tloop;D'
1
2
3
4
5

Die Kernidee ist:

print ONLY once of each duplicate consecutive lines at its FIRST appearance and use : command & t command to implement LOOP.

Erklärt:

  1. Lesen Sie eine neue Zeile aus dem Eingabestream oder der Eingabedatei und drucken Sie sie einmal aus.
  2. Verwenden Sie den :loopBefehlssatz a labelnamed loop.
  3. Verwenden Sie N, um die nächste Zeile in die zu lesen pattern space.
  4. Verwenden Sie s/^(.*)\n\1$/\1/diese Option, um die aktuelle Zeile zu löschen. Wenn die nächste Zeile mit der aktuellen Zeile übereinstimmt, verwenden Sie den sBefehl, um die deleteAktion auszuführen.
  5. Wenn der sBefehl erfolgreich ausgeführt wird, springen Sie mit der tloopBefehlskraft sedzum labelNamen loop, wodurch dieselbe Schleife zu den nächsten Zeilen ausgeführt wird, wenn keine doppelten aufeinander folgenden Zeilen der Zeile vorhanden sind latest printed. Andernfalls verwenden Sie den DBefehl für deletedie Zeile, die mit der identisch ist latest-printed line, und erzwingen Sie sedden Sprung zum ersten Befehl, bei dem es sich um den pBefehl handelt. Der Inhalt der aktuellen pattern spaceZeile ist die nächste neue Zeile.
Weike
quelle
Gleicher Befehl unter Windows mit Busybox:busybox echo -e "1\n2\n2\n3\n3\n3\n4\n4\n4\n4\n5" | busybox sed -nr "$!N;/^(.*)\n\1$/!P;D"
Scavenger
-1

Dies kann erreicht werden, indem awk
Below Line eindeutige Werte anzeigt

awk file_name | uniq

Sie können diese eindeutigen Werte in eine neue Datei ausgeben

awk file_name | uniq > uniq_file_name

Die neue Datei uniq_file_name enthält nur eindeutige Werte, keine Duplikate

Aashutosh
quelle
-4
cat filename | sort | uniq -c | awk -F" " '$1<2 {print $2}'

Löscht die doppelten Zeilen mit awk.

Sadhun
quelle
1
Dies stört die Reihenfolge der Zeilen.
Vijay
1
Was ist mit 20 GB Textdatei? Zu langsam.
Alexander Lubyagin
Wie immer ist das catnutzlos. Jedenfalls uniqerledigt dies bereits von selbst und erfordert nicht, dass die Eingabe genau ein Wort pro Zeile ist.
Tripleee