Esiste un modo per "uniq" per colonna?


195

Ho un file .csv come questo:

stack2@example.com,2009-11-27 01:05:47.893000000,example.net,127.0.0.1
overflow@example.com,2009-11-27 00:58:29.793000000,example.net,255.255.255.0
overflow@example.com,2009-11-27 00:58:29.646465785,example.net,256.255.255.0
...

Devo rimuovere le e-mail duplicate (l'intera riga) dal file (ovvero una delle righe che contengono overflow@example.coml'esempio sopra). Come si usa uniqsolo sul campo 1 (separato da virgole)? Secondo man, uniqnon ha opzioni per le colonne.

Ho provato qualcosa sort | uniqma non funziona.

Risposte:


325
sort -u -t, -k1,1 file
  • -u per unico
  • -t, quindi la virgola è il delimitatore
  • -k1,1 per il campo chiave 1

Risultato del test:

overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0 
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1 

3
questo non funziona se la colonna contiene la virgola stessa (con virgolette)
— user775187

13
perché hai bisogno di, 1 in -k1,1? perché non solo -k1?
— hello_there_andy,

18
@hello_there_andy: questo è spiegato nel manuale ( man sort). Indica la posizione di inizio e fine.
— Serrano,

3
@CarlSmotricz: l'ho testato e ha confermato ciò sortche dice la manpage: " -u, --unique con -c, controlla per ordine rigoroso; senza -c, emette solo il primo di una corsa uguale ". Quindi, è davvero "la prima occorrenza del duplicato prima dell'ordinamento".
— Geremia,

2
questo cambia anche l'ordine delle linee, no?
— rkachach,

102
awk -F"," '!_[$1]++' file
  • -F imposta il separatore di campo.
  • $1 è il primo campo.
  • _[val]cerca valnell'hash_ (una variabile regolare).
  • ++ incrementare e restituire il vecchio valore.
  • ! non restituisce logico.
  • c'è una stampa implicita alla fine.

4
Questo approccio è due volte più veloce
— dell'ordinamento

9
Questo ha anche l'ulteriore vantaggio di mantenere le linee nell'ordine originale!
— AffluentOwl

8
Se hai bisogno dell'ultimo uniq invece del primo, questo script awk ti aiuterà:awk -F',' '{ x[$1]=$0 } END { for (i in x) print x[i] }' file
— Sukima,

3
@eshwar basta aggiungere altri campi all'indice del dizionario! Ad esempio, !_[$1][$2]++può essere utilizzato per ordinare in base ai primi due campi. Il mio awk-fu non è abbastanza forte da essere in grado di essere unico su una vasta gamma di campi, però. :(
— Soham Chowdhury,

1
Brillante! questa opzione è migliore della risposta perché mantiene l'ordine delle righe
— rkachach

16

Considerare più colonne.

Ordina e dai un elenco univoco basato sulla colonna 1 e sulla colonna 3:

sort -u -t : -k 1,1 -k 3,3 test.txt
  • -t : i due punti sono separatori
  • -k 1,1 -k 3,3 basato sulla colonna 1 e colonna 3

8

o se vuoi usare uniq:

<mycvs.cvs tr -s ',' ' ' | awk '{print $3" "$2" "$1}' | uniq -c -f2

dà:

1 01:05:47.893000000 2009-11-27 tack2@domain.com
2 00:58:29.793000000 2009-11-27 overflow@domain2.com
1

5
Vorrei sottolineare una possibile semplificazione: è possibile scaricare il cat! Invece di eseguire il piping in tr, lascia che tr legga il file usando <. Piping through catè una complicazione inutile comune usata dai principianti. Per grandi quantità di dati c'è un effetto prestazionale da avere.
— Carl Smotricz,

4
Buono a sapersi. Grazie! (Naturalmente questo ha senso, pensando a "gatto" e "pigrizia";))
— Carsten C.

L'inversione dei campi può essere semplificata con rev.
— Hielke Walinga,

5

Se si desidera conservare l'ultimo dei duplicati che è possibile utilizzare

 tac a.csv | sort -u -t, -r -k1,1 |tac

Qual era il mio requisito

Qui

tac inverte il file riga per riga


1

Ecco un modo molto elegante.

Prima formatta il contenuto in modo tale che la colonna da confrontare per unicità sia una larghezza fissa. Un modo per farlo è usare awk printf con un identificatore di larghezza di campo / colonna ("% 15s").

Ora le opzioni -f e -w di uniq possono essere usate per saltare i campi / le colonne precedenti e per specificare la larghezza del confronto (larghezza delle colonne).

Ecco tre esempi.

Nel primo esempio ...

1) Rendi temporaneamente la colonna di interesse una larghezza fissa maggiore o uguale alla larghezza massima del campo.

2) Utilizzare l'opzione -f uniq per saltare le colonne precedenti e utilizzare l'opzione -w uniq per limitare la larghezza a tmp_fixed_width.

3) Rimuovi gli spazi finali dalla colonna per "ripristinarne" la larghezza (supponendo che prima non ci fossero spazi finali).

printf "%s" "$str" \
| awk '{ tmp_fixed_width=15; uniq_col=8; w=tmp_fixed_width-length($uniq_col); for (i=0;i<w;i++) { $uniq_col=$uniq_col" "}; printf "%s\n", $0 }' \
| uniq -f 7 -w 15 \
| awk '{ uniq_col=8; gsub(/ */, "", $uniq_col); printf "%s\n", $0 }'

Nel secondo esempio ...

Creare una nuova colonna uniq 1. Quindi rimuoverla dopo aver applicato il filtro uniq.

printf "%s" "$str" \
| awk '{ uniq_col_1=4; printf "%15s %s\n", uniq_col_1, $0 }' \
| uniq -f 0 -w 15 \
| awk '{ $1=""; gsub(/^ */, "", $0); printf "%s\n", $0 }'

Il terzo esempio è uguale al secondo, ma per più colonne.

printf "%s" "$str" \
| awk '{ uniq_col_1=4; uniq_col_2=8; printf "%5s %15s %s\n", uniq_col_1, uniq_col_2, $0 }' \
| uniq -f 0 -w 5 \
| uniq -f 1 -w 15 \
| awk '{ $1=$2=""; gsub(/^ */, "", $0); printf "%s\n", $0 }'

-3

bene, più semplice che isolare la colonna con awk, se hai bisogno di rimuovere tutto con un certo valore per un dato file, perché non fare semplicemente grep -v:

ad es. per eliminare tutto con il valore "col2" nella seconda riga: col1, col2, col3, col4

grep -v ',col2,' file > file_minus_offending_lines

Se questo non è abbastanza buono, perché alcune righe potrebbero essere rimosse in modo improprio dal fatto che il valore corrispondente venga mostrato in una colonna diversa, puoi fare qualcosa del genere:

awk per isolare la colonna offensiva: ad es

awk -F, '{print $2 "|" $line}'

-F imposta il campo delimitato da ",", $ 2 indica la colonna 2, seguita da un delimitatore personalizzato e quindi l'intera riga. È quindi possibile filtrare rimuovendo le righe che iniziano con il valore offensivo:

 awk -F, '{print $2 "|" $line}' | grep -v ^BAD_VALUE

e quindi rimuovere le cose prima del delimitatore:

awk -F, '{print $2 "|" $line}' | grep -v ^BAD_VALUE | sed 's/.*|//g'

(nota: il comando sed è sciatto perché non include valori di escape. Inoltre, il modello sed dovrebbe essere qualcosa di simile a "[^ |] +" (cioè qualsiasi cosa non il delimitatore). Speriamo che sia abbastanza chiaro.


3
Non vuole eliminare le righe, vuole conservare una singola copia di una riga con una stringa specifica. Uniq è il caso d'uso giusto.
— ingyhere il

-3

Ordinando prima il file sort, è quindi possibile applicareuniq .

Sembra ordinare il file bene:

$ cat test.csv
overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

$ sort test.csv
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 
overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

$ sort test.csv | uniq
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 
overflow@domain2.com,2009-11-27 00:58:29.793000000,xx3.net,255.255.255.0
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1

Potresti anche fare un po 'di magia AWK:

$ awk -F, '{ lines[$1] = $0 } END { for (l in lines) print lines[l] }' test.csv
stack2@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack4@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
stack3@domain.com,2009-11-27 01:05:47.893000000,xx2.net,127.0.0.1
overflow@domain2.com,2009-11-27 00:58:29.646465785,2x3.net,256.255.255.0 

Questo non è univoco per colonna, come richiesto nella domanda. Questo è unico per l'intera linea. Inoltre, non è necessario eseguire un ordinamento per eseguire un uniq. I due si escludono a vicenda.
— Javid Jamae,

1
Si hai ragione. L'ultimo esempio fa comunque la domanda, anche se la risposta accettata è molto più chiara. Per quanto riguarda sort, quindi uniq, sortdeve essere fatto prima di fare uniqaltrimenti non funziona (ma puoi saltare il secondo comando e semplicemente usare sort -u). Da uniq(1): "Filtra le linee corrispondenti adiacenti da INPUT (o input standard), scrivendo su OUTPUT (o output standard)."
— Mikael S,

Ah, hai ragione sull'ordinamento prima di uniq. Non ho mai capito che uniq funziona solo su linee adiacenti. Immagino di usare sempre solo sort -u.
— Javid Jamae,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.