¿Forma rápida de encontrar líneas en un archivo que no están en otro?

241

Tengo dos archivos grandes (conjuntos de nombres de archivo). Aproximadamente 30,000 líneas en cada archivo. Estoy tratando de encontrar una forma rápida de encontrar líneas en el archivo1 que no estén presentes en el archivo2.

Por ejemplo, si este es el archivo1:

line1
line2
line3

Y este es el archivo2:

line1
line4
line5

Entonces mi resultado / salida debería ser:

line2
line3

Esto funciona:

grep -v -f file2 file1

Pero es muy, muy lento cuando se usa en mis archivos grandes.

Sospecho que hay una buena manera de hacer esto usando diff (), pero la salida debería ser solo las líneas, nada más, y parece que no puedo encontrar un interruptor para eso.

¿Alguien puede ayudarme a encontrar una manera rápida de hacer esto, usando bash y binarios básicos de Linux?

EDITAR: Para seguir mi propia pregunta, esta es la mejor manera que he encontrado hasta ahora usando diff ():

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

Seguramente, debe haber una mejor manera?

Niels2000
fuente
1
podría intentar esto si es más rápido:awk 'NR==FNR{a[$0];next}!($0 in a)' file2 file1 > out.txt
Kent
sin requisitos rápidos: stackoverflow.com/questions/4366533/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
44
Gracias por contarnos sobre grep -v -f file2 file1
Rahul Prasad
Forma simple con un conjunto de herramientas reducido: cat file1 file2 file2 | sort | uniq --uniquevea mi respuesta a continuación.
Ondra Žižka

Respuestas:

233

Puede lograr esto controlando el formato de las líneas antiguas / nuevas / sin cambios en la diffsalida GNU :

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

Los archivos de entrada deben ordenarse para que esto funcione. Con bash(y zsh) puede ordenar en el lugar con la sustitución del proceso <( ):

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

En lo anterior, las líneas nuevas y sin cambios se suprimen, por lo que solo se emiten las cambiadas (es decir, las líneas eliminadas en su caso). También puede utilizar un par de diffopciones que otras soluciones no ofrecen, tales como -ihacer caso omiso de caso, o varias opciones en blanco ( -E, -b, -vetc) por menos estricta coincidencia.


Explicación

Las opciones --new-line-format, --old-line-formaty --unchanged-line-formatpermiten controlar la forma en que difflos formatos de las diferencias, de forma similar a printflos especificadores de formato. Estas opciones dan formato a líneas nuevas (agregadas), antiguas (eliminadas) y sin cambios respectivamente. Establecer uno para vaciar "" previene la salida de ese tipo de línea.

Si está familiarizado con el formato diff unificado , puede recrearlo en parte con:

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

El %Lespecificador es la línea en cuestión y prefijamos cada uno con "+" "-" o "", como diff -u (tenga en cuenta que solo genera diferencias, carece de las líneas --- +++y @@en la parte superior de cada cambio agrupado). También puede usar esto para hacer otras cosas útiles como numerar cada línea con %dn.


El diffmétodo (junto con otras sugerencias commy join) solo produce la salida esperada con la entrada ordenada , aunque puede usar <(sort ...)para ordenar en el lugar. Aquí hay un awkscript simple (nawk) (inspirado en los scripts vinculados a la respuesta de Konsolebox) que acepta archivos de entrada ordenados arbitrariamente y genera las líneas que faltan en el orden en que aparecen en el archivo1.

# 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]
}

Esto almacena todo el contenido del archivo1 línea por línea en una matriz indexada de número de línea ll1[], y todo el contenido del archivo2 línea por línea en una matriz asociativa indexada de contenido de línea ss2[]. Después de leer ambos archivos, repita ll1y use el inoperador para determinar si la línea en el archivo1 está presente en el archivo2. (Esto tendrá un resultado diferente para el diffmétodo si hay duplicados).

En el caso de que los archivos sean lo suficientemente grandes como para almacenarlos y causar un problema de memoria, puede cambiar la CPU por memoria almacenando solo el archivo1 y eliminando coincidencias a medida que se lee el archivo2.

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]
}

Lo anterior almacena todo el contenido del archivo1 en dos matrices, una indexada por número de línea ll1[]y otra indexada por contenido de línea ss1[]. Luego, a medida que se lee el archivo2, cada línea coincidente se elimina de ll1[]y ss1[]. Al final, salen las líneas restantes del archivo1, conservando el orden original.

En este caso, con el problema como se indicó, también puede dividir y conquistar usando GNU split(el filtrado es una extensión de GNU), ejecuciones repetidas con fragmentos de archivo1 y lectura de archivo2 completamente cada vez:

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

Tenga en cuenta el uso y la ubicación del -significado stdinen la gawklínea de comando. Esto lo proporciona splitfrom file1 en fragmentos de 20000 líneas por invocación.

Para los usuarios en los sistemas no GNU, es casi seguro que un paquete GNU coreutils puede obtener, incluso en OSX como parte de los de Apple Xcode herramientas que proporciona GNU diff, awk, aunque sólo un POSIX / BSD spliten lugar de una versión de GNU.

Sr. púrpura
fuente
1
Esto hace exactamente lo que necesito, en una pequeña fracción del tiempo que toma el enorme grep. ¡Gracias!
Niels2000
1
Encontré esta página de manual de GNU
Juto
algunos de nosotros no estamos en GNU [OS X bsd aquí ...] :)
rogerdpack
1
Supongo que te refieres a diff: en general, los archivos de entrada serán diferentes, diffen ese caso se devuelve 1 . Considérelo un bono ;-) Si está probando en un script de shell 0 y 1 son códigos de salida esperados, 2 indica un problema.
Sr.Spuratic
1
@ mr.spuratic ah sí, ahora lo encuentro en el man diff. ¡Gracias!
Archeosudoerus
246

El comando comm (abreviatura de "común") puede ser útilcomm - 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 

El manarchivo es realmente bastante legible para esto.

JnBrymn
fuente
66
Funciona perfectamente en OSX.
pisaruk
41
El requisito de entrada ordenada quizás debería resaltarse.
tripleee
21
commtambién tiene una opción para verificar que la entrada esté ordenada --check-order(lo que parece hacer de todos modos, pero esta opción provocará un error en lugar de continuar). Pero para ordenar los archivos, sólo tiene que hacer: com -23 <(sort file1) <(sort file2)y así sucesivamente
michael
Estaba comparando un archivo que se generó en Windows con un archivo que se generó en Linux y parecía que commno funcionaba en absoluto. Me tomó un tiempo darme cuenta de que se trata de las terminaciones de línea: incluso las líneas que parecen idénticas se consideran diferentes si tienen diferentes terminaciones de línea. El comando dos2unixse puede usar para convertir las terminaciones de línea CRLF a LF solamente.
ZeroOne
23

Como sugirió konsolebox, la solución grep de carteles

grep -v -f file2 file1

en realidad funciona muy bien (rápido) si simplemente agrega la -Fopción, para tratar los patrones como cadenas fijas en lugar de expresiones regulares. Verifiqué esto en un par de ~ 1000 listas de archivos de líneas que tuve que comparar. Con -Festo tomó 0.031 s (real), mientras que sin tomó 2.278 s (real), al redirigir la salida grep a wc -l.

Estas pruebas también incluyeron el -xinterruptor, que son parte necesaria de la solución para garantizar una precisión total en los casos en que el archivo 2 contiene líneas que coinciden con una, o no todas, una o más líneas en el archivo 1.

Entonces, una solución que no requiere que se ordenen las entradas, es rápida y flexible (mayúsculas y minúsculas, etc.) es:

grep -F -x -v -f file2 file1

Esto no funciona con todas las versiones de grep, por ejemplo, falla en macOS, donde una línea en el archivo 1 se mostrará como no presente en el archivo 2, aunque lo sea, si coincide con otra línea que es una subcadena del mismo . Alternativamente, puede instalar GNU grep en macOS para usar esta solución.

pbz
fuente
Sí, funciona, pero incluso con -Festo no escala bien.
Molomby
esto no es tan rápido, esperé 5 minutos por 2 archivos de ~ 500k líneas antes de
rendirme
En realidad, esta forma es aún más lento que forma de comunicación, ya que éste puede manejar archivos sin ordenar, por tanto, arrastrados por unsorting, comm tiene la ventaja de clasificar
workplaylifecycle
@workplaylifecycle Debe agregar el tiempo de clasificación, que puede ser el cuello de botella para los extremadamente grandes file2.
TAAR
Sin embargo, grep con la -xopción aparentemente usa más memoria. Con file2contienen 180M palabras de 6-10 bytes de mi proceso puso Killeden una máquina de memoria RAM 32 GB ...
TAAR
11

¿Cuál es la velocidad de como sort y diff?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted
Puggan Se
fuente
1
Gracias por recordarme la necesidad de ordenar los archivos antes de hacer diff. sort + diff es MUCHO más rápido.
Niels2000
44
one liner ;-) diff <(ordenar archivo1 -u) <(ordenar archivo2 -u)
steveinatorx
11

Si estás corto de "herramientas de fantasía", por ejemplo en alguna distribución de Linux mínimo, hay una solución con sólo cat, sorty uniq:

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

Prueba:

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    

Esto también es relativamente rápido, en comparación con grep.

Ondra Žižka
fuente
1
Nota: algunas implementaciones no reconocerán la --uniqueopción. Debería poder utilizar la opción POSIX estandarizada para esto:| uniq -u
AndrewF
1
En el ejemplo, ¿de dónde vino el "2"?
Niels2000
1
@ Niels2000, seq 1 1 7crea números desde 1, con incremento 1, hasta 7, es decir, 1 2 3 4 5 6 7. ¡Y ahí está tu 2!
Eirik Lygre
5
$ join -v 1 -t '' file1 file2
line2
line3

El -tse asegura de que compara toda la línea, si tenía un espacio en algunas de las líneas.

Steven Penny
fuente
Como comm, joinrequiere que ambas líneas de entrada se ordenen en el campo en el que está realizando la operación de combinación.
tripleee
4

Puedes usar 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())
'
Hola Adios
fuente
4

Utilizar combinede moreutilspaquete, una utilidad que soporta conjuntos not, and, or, xoroperaciones

combine file1 not file2

es decir, dame líneas que estén en el archivo 1 pero no en el archivo 2

O dame líneas en el archivo1 menos líneas en el archivo2

Nota: combine ordena y encuentra líneas únicas en ambos archivos antes de realizar cualquier operación, pero diffno lo hace. Por lo tanto, puede encontrar diferencias entre la salida de diffy combine.

Entonces, en efecto, estás diciendo

Encuentra líneas distintas en archivo1 y archivo2 y luego dame líneas en archivo1 menos líneas en archivo2

En mi experiencia, es mucho más rápido que otras opciones

GitanoCosmonauta
fuente
1

La forma en que generalmente hago esto es usando la --suppress-common-linesbandera, aunque tenga en cuenta que esto solo funciona si lo hace en formato de lado a lado.

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

BAustin
fuente
0

Encontré que para mí usar una instrucción de bucle if y for normal funcionó perfectamente.

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
Tman
fuente
2
Ver DontReadLinesWithFor . Además, este código se comportará muy mal si alguno de sus grepresultados se expande a varias palabras, o si file2el shell puede tratar cualquiera de sus entradas como un globo.
Charles Duffy