Eliminar líneas de un archivo que están en otro archivo

126

Tengo un archivo f1:

line1
line2
line3
line4
..
..

Quiero eliminar todas las líneas que están en otro archivo f2:

line2
line8
..
..

Intenté algo con caty sed, que ni siquiera estaba cerca de lo que pretendía. ¿Cómo puedo hacer esto?

lalli
fuente
44
posible duplicado de Eliminar líneas del archivo que aparece en otro archivo
Sven Hohenstein
Si está buscando eliminar líneas de un archivo que "incluso contienen" cadenas de otro archivo (por ejemplo, coincidencias parciales) vea unix.stackexchange.com/questions/145079/…
rogerdpack

Respuestas:

154

grep -v -x -f f2 f1 debería hacer el truco.

Explicación:

  • -v para seleccionar líneas no coincidentes
  • -x para que coincida solo con líneas enteras
  • -f f2 para obtener patrones de f2

En su lugar se puede utilizar grep -Fo fgreppara que coincida con cuerdas fijas del f2lugar de los patrones (en caso de querer quitar las líneas de una manera "lo que se ve si lo que se obtiene" en lugar de tratar las líneas en f2que los patrones de expresiones regulares).

gabuzo
fuente
22
Esto tiene una complejidad O (n²) y comenzará a tardar horas en completarse una vez que los archivos contengan más de unas pocas líneas K.
Arnaud Le Blanc
11
Averiguar qué algoritmos sugeridos por SO tienen complejidad O (n ^ 2) solo tiene complejidad O (n), pero aún puede tomar horas para competir.
HDave
2
Acabo de probar esto en 2 archivos de ~ 2k líneas cada uno, y fue eliminado por el sistema operativo (concedido, esta es una máquina virtual no tan potente, pero aún así).
Trebor Rude
1
Me encanta la elegancia de esto; Prefiero la velocidad de la respuesta de Jona Christopher Sahnwal.
Alex Hall el
1
@ arnaud576875: ¿Estás seguro? Depende de la implementación de grep. Si se procesa f2correctamente antes de comenzar a buscar, la búsqueda solo tomará tiempo O (n).
Hola
56

Prueba comm en su lugar (suponiendo que f1 y f2 están "ya ordenados")

comm -2 -3 f1 f2
Ignacio Vazquez-Abrams
fuente
55
No estoy seguro de commque es la solución tiene la pregunta no indica que las líneas en f1se ordenan que es un requisito previo para el usocomm
gabuzo
1
Esto funcionó para mí, ya que mis archivos estaban ordenados y tenían más de 250,000 líneas en uno de ellos, solo 28,000 en el otro. ¡Gracias!
Invierno
1
Cuando esto funciona (los archivos de entrada están ordenados), ¡esto es extremadamente rápido!
Mike Jarvis
Al igual que en la solución de arnaud576875, para mí usar cygwin, esto eliminó las líneas duplicadas en el segundo archivo que es posible que desee conservar.
Alex Hall
8
Puede utilizar la sustitución de procesos para ordenar los archivos primero, por supuesto:comm -2 -3 <(sort f1) <(sort f2)
davemyron
14

Para excluir archivos que no son demasiado grandes, puede usar las matrices asociativas de AWK.

awk 'NR == FNR { list[tolower($0)]=1; next } { if (! list[tolower($0)]) print }' exclude-these.txt from-this.txt 

La salida estará en el mismo orden que el archivo "from-this.txt". La tolower()función hace que no distinga entre mayúsculas y minúsculas, si lo necesita.

La complejidad algorítmica probablemente será O (n) (exclude-these.txt size) + O (n) (from-this.txt size)

Pausado hasta nuevo aviso.
fuente
¿Por qué dices archivos que no son demasiado grandes? El temor aquí es (supongo) que awk está ejecutando el sistema sin memoria del sistema para crear el hash, ¿o hay alguna otra limitación?
rogerdpack
para los seguidores, hay incluso otra opción más agresiva para "desinfectar" las líneas (ya que la comparación debe ser exacta para usar la matriz asociativa), ex unix.stackexchange.com/a/145132/8337
rogerdpack
@rogerdpack: un archivo de exclusión grande requerirá una gran matriz de hash (y un largo tiempo de procesamiento). Un gran "from-this.txt" solo requerirá un largo tiempo de procesamiento.
Pausado hasta nuevo aviso.
1
Esto falla (es decir, no produce ninguna salida) si exclude-these.txtestá vacío. La respuesta de @ jona-christopher-sahnwaldt a continuación funciona en este caso. También puede especificar varios archivos, por ejemploawk '{if (f==1) { r[$0] } else if (! ($0 in r)) { print $0 } } ' f=1 done.out failed.out f=2 all-files.out
Graham Russell
11

Similar a la respuesta de Dennis Williamson (en su mayoría cambios sintácticos, por ejemplo, establecer el número de archivo explícitamente en lugar del NR == FNRtruco):

awk '{if (f==1) { r[$0] } else if (! ($0 in r)) { print $0 } } ' f=1 exclude-these.txt f=2 from-this.txt

El acceso r[$0]crea la entrada para esa línea, no es necesario establecer un valor.

Suponiendo que awk usa una tabla hash con búsqueda constante y (en promedio) tiempo de actualización constante, la complejidad del tiempo será O (n + m), donde n y m son las longitudes de los archivos. En mi caso, n fue de ~ 25 millones ym ~ 14000. La solución awk fue mucho más rápida que la clasificación, y también preferí mantener el orden original.

jcsahnwaldt Restablecer Monica
fuente
¿Cómo difiere esto de la respuesta de Dennis Williamson? ¿Es la única diferencia que no hace una asignación en el hash, tan ligeramente más rápido que esto? ¿La complejidad algorítmica es igual a la suya?
rogerdpack
La diferencia es principalmente sintáctica. Encuentro la variable fmás clara que NR == FNR, pero eso es cuestión de gustos. La asignación al hash debe ser tan rápida que no haya una diferencia de velocidad medible entre las dos versiones. Creo que estaba equivocado acerca de la complejidad: si la búsqueda es constante, la actualización también debería ser constante (en promedio). No sé por qué pensé que la actualización sería logarítmica. Editaré mi respuesta.
jcsahnwaldt Restablece a Monica el
Intenté muchas de estas respuestas, y esta fue AMAZEBALLS rápidamente. Tenía archivos con cientos de miles de líneas. ¡Trabajado como un encanto!
Sr. T
1
Esta es mi solución preferida. Funciona con múltiples archivos y también con archivos de exclusión vacíos, por ejemplo awk '{if (f==1) { r[$0] } else if (! ($0 in r)) { print $0 } } ' f=1 empty.file done.out failed.out f=2 all-files.out. Mientras que la otra awksolución falla con el archivo de exclusión vacío y solo puede tomar uno.
Graham Russell
5

si tienes Ruby (1.9+)

#!/usr/bin/env ruby 
b=File.read("file2").split
open("file1").each do |x|
  x.chomp!
  puts x if !b.include?(x)
end

Que tiene complejidad O (N ^ 2). Si quieres preocuparte por el rendimiento, aquí hay otra versión

b=File.read("file2").split
a=File.read("file1").split
(a-b).each {|x| puts x}

que usa un hash para efectuar la resta, también lo es la complejidad O (n) (tamaño de a) + O (n) (tamaño de b)

Aquí hay un pequeño punto de referencia, cortesía del usuario576875, pero con 100K líneas, de lo anterior:

$ for i in $(seq 1 100000); do echo "$i"; done|sort --random-sort > file1
$ for i in $(seq 1 2 100000); do echo "$i"; done|sort --random-sort > file2
$ time ruby test.rb > ruby.test

real    0m0.639s
user    0m0.554s
sys     0m0.021s

$time sort file1 file2|uniq -u  > sort.test

real    0m2.311s
user    0m1.959s
sys     0m0.040s

$ diff <(sort -n ruby.test) <(sort -n sort.test)
$

diff se utilizó para mostrar que no hay diferencias entre los 2 archivos generados.

Kurumi
fuente
1
Esto tiene una complejidad O (n²) y comenzará a tardar horas en completarse una vez que los archivos contengan más de unas pocas líneas K.
Arnaud Le Blanc
Realmente no me importa en este momento, porque no mencionó ningún archivo grande.
kurumi
3
No hay necesidad de estar tan a la defensiva, no es como si @ user576875 hubiera rechazado su respuesta o algo así. :-)
John Parker
muy buena segunda versión, Ruby gana :)
Arnaud Le Blanc
4

Algunas comparaciones de tiempo entre varias otras respuestas:

$ for n in {1..10000}; do echo $RANDOM; done > f1
$ for n in {1..10000}; do echo $RANDOM; done > f2
$ time comm -23 <(sort f1) <(sort f2) > /dev/null

real    0m0.019s
user    0m0.023s
sys     0m0.012s
$ time ruby -e 'puts File.readlines("f1") - File.readlines("f2")' > /dev/null

real    0m0.026s
user    0m0.018s
sys     0m0.007s
$ time grep -xvf f2 f1 > /dev/null

real    0m43.197s
user    0m43.155s
sys     0m0.040s

sort f1 f2 | uniq -u ni siquiera es una diferencia simétrica, porque elimina las líneas que aparecen varias veces en cualquier archivo.

comm también se puede usar con stdin y aquí cadenas:

echo $'a\nb' | comm -23 <(sort) <(sort <<< $'c\nb') # a
Lri
fuente
2

Parece ser un trabajo adecuado para el shell SQLite:

create table file1(line text);
create index if1 on file1(line ASC);
create table file2(line text);
create index if2 on file2(line ASC);
-- comment: if you have | in your files then specify  .separator ××any_improbable_string×× 
.import 'file1.txt' file1
.import 'file2.txt' file2
.output result.txt
select * from file2 where line not in (select line from file1);
.q
Benoit
fuente
1

¿Intentaste esto con sed?

sed 's#^#sed -i '"'"'s%#g' f2 > f2.sh

sed -i 's#$#%%g'"'"' f1#g' f2.sh

sed -i '1i#!/bin/bash' f2.sh

sh f2.sh
Ruan
fuente
0

No es una respuesta de 'programación', pero aquí hay una solución rápida y sucia: solo vaya a http://www.listdiff.com/compare-2-lists-difference-tool .

Obviamente no funcionará para archivos grandes, pero me sirvió. Algunas notas

  • No estoy afiliado al sitio web de ninguna manera (si todavía no me cree, puede buscar una herramienta diferente en línea; utilicé el término de búsqueda "establecer lista de diferencias en línea")
  • El sitio web vinculado parece hacer llamadas de red en cada comparación de lista, así que no le des datos confidenciales
youngrrrr
fuente