Uniq eine CSV-Datei, die eine Spalte ignoriert, awk vielleicht?

7

Angesichts dieser Datei (Anmerkungen sind nicht Teil der Datei, sondern Teil der Erklärung) ...

x,a,001,b,c,d,y
x,a,002,b,c,e,yy
x,bb,003,b,d,e,y
x,c,004,b,d,e,y
x,c,005,b,d,e,y   # nb - dupe of row 4
x,dd,006,b,d,e,y
x,c,007,b,d,e,y   # nb - dupe of row 4 and 5
x,dd,008,b,d,f,y
x,dd,009,b,d,e,y   # nb - dupe of row 6
x,e,010,b,d,f,y

... Ich möchte folgende Ausgabe ableiten:

x,a,001,b,c,d,y
x,a,002,b,c,e,yy
x,bb,003,b,d,e,y
x,c,004,b,d,e,y
x,dd,006,b,d,e,y
x,dd,008,b,d,f,y
x,e,010,b,d,f,y

Wenn Spalte 3 aus der Datei herausgeschnitten wurde und dann uniq über die Datei ausgeführt wurde. Wenn für die verbleibenden Zeilen der Wert für Spalte 3 wieder an der richtigen Stelle hinzugefügt wurde, würde ich das obige Ergebnis erhalten.

Aber ich kämpfe wirklich darum, etwas zu finden, das dies tun würde. Ich würde eine Gelegenheit begrüßen, mehr über die Textverarbeitungsprogramme von Linux zu erfahren.

Leistung: Dateien werden wahrscheinlich nicht größer als 1 MB, und es gibt nur 1 Datei pro Tag.

Ziel: Debian GNU / Linux 7 amd64, 256 MB / Xeon.

Bearbeiten: Beispiel optimiert, da Felder keine feste Breite haben und eine Lösung mit uniq --skip-chars=n, soweit ich das beurteilen kann, nicht funktioniert.

jon
quelle
Sie waren auf dem richtigen Weg und suchten nach Optionen, uniqum meine aktualisierte Antwort zu überprüfen. :)
Peterph

Antworten:

18

Mit awkkönnten Sie tun:

awk -F, -vOFS=, '{l=$0; $3=""}; ! ($0 in seen) {print l; seen[$0]}'
Stéphane Chazelas
quelle
2
Wow, elegant und einfach (und schnell, wahrscheinlich auch mit Hash-Lookups, um sie mit vorherigen Zeilen zu vergleichen). Entfernt es jedoch nicht auch Duplikate, die nach etwas dazwischen auftreten? (dh anders als "Uniq wurden in der Datei ausgeführt [wenn die dritte Spalte entfernt wurde]", wie vom OP angefordert? dh: Zeile1 = "x, a, 001, b, c, d, y", dann Zeile12 = "x, a, 999, b, c, d, y "würde nicht mit Ihrer Lösung erscheinen, sollte aber (vielleicht)?)
Olivier Dulac
2
Sie haben Recht, dass es Zeilen nach etwas dazwischen entfernt, und Sie haben Recht, dass uniq das nicht tun würde. Aber wenn Sie sich das OP ansehen, scheint er geglaubt zu haben, dass uniq so handeln würde, wie es dieses Skript tut, also ist dieses Skript wahrscheinlich das, was er eigentlich wollte.
The Spooniest
@ TheSpooniest: gut, dann definitiv +1 an Stephane, um das XYProblem durchzulesen ^^
Olivier Dulac
7

Der einfachste Weg :

sort -u -t, -k1,2 -k4
  • -u: gibt nur die erste Zeile gleich aus
  • -t,: Komma als Feldtrennzeichen verwenden
  • -k1,2 -k4: sortiere nur nach den Feldern 1,2 und 4 und dem Rest

Eine andere Option ist das Neuanordnen der Daten mit sed(beachten Sie die GNU-Option -r) auf beiden Seiten - dies erfordert, dass die Datensätze größtenteils eine feste Länge haben, andernfalls wird es fehlschlagen (und nur kaum merklich):

sed -r       's/^([^,]+,[^,]+)(,[^,]+)(.*)$/\1\3\2/' \
    | sort \
    | uniq -w 12 \
    | sed -r 's/^([^,]+,[^,]+)(.*)(,[^,]+)$/\1\3\2/'

sortWenn Sie möchten, können Sie am Ende eine weitere hinzufügen , um sie nach Zahlen -kzu sortieren (verwenden Sie die Option, um auszuwählen, wie die Sortierung durchgeführt werden soll - z. B. so etwas wie sed -k3 -t,).

In Perl können Sie beispielsweise die Teile, für die Sie die Eindeutigkeit bestimmen möchten, als Schlüssel in einem Hash verwenden (die Werte die vollständigen Zeilen) und nur dann in den Hash einfügen, wenn der Schlüssel noch nicht definiert ist. Dies ist natürlich viel flexibler als das Verwenden sed(oder awk), aber auch das Schreiben (ich bin weit entfernt von einem Perl-Guru, daher ist es sehr wahrscheinlich, dass es viel eleganter gemacht werden kann - siehe andere Antworten für Perl-like Perl-Lösungen):

#!/usr/bin/perl
use strict;

my %lines;
while (<>) {
    (my $k1, my $v, my $k2) = /^([^,]+,[^,]+,)([^,]+)(,.*)$/;
    my $k = $k1 . $k2;
    if (!exists($lines{$k})) {
        $lines{$k} = $_;
    }
}

for my $k (sort(keys(%lines))) {
    print $lines{$k};
}
Peterph
quelle
Danke, leider haben Felder keine feste Breite. Ich habe die Frage aktualisiert, entschuldige. Ihr Beispiel funktioniert auf meinem System weder für alte noch für überarbeitete Testfälle :(
Jon
meine Antwort zu löschen und deine zu bewerten - scheint den von mir beschriebenen Algorithmus grob zu implementieren. Ich würde wahrscheinlich eher Split als Regexp für die Feldextraktion verwenden, und es wäre viel einfacher mit just$lines{$k} = $_ unless $lines{$k};
cas
Genial, +1! Ich habe versucht , dass mit dem zu tun uniq's Feldoptionen und konnte nicht, hätte nicht gedacht , zu verwenden sortist -u. Übrigens denke ich , dass dies sort -ueine GNU-Erweiterung ist, nicht POSIX, aber dies wird auf Linux-Systemen gut funktionieren.
Terdon
@terdon Ich denke du hast recht, dass es eine Erweiterung ist.
Peterph
Schöne und elegante Lösung! (der perleine) Als totaler Perl-Neuling brauchte ich jedoch ein wenig RTFM, um zu verstehen, was Sie hier machten. %lines(leicht erkennbar am Prozentzeichen) ist ein assoziatives Array (im Perl-Jargon auch als "Hash-Variable" bezeichnet), das möglicherweise "echte" Zeichenfolgen als Schlüsselkennungen akzeptiert, nicht nur Indexnummern. Dies ist das Element, das für all die wundersame "Magie" verantwortlich ist, die hier getan wird.
Syntaxfehler
3

Ein Weg, dies zu tun mit awk | sort | uniq | awk:

awk -F, '{a=$1;$1=$3;$3=a;print}' file | sort -k 2 | uniq -f 1 | awk -v OFS=',' '{a=$1;$1=$3;$3=a;print}'
eilen
quelle
2

Ein einfacherer Perl-Weg wäre:

perl -F"," -ane '$a=join(",",@F[0,1,3 .. $#F]); print unless $k{$a}; $k{$a}++' file

Das -ateilt Felder in das @FArray auf und -F","setzt das Feldtrennzeichen auf ,. -nbedeutet, dass das von -ein jeder Zeile der Eingabedatei angegebene Skript ausgeführt wird .

Die Idee ist, ein Array-Slice (Elemente 0,1 und 3 bis zum Ende des Arrays) zu nehmen, sie zu einem String ( $a) zu verbinden und diesen String als Hash-Referenz (assoziatives Array) zu verwenden. Sie drucken dann jede Zeile nur, wenn der Hash-Schlüssel zuvor noch nicht gesehen wurde.

terdon
quelle
Das würde das sagen ab,c,1,dund a,bc,2,dist das gleiche. Du brauchst join(",". Sie können auch optimieren, indem Sie das $k{$a}++in den unless() { }Block verschieben. Und dann wäre das gleichbedeutend mit meiner awkLösung ;-).
Stéphane Chazelas
Ich denke nicht, dass es sich identifizieren würde ab,c,1,dund a,bc,2,dals identisch - der Vergleich wird an einer rekonstruierten Zeichenfolge durchgeführt (mit den Kommas an den richtigen Stellen).
Peterph
@ Peterph ja, aber das liegt daran, dass ich den Fehler, den Stephane entdeckt und hinzugefügt hat, bereits korrigiert habe join(",".
Terdon
2
Es ist nur so, dass du es nicht brauchst, $k{$a}++wenn $aes schon drin ist %k. Sie könnten es kürzer machen mit:perl -F, -ane'print if!$k{join",",@F[0,1,3..-1]}++'
Stéphane Chazelas
1
Stephane, Ihr letzter Vorschlag liefert nicht die erwartete Ausgabe, Terdons Perl in der bearbeiteten Antwort schon.
bbaassssiiee