Modo rapido per trovare linee in un file che non sono in un altro?


241

Ho due file di grandi dimensioni (set di nomi di file). Circa 30.000 righe in ciascun file. Sto cercando di trovare un modo rapido per trovare le righe nel file1 che non sono presenti nel file2.

Ad esempio, se questo è file1:

line1
line2
line3

E questo è file2:

line1
line4
line5

Quindi il mio risultato / output dovrebbe essere:

line2
line3

Questo funziona:

grep -v -f file2 file1

Ma è molto, molto lento se utilizzato su file di grandi dimensioni.

Ho il sospetto che ci sia un buon modo per farlo usando diff (), ma l'output dovrebbe essere solo le linee, nient'altro, e non riesco a trovare un interruttore per quello.

Qualcuno può aiutarmi a trovare un modo rapido per farlo, usando i binari bash e di base di Linux?

EDIT: Per dare seguito alla mia domanda, questo è il modo migliore che ho trovato finora usando diff ():

diff file2 file1 | grep '^>' | sed 's/^>\ //'

Sicuramente, ci deve essere un modo migliore?


1
potresti provare questo se è più veloce:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
Kent


4
Grazie per aver parlato di grep -v -f file2 file1
Rahul Prasad il


Modo semplice con set di strumenti ridotto:, cat file1 file2 file2 | sort | uniq --uniquevedere la mia risposta di seguito.
Ondra Žižka,

Risposte:


233

Puoi farlo controllando la formattazione delle linee vecchie / nuove / invariate diffnell'output GNU :

diff --new-line-format="" --unchanged-line-format=""  file1 file2

I file di input devono essere ordinati affinché funzioni. Con bash(e zsh) è possibile ordinare sul posto con la sostituzione del processo <( ):

diff --new-line-format="" --unchanged-line-format="" <(sort file1) <(sort file2)

Nelle righe nuove e invariate sopra vengono eliminate , quindi vengono emesse solo le linee modificate (ovvero le linee rimosse nel tuo caso). Si può anche utilizzare un paio di diffopzioni che altre soluzioni non offrono, come ad esempio -idi ignorare caso, o le varie opzioni di spaziatura ( -E, -b, -vecc) per meno severe di corrispondenza.


Spiegazione

Le opzioni --new-line-format, --old-line-formate --unchanged-line-formatconsentono di controllare il modo in cui diffformatta le differenze, simili a printfidentificatori di formato. Queste opzioni formattano rispettivamente le linee nuove (aggiunte), vecchie (rimosse) e invariate . L'impostazione di uno su "" impedisce l'output di quel tipo di linea.

Se hai familiarità con il formato diff unificato , puoi ricrearlo parzialmente con:

diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2

Lo %Lspecificatore è la riga in questione e ogni prefisso è "+" "-" o "", come diff -u (si noti che genera solo differenze, manca le righe --- +++e @@nella parte superiore di ogni modifica raggruppata). È inoltre possibile utilizzare questo per fare altre cose utili come numero di ogni riga con %dn.


Il diffmetodo (insieme ad altri suggerimenti comme join) produce solo l'output previsto con input ordinato , sebbene sia possibile utilizzare <(sort ...)per ordinare in posizione. Ecco un semplice awkscript (nawk) (ispirato agli script collegati nella risposta di Konsolebox) che accetta file di input ordinati in modo arbitrario e genera le righe mancanti nell'ordine in cui si verificano nel file1.

# output lines in file1 that are not in file2
BEGIN { FS="" }                         # preserve whitespace
(NR==FNR) { ll1[FNR]=$0; nl1=FNR; }     # file1, index by lineno
(NR!=FNR) { ss2[$0]++; }                # file2, index by string
END {
    for (ll=1; ll<=nl1; ll++) if (!(ll1[ll] in ss2)) print ll1[ll]
}

Ciò memorizza l'intero contenuto di file1 riga per riga in un array indicizzato numero riga ll1[]e l'intero contenuto di file2 riga per riga in un array associativo indicizzato contenuto riga ss2[]. Dopo aver letto entrambi i file, scorrere ll1e utilizzare l' inoperatore per determinare se la riga in file1 è presente in file2. (Questo avrà un output diverso rispetto al diffmetodo se ci sono duplicati.)

Nel caso in cui i file siano sufficientemente grandi da archiviarli entrambi, si verifica un problema di memoria, è possibile scambiare la CPU con la memoria archiviando solo file1 ed eliminando le corrispondenze lungo la lettura di file2.

BEGIN { FS="" }
(NR==FNR) {  # file1, index by lineno and string
  ll1[FNR]=$0; ss1[$0]=FNR; nl1=FNR;
}
(NR!=FNR) {  # file2
  if ($0 in ss1) { delete ll1[ss1[$0]]; delete ss1[$0]; }
}
END {
  for (ll=1; ll<=nl1; ll++) if (ll in ll1) print ll1[ll]
}

Quanto sopra memorizza l'intero contenuto di file1 in due array, uno indicizzato per numero di riga ll1[], uno indicizzato per contenuto di riga ss1[]. Quindi, quando viene letto file2, ogni riga corrispondente viene eliminata da ll1[]e ss1[]. Alla fine vengono emesse le righe rimanenti da file1, preservando l'ordine originale.

In questo caso, con il problema come indicato, puoi anche dividere e conquistare usando GNU split(il filtro è un'estensione GNU), ripetute esecuzioni con blocchi di file1 e lettura di file2 ogni volta:

split -l 20000 --filter='gawk -f linesnotin.awk - file2' < file1

Nota l'uso e il posizionamento del -significato stdinsulla gawkriga di comando. Questo è fornito da splitfile1 in blocchi di 20000 righe per invocazione.

Per gli utenti di sistemi non-GNU, non v'è quasi certamente un coreutils GNU pacchetto è possibile ottenere, anche su OSX come parte delle di Apple Xcode strumenti che fornisce GNU diff, awk, anche se solo un POSIX / BSD split, piuttosto che una versione di GNU.


1
Questo fa esattamente quello di cui ho bisogno, in una piccola parte del tempo impiegato dall'enorme grep. Grazie!
Niels2000,

1
Ho trovato questa manpage gnu
Juto il

alcuni di noi non sono su GNU [OS X bsd qui ...] :)
rogerdpack,

1
Presumo che tu intenda per diff: in generale i file di input saranno diversi, diffin questo caso viene restituito 1 . Consideralo un bonus ;-) Se stai testando in uno script di shell 0 e 1 sono previsti codici di uscita, 2 indica un problema.
signor spuratic

1
@ mr.spuratic ah sì, ora lo trovo nel man diff. Grazie!
Archeosudoerus,

246

Il comando comm (abbreviazione di "comune") può essere utilecomm - compare two sorted files line by line

#find lines only in file1
comm -23 file1 file2 

#find lines only in file2
comm -13 file1 file2 

#find lines common to both files
comm -12 file1 file2 

Il manfile è in realtà abbastanza leggibile per questo.


6
Funziona perfettamente su OSX.
pisaruk,

41
Il requisito per l'input ordinato dovrebbe forse essere evidenziato.
Tripleee,

21
commha anche un'opzione per verificare che l'input sia ordinato --check-order(cosa che sembra fare comunque, ma questa opzione causerà un errore invece di continuare). Ma per ordinare i file, fai semplicemente: com -23 <(sort file1) <(sort file2)e così via
michael

Stavo confrontando un file generato in Windows con un file generato in Linux e sembrava che commnon funzionasse affatto. Mi ci è voluto un po 'di tempo per capire che si trattava dei finali di linea: anche le linee che sembrano identiche sono considerate diverse se hanno terminazioni di linea diverse. Il comando dos2unixpuò essere utilizzato per convertire le terminazioni di riga CRLF in LF.
ZeroOne il

23

Come suggerito da Konsolebox, la soluzione grep dei poster

grep -v -f file2 file1

funziona davvero alla grande (veloce) se aggiungi semplicemente l' -Fopzione, per trattare i pattern come stringhe fisse invece che espressioni regolari. Ho verificato questo su un paio di ~ 1000 elenchi di file di linea che ho dovuto confrontare. Con -Fesso ci sono voluti 0,031 s (reali), mentre senza ha impiegato 2.278 s (reali), quando si reindirizza l'output grep su wc -l.

Questi test includevano anche lo -xswitch, che sono parte integrante della soluzione al fine di garantire la totale accuratezza nei casi in cui file2 contiene righe che corrispondono a parte, ma non tutte, di una o più righe nel file1.

Quindi una soluzione che non richiede che gli input siano ordinati, è veloce, flessibile (case sensitive, ecc.) È:

grep -F -x -v -f file2 file1

Questo non funziona con tutte le versioni di grep, ad esempio non riesce in macOS, dove una riga nel file 1 verrà mostrata come non presente nel file 2, anche se lo è, se corrisponde a un'altra riga che ne è una sottostringa . In alternativa puoi installare GNU grep su macOS per utilizzare questa soluzione.


Sì, funziona ma anche con -Fquesto non si adatta bene.
Molomby,

non è così veloce, ho aspettato 5 minuti per 2 file di ~ 500k righe prima di arrendermi
cahen

in realtà, questo modo è ancora più lento del modo comm, perché questo può gestire file non ordinati quindi trascinati verso il basso da non ordinamento, comm sfrutta l'ordinamento
workplaylifecycle

@workplaylifecycle È necessario aggiungere il tempo per l'ordinamento, che può essere il collo di bottiglia per estremamente grande file2.
primo

Tuttavia, grep con l' -xopzione apparentemente usa più memoria. Con un file2contenuto di 180 milioni di parole di 6-10 byte il mio processo è arrivato Killedsu una macchina RAM da 32 GB ...
primo

11

qual è la velocità di come ordinamento e diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted

1
Grazie per avermi ricordato la necessità di ordinare i file prima di fare diff. sort + diff è MOLTO più veloce.
Niels2000,

4
one liner ;-) diff <(ordina file1 -u) <(ordina file2 -u)
steveinatorx

11

Se siete a corto di "strumenti di fantasia", ad esempio in qualche distribuzione Linux minimale, c'è una soluzione con un solo cat, sorte uniq:

cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

Test:

seq 1 1 7 | sort --random-sort > includes.txt
seq 3 1 9 | sort --random-sort > excludes.txt
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

# Output:
1
2    

Anche questo è relativamente veloce, rispetto a grep.


1
Nota: alcune implementazioni non riconosceranno l' --uniqueopzione. Dovresti essere in grado di utilizzare l' opzione POSIX standardizzata per questo:| uniq -u
AndrewF

1
Nell'esempio, da dove viene il "2"?
Niels2000,

1
@ Niels2000, seq 1 1 7crea numeri da 1, con incremento 1, fino a 7, ovvero 1 2 3 4 5 6 7. E proprio qui ci sono i tuoi 2!
Eirik Lygre,

5
$ join -v 1 -t '' file1 file2
line2
line3

Si -tassicura che confronta l'intera linea, se si dispone di uno spazio in alcune delle linee.


Come comm, joinrichiede che entrambe le righe di input siano ordinate sul campo su cui si sta eseguendo l'operazione di join.
Tripleee,

4

Puoi usare Python:

python -c '
lines_to_remove = set()
with open("file2", "r") as f:
    for line in f.readlines():
        lines_to_remove.add(line.strip())

with open("f1", "r") as f:
    for line in f.readlines():
        if line.strip() not in lines_to_remove:
            print(line.strip())
'

4

Utilizzare combineda moreutilspacchetto, un programma di utilità set che supporta not, and, or, xorle operazioni di

combine file1 not file2

cioè dammi linee che sono in file1 ma non in file2

OPPURE dammi righe in file1 meno righe in file2

Nota: combine ordina e trova linee univoche in entrambi i file prima di eseguire qualsiasi operazione, ma diffnon lo fa. Quindi potresti trovare differenze tra l'output di diffe combine.

Quindi in effetti stai dicendo

Trova linee distinte in file1 e file2 e poi dammi linee in file1 meno righe in file2

Nella mia esperienza, è molto più veloce di altre opzioni



1

Il modo in cui lo faccio di solito è usare la --suppress-common-linesbandiera, anche se nota che funziona solo se lo fai in formato side-by-side.

diff -y --suppress-common-lines file1.txt file2.txt


0

Ho scoperto che per me usare una normale istruzione if e for loop ha funzionato perfettamente.

for i in $(cat file2);do if [ $(grep -i $i file1) ];then echo "$i found" >>Matching_lines.txt;else echo "$i missing" >>missing_lines.txt ;fi;done

2
Vedi DontReadLinesWithFor . Inoltre, questo codice si comporterà molto male se uno qualsiasi dei greprisultati si espande in più parole o se una delle file2voci può essere trattata dalla shell come un glob.
Charles Duffy,
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.