Come rimuovere le linee duplicate all'interno di un file di testo?


126

Un mio enorme file di testo (fino a 2 GiB) contiene circa 100 duplicati esatti di ogni riga (inutile nel mio caso, poiché il file è una tabella di dati simile a CSV).

Ciò di cui ho bisogno è di rimuovere tutte le ripetizioni mentre (preferibilmente, ma questo può essere sacrificato per un significativo aumento delle prestazioni) mantenendo l'ordine originale della sequenza. Nel risultato ogni riga deve essere unica. Se ci fossero 100 linee uguali (di solito i duplicati sono distribuiti nel file e non saranno vicini), ne rimarrà solo uno del tipo.

Ho scritto un programma in Scala (consideralo Java se non conosci Scala) per implementarlo. Ma forse ci sono strumenti nativi scritti in C più veloci in grado di farlo più velocemente?

AGGIORNAMENTO: la awk '!seen[$0]++' filenamesoluzione sembrava funzionare bene per me fintanto che i file erano vicini a 2 GiB o più piccoli, ma ora come sto per ripulire un file da 8 GiB non funziona più. Sembra che l'infinito su un Mac con 4 GB di RAM e un PC Windows 7 a 64 bit con 4 GB di RAM e 6 GB di memoria si esaurisca. E non mi sento entusiasta di provarlo su Linux con 4 GB di RAM data questa esperienza.


questo distruggerà il tuo ordine ma, hai provato sort -u, non ho idea di come o se possa essere eseguito su un file così massiccio
0x7c0

5
C spesso non è significativamente più veloce di Java e se lo stai eseguendo (in ordine) ora, c'è una buona probabilità che finisca prima di ottenere una risposta qui, implementala e termina l'esecuzione; fuori servizio, sort -uprobabilmente sarà più veloce.
Kevin

Risposte:


215

Una awksoluzione vista su #bash (Freenode):

awk '!seen[$0]++' filename

1
Ho appena provato questo su un file 2G e ci sono voluti tre minuti sul mio notebook. Non male. Ho anche provato il nome file uniq | awk '! visto [$ 0] ++', ma non è stato più veloce.
mgjk,

Questo è sorprendentemente più veloce di una awkversione più dettagliata usando 2 ricerche di array (mostrate come spiegazione estesa nella risposta di Gilles): 0m36.132s contro 0m49.958s .. per 50 milioni di righe .. Pensavo che il collo di bottiglia sarebbe stato l'I / O, ma la ricerca extra dell'array è ... 1 milione di elementi nell'array sembrano fare un
bel po 'di male

Ma come si confronta con l'ordinamento -u ....?
HashWizard,

1
@HashWizard: questo comando non ordina, ma elimina ogni occorrenza successiva della stessa riga
enzotib

1
@MaxWilliams sì, funziona se sono distribuiti casualmente.
Setholopolus,

47

Esiste un metodo semplice (che non è ovvio) che utilizza utility standard che non richiedono una grande memoria se non per essere eseguite sort, che nella maggior parte delle implementazioni ha ottimizzazioni specifiche per file di grandi dimensioni (un buon algoritmo di ordinamento esterno). Un vantaggio di questo metodo è che circola solo su tutte le linee all'interno di utility per scopi speciali, mai all'interno di linguaggi interpretati.

<input nl -b a -s : |           # number the lines
sort -t : -k 2 -u |             # sort and uniquify ignoring the line numbers
sort -t : -k 1n |               # sort according to the line numbers
cut -d : -f 2- >output          # remove the line numbers

Se tutte le righe iniziano con un carattere non bianco, puoi rinunciare ad alcune delle opzioni:

<input nl | sort -k 2 -u | sort -k 1n | cut -f 2- >output

Per una grande quantità di duplicazione, un metodo che richiede solo l'archiviazione di una singola copia di ogni riga in memoria funzionerà meglio. Con qualche overhead di interpretazione, c'è uno script awk molto conciso per quello (già pubblicato da enzotib ):

<input awk '!seen[$0]++'

Meno concisamente: ad !seen[$0] {print} {seen[$0] += 1}esempio, stampare la riga corrente se non è stata ancora vista, quindi incrementare il seencontatore per questa riga (le variabili non inizializzate o gli elementi dell'array hanno il valore numerico 0).

Per le righe lunghe, è possibile risparmiare memoria mantenendo solo un checksum non spoofable (ad esempio un digest crittografico) di ciascuna riga. Ad esempio, utilizzando SHA-1, sono necessari solo 20 byte più un overhead costante per riga. Ma il calcolo dei digest è piuttosto lento; questo metodo vincerà solo se hai una CPU veloce (in particolare una con un acceleratore hardware per calcolare i digest) e non molta memoria relativa alla dimensione del file e linee sufficientemente lunghe. Nessuna utilità di base consente di calcolare un checksum per ogni riga; dovresti sopportare l'interpretazione generale di Perl / Python / Ruby / ... o scrivere un programma compilato dedicato.

<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output

@Gilles In base alla tua spiegazione di awk '!seen[$0]++', significa che se awk vede 2 righe duplicate, manterrà sempre la prima e ignorerà tutte le successive? (O manterrà l'ultimo?)
user779159,

1
@ user779159 Mantiene la prima: ogni riga di input viene stampata immediatamente (prima occorrenza) o per niente (ripetizione ricorrente).
Gilles,

Ma come si confronta con l'ordinamento -u ...?
HashWizard,

@HashWizard Un piano sort -ucambia l'ordine. La mia risposta mostra soluzioni che preservano l'ordine (l'ordine delle prime occorrenze, per essere precisi).
Gilles,

@Gilles diresti che è più veloce dell'ordinamento -u per file di grandi dimensioni (10G) con duplicati del 50%?
HashWizard,

25
sort -u big-csv-file.csv > duplicates-removed.csv

Si noti che il file di output verrà ordinato.


1
Non veloce come il awkcomando in altre risposte, ma concettualmente semplice!
Johann,

@Johann Lo sto facendo abbastanza spesso su file con centinaia di migliaia (persino milioni) di stringhe con terminazione newline brevi. Ottengo i risultati abbastanza rapidamente per gli esperimenti che sto facendo. Può essere più importante se utilizzato negli script che vengono eseguiti più volte, i risparmi nel tempo possono essere considerevoli.
Vladislavs Dovgalecs,

1
Utilizzare sort -uper rimuovere i duplicati durante l'ordinamento, piuttosto che dopo. (E salva la larghezza di banda della memoria) eseguendo il piping su un altro programma). Questo è migliore della awkversione solo se si desidera ordinare anche l'output. (L'OP su questa domanda vuole che il suo ordinamento originale sia preservato , quindi questa è una buona risposta per un caso d'uso leggermente diverso.)
Peter Cordes,

Ho impiegato circa un minuto, per me, per un file di 5,5 milioni di righe (1,8 GB in totale). Brillante.
Max Williams,

18

Supponendo che tu possa permetterti di conservare quanto il file de-duplicato in memoria (se i tuoi dati sono effettivamente duplicati di un fattore 100, che dovrebbe essere di circa 20 MiB + overhead), puoi farlo molto facilmente con Perl.

$ perl -ne 'print unless $dup{$_}++;' input_file > output_file

Questo preserva anche l'ordine.

%dupSe lo desideri, puoi estrarre il numero di occorrenze di ogni riga dall'hash, come bonus gratuito aggiunto.

Se preferisci awk, anche questo dovrebbe farlo (stessa logica della versione perl, stesso ordine, stessi dati raccolti nella dupvariabile):

$ awk '{if (++dup[$0] == 1) print $0;}' input_file > output_file

Questo è troppo buono @Mat, stavo per slurp il file, lol ;-).
Nikhil Mulley,

Ora aspetto @ManAtWork anche per la sua sed e la sua magica trama magica :-)
Nikhil Mulley

di nuovo fantastico per il suggerimento awk :-)
Nikhil Mulley

1
È possibile modificare lo script perl per rimuovere solo le righe adiacenti duplicate?
silente

2
@dumbledad: uniqfa tutto da solo
Mat

3

Come nessun'altra risposta fornita supporto sul posto, eccone una:

gawk -i inplace '!a[$0]++' file

Questo preserva l'ordine? A proposito, questo non ha funzionato per me. La mia versione è:GNU Awk 4.0.2
Leonid,

1
@Leonid sì, lo fa. Stampa la prima occorrenza di una riga univoca. Il supporto sul posto è stato introdotto per la prima volta nella versione 4.1, che è stata rilasciata nel 2013.
Jan Chren - rindeal

3

Puoi usare uniq http://www.computerhope.com/unix/uuniq.htm

uniq riporta o filtra le righe ripetute in un file.


Quando dai una risposta è preferibile dare una spiegazione del PERCHÉ la tua risposta è quella. Quindi, in che modo questa risposta differisce da molte delle risposte precedenti?
Stephen Rauch,

1
Dalla pagina man uniq: Nota: 'uniq' does not detect repeated lines unless they are adjacent. Quindi devi prima ordinarlo e perdere l'ordine delle righe non duplicate.
Vindolin,

2

Fodere Python One:

python -c "import sys; lines = sys.stdin.readlines(); print ''.join(sorted(set(lines)))" < InputFile

questo fa sì che l'intero file venga trascinato nella memoria e potrebbe non essere adatto al problema dell'OP. Inoltre, non è garantito il mantenimento dell'ordine
iruvar,

Grazie per il suggerimento, ho appena imparato Python .. ho appena provato questo a scopo di apprendimento .. :)
Rahul Patil,

Ecco una versione di Python 2.7 che non è una riga, ma (in modo succinto) restituisce linee uniche che preservano l'ordine senza caricare l'intero file in memoria o creare una singola stringa gigantesca da alimentare per la stampa
iruvar

Grazie @ 1_CR Ho qualcosa da imparare oggi :)OrderedDict
Rahul Patil,

0

Nessuna delle risposte qui ha funzionato per me sul mio Mac, quindi ho scritto un semplice script Python che funziona per me. Sto ignorando gli spazi bianchi iniziali / finali e inoltre non mi interessa il consumo di memoria.

import sys

inputfile = sys.argv[1]
outputfile = sys.argv[2]

with open(inputfile) as f:
    content = f.readlines()

content = [x.strip() for x in content]

my_list = list(set(content))

with open(outputfile, 'w') as output:
    for item in my_list:
        output.write("%s\n" % item)

Salvare quanto sopra in unique.py ed eseguire in questo modo:

python unique.py inputfile.txt outputfile.txt

-1

Con bash 4 è possibile utilizzare una soluzione pure-bash che sfrutta gli array associativi . Ecco un esempio

unset llist; declare -A llist;
while read -r line; do
if [[ ${llist[$line]} ]]; then
  continue
else 
  printf '%s\n' "$line"
  llist[$line]="x"
fi
done < file.txt

2
Non utilizzare i readloop per elaborare file di testo di grandi dimensioni. bash deve leggere un byte alla volta per evitare il superamento di una nuova riga. Bash non è anche molto veloce nell'elaborazione del testo in generale rispetto a awk. Se lo usi, read -raeviterai di mangiare barre rovesciate nei tuoi input. Inoltre, non dimenticare di unset llist dopo il ciclo, se lo metti in una funzione shell o lo usi in modo interattivo.
Peter Cordes,

2
@PeterCordes, o avresti potuto fare solo riferimento a questo :-)
iruvar
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.