Come trovare linee duplicate in molti file di grandi dimensioni?


9

Ho ~ 30k file. Ogni file contiene ~ 100k righe. Una linea non contiene spazi. Le righe all'interno di un singolo file vengono ordinate e duplicate gratuitamente.

Il mio obiettivo: voglio trovare tutte le righe duplicate su due o più file e anche i nomi dei file che contenevano voci duplicate.

Una soluzione semplice sarebbe questa:

cat *.words | sort | uniq -c | grep -v -F '1 '

E poi correrei:

grep 'duplicated entry' *.words

Vedi un modo più efficiente?

Risposte:


13

Poiché tutti i file di input sono già ordinati, è possibile ignorare l'effettiva fase di ordinamento e utilizzarli solo sort -mper unire i file.

Su alcuni sistemi Unix (per quanto ne so solo Linux), potrebbe essere sufficiente farlo

sort -m *.words | uniq -d >dupes.txt

per ottenere le righe duplicate scritte nel file dupes.txt.

Per trovare i file da cui provengono queste linee, puoi farlo

grep -Fx -f dupes.txt *.words

Questo indicherà grepdi trattare le linee in dupes.txt( -f dupes.txt) come schemi di stringa fissi ( -F). greprichiederà inoltre che l'intera riga corrisponda perfettamente dall'inizio alla fine ( -x). Stampa il nome del file e la linea sul terminale.

Unices non Linux (o anche più file)

Su alcuni sistemi Unix, i nomi di file 30000 si espandono in una stringa che è troppo lunga per passare a una singola utility (il che significa sort -m *.wordsche fallirà Argument list too long, cosa che fa sul mio sistema OpenBSD). Anche Linux se ne lamenterà se il numero di file è molto più grande.

Trovare i duplicati

Ciò significa che nel caso generale (funzionerà anche con molti più di soli 30000 file), si dovrà "tagliare" l'ordinamento:

rm -f tmpfile
find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh 

In alternativa, creare tmpfilesenza xargs:

rm -f tmpfile
find . -type f -name '*.words' -exec sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh {} +

Questo troverà tutti i file nella directory corrente (o sotto) i cui nomi corrispondono *.words. Per un pezzo di dimensioni appropriate di questi nomi alla volta, la cui dimensione è determinata da xargs/ find, li unisce nel tmpfilefile ordinato . Se tmpfileesiste già (per tutti tranne il primo blocco), questo file viene anche unito agli altri file nel blocco corrente. A seconda della lunghezza dei nomi dei file e della lunghezza massima consentita di una riga di comando, ciò potrebbe richiedere più o più di 10 singole esecuzioni dello script interno ( find/ xargslo farà automaticamente).

La shsceneggiatura "interna" ,

if [ -f tmpfile ]; then
    sort -o tmpfile -m tmpfile "$@"
else
    sort -o tmpfile -m "$@"
fi

usa sort -o tmpfileper l'output in tmpfile(questo non sovrascriverà tmpfileanche se anche questo è un input per sort) e -mper fare l'unione. In entrambi i rami, "$@"verrà espanso in un elenco di nomi di file citati singolarmente passati allo script da findo xargs.

Poi, basta eseguire uniq -dsu tmpfiledi ottenere tutte le linee che vengono duplicati:

uniq -d tmpfile >dupes.txt

Se ti piace il principio "DRY" ("Non ripetere te stesso"), puoi scrivere lo script interno come

if [ -f tmpfile ]; then
    t=tmpfile
else
    t=/dev/null
fi

sort -o tmpfile -m "$t" "$@"

o

t=tmpfile
[ ! -f "$t" ] && t=/dev/null
sort -o tmpfile -m "$t" "$@"

Da dove provengono?

Per gli stessi motivi di cui sopra, non possiamo usare grep -Fx -f dupes.txt *.wordsper trovare da dove provengono queste duplicazioni, quindi invece findriutilizziamo:

find . -type f -name '*.words' \
    -exec grep -Fx -f dupes.txt {} +

Poiché non è necessario eseguire elaborazioni "complicate", è possibile invocare grepdirettamente -exec. L' -execopzione accetta un comando di utilità e inserirà i nomi trovati {}. Con +alla fine, findinserirà tanti argomenti al posto di {}quelli che la shell corrente supporta in ogni invocazione dell'utilità.

Per essere del tutto corretti, si potrebbe voler usare entrambi

find . -type f -name '*.words' \
    -exec grep -H -Fx -f dupes.txt {} +

o

find . -type f -name '*.words' \
    -exec grep -Fx -f dupes.txt /dev/null {} +

per essere sicuri che i nomi dei file siano sempre inclusi nell'output di grep.

La prima variante utilizza grep -Hper generare sempre nomi di file corrispondenti. L'ultima variante utilizza il fatto che grepincluderà il nome del file corrispondente se nella riga di comando viene fornito più di un file .

Ciò è importante dal momento che l'ultimo blocco di nomi di file inviato grepda findpuò effettivamente contenere solo un singolo nome file, nel qual caso grepnon lo menzionerebbe nei suoi risultati.


Materiale bonus:

Dissezione del comando find+ xargs+ sh:

find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh 

find . -type f -name '*.words'genererà semplicemente un elenco di nomi di percorso dalla directory corrente (o inferiore) in cui ciascun percorso è quello di un normale file ( -type f) e che ha un componente nome file alla fine corrispondente *.words. Se si deve cercare solo la directory corrente , si può aggiungere -maxdepth 1dopo il ., prima -type f.

-print0farà in modo che tutti i percorsi trovati vengano emessi con un carattere \0( nul) come delimitatore. Questo è un personaggio che non è valido in un percorso Unix e ci consente di elaborare i nomi dei percorsi anche se contengono caratteri di nuova riga (o altre cose strane).

findconvoglia il suo output a xargs.

xargs -0leggerà l' \0elenco -delimitato di nomi di percorso ed eseguirà ripetutamente l'utilità data con blocchi di questi, assicurandosi che l'utilità sia eseguita con argomenti sufficienti per non far lamentare alla shell un elenco di argomenti troppo lungo, fino a quando non ci sono più input da find.

L'utilità invocata da xargsè shcon uno script fornito nella riga di comando come stringa usando il suo -cflag.

Quando si invocano gli sh -c '...some script...'argomenti seguenti, gli argomenti saranno disponibili per lo script $@, ad eccezione del primo argomento , che verrà inserito $0(questo è il "nome comando" che è possibile individuare, ad esempio topse si è abbastanza veloci). Questo è il motivo per cui inseriamo la stringa shcome primo argomento dopo la fine dello script reale. La stringa shè un argomento fittizio e potrebbe essere qualsiasi parola singola (alcuni sembrano preferire _o sh-find).


Alla fine del tuo primo blocco di script di shell, a che cosa serve fi' sh?
dan

@danielAzuelos fiÈ la fine dell'istruzione ifnello shscript di shell "interno" . L' 'estremità che shell script (l'intero script è una stringa citata singolarmente). Il shsarà passato allo script interno in $0(non parte di $@, che conterrà i nomi dei file). In questo caso, quella shstringa può effettivamente essere qualsiasi parola. Se si lascia fuori shalla fine, il primo nome file verrebbe passato $0e non farebbe parte dell'elaborazione che sta facendo lo script di shell interno.
Kusalananda

8

Le righe all'interno di un singolo file vengono ordinate e duplicate gratuitamente.

Il che significa che probabilmente potresti trovare qualche utilità per sort -m:

 -m, --merge
        merge already sorted files; do not sort

L'altra ovvia alternativa a ciò sarebbe una semplice awkraccolta delle linee in un array e contarle. Ma come ha commentato @ dave_thompson_085 , quei 3 000 milioni di righe (o comunque molte di quelle uniche presenti) richiederebbero probabilmente una notevole quantità di memoria da archiviare, quindi potrebbe non funzionare molto bene.


3

Con awk puoi ottenere tutte le righe ripetute in tutti i file con un breve comando:

$ awk '_[$0]++' *.words

Ma ripeterà le righe se una riga esiste 3 o più volte.
C'è una soluzione per ottenere solo il primo duplicato:

$ awk '_[$0]++==1' *.words

Dovrebbe essere abbastanza veloce (se le ripetizioni sono poche) ma consumerà molta memoria per mantenere tutte le linee in memoria. Forse, a seconda dei file e delle ripetizioni effettivi, provare prima con 3 o 4 file.

$ awk '_[$0]++==1' [123]*.words

Altrimenti, puoi fare:

$ sort -m *.words | uniq -d

Che stamperà righe ripetute uniq.


2
+1 persort -m * | uniq -d
Jeff Schaller

awk può evitare le ripetizioni con, 'x[$0]++==1'ma avrà davvero bisogno di molta memoria; se le linee 3G hanno valori distinti 1G e se il tuo awk ha bisogno di dire 50 byte per una voce hasharray che associa una stringa (presumibilmente abbreviata) al valore uninit, questo è 50 GB. Per un input ordinato, puoi farlo uniq -dmanualmente awk '$0==p&&n++==1;$0!=p{p=$0;n=1}'ma perché preoccuparsi?
dave_thompson_085,

@ dave_thompson_085 Grazie per il concetto di ==1, ottima idea.
Isacco,

Supponendo che 30000 file con 100000 righe di 80 caratteri ciascuno e nessun duplicato , ciò richiederà la awkmemorizzazione di 2,4E11 byte (223 GiB).
Kusalananda

sort -m *.words | uniq -dfunziona alla grande! Dopo il processo corro grepper trovare un file che contiene una voce duplicata. Vedi un modo per stampare almeno un nome file che contiene una voce duplicata?
Lars Schneider,

3

Soluzione ottimizzata sort+ uniq:

sort --parallel=30000 *.words | uniq -d
  • --parallel=N - modifica il numero di ordinamenti eseguiti contemporaneamente N
  • -d, --repeated - stampa solo righe duplicate, una per ciascun gruppo
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.