Elimine todas las líneas en el archivo A que contienen las cadenas en el archivo B

15

Tengo un archivo CSV users.csvcon una lista de nombres de usuario, ID de usuario y otros datos:

username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"Paul McCartny", 30923833, "left", "black"
"Ringo Starr", 77392318, "right", "blue"
"George Harrison", 72349482, "left", "green"

En otro archivo toremove.txttengo una lista de ID de usuario:

30923833
77392318

¿Existe una manera inteligente y eficiente de eliminar todas las filas del users.csvarchivo que contienen los ID toremove.txt? He escrito una aplicación simple de Python para analizar los dos archivos y escribir en un nuevo archivo solo aquellas líneas que no se encuentran toremove.txt, pero es extraordinariamente lenta. Tal vez algo sedo awkmagia puede ayudar aquí?

Este es el resultado deseado, considerando los ejemplos anteriores:

username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"
dotancohen
fuente
Tal vez deberías compartir tu script de Python. Sospecho que hay algo mal allí, como ser O (N²), aunque si mantiene y elimina millones de registros, la magia no ayudará demasiado.
Ángel
De hecho, el script es O (n <sup> 2 </sup>): n para las users.csvlíneas del archivo yn para las líneas de toremove.txt. No estoy realmente seguro de cómo hacerlo con menor complejidad. El quid de la cuestión es: for u in users: if not any(toremove in u): outputfile.write(u). Puedo publicarlo en Code Review.
dotancohen
1
Lo leería toremove.txt, guardando las entradas como claves . Iterate users.csv, imprimiendo aquellos donde la identificación no está en el dict. Obtiene el procesamiento O (n) para ambos toremove.txty users.csv, y el uso de memoria O (n) para toremove.txt(que probablemente sea relativamente pequeño)
Ángel
@ Ángel: Sí, ¡así es exactamente como funciona el guión!
dotancohen
1
Comprobar si existe una clave en un diccionario, equivale a una comprobación de tabla hash, que es (casi) O (1). Por otro lado, si necesita iterar los elementos para eliminar, eso es O (m)
Ángel

Respuestas:

15

Con grep, puedes hacer:

$ grep -vwF -f toremove.txt users.txt 
username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"

Con awk:

$ awk -F'[ ,]' 'FNR==NR{a[$1];next} !($4 in a)' toremove.txt users.txt 
username, userid, sidebar_side, sidebar_colour
"John Lennon", 90123412, "left", "blue"
"George Harrison", 72349482, "left", "green"
Cuonglm
fuente
@terdon: Dang! Yo iba a decir eso. Sin embargo, tenga en cuenta que la respuesta de Gnouc (posiblemente) hace lo que pide la pregunta , pero podría no ser lo que el usuario quiere.
Scott
La awksolución es muy sensible a los archivos que se formatean exactamente como se muestra en la pregunta. Lo más evidente es que si un nombre es solo una palabra / ficha (es decir, no contiene espacios; por ejemplo "Bono") o si tiene más de dos fichas (es decir, contiene más de un espacio; por ejemplo "Sir Paul McCartney"), pasará incluso si el coincidencias de ID de usuario. Menos obvio, lo mismo sucede si no hay espacio entre la primera coma y el ID de usuario, o si hay más de un espacio (por ejemplo, "John Lennon", 90123412, …).
Scott
@Scott: Sí, es la razón por la que puse awksolución detrásgrep
cuonglm
4

Aquí está la awkrespuesta de Gnouc , modificada para ser ciega al espacio:

awk -F, 'FNR==NR{a[$1];next} !(gensub("^ *","",1,$2) in a)' toremove.txt users.csv

Dado que usa solo comas (y no espacios) como delimitadores, $1es "John Lennon", $2es  90123412(con un espacio inicial), etc. Por lo tanto, usamos gensubpara eliminar cualquier número de espacios iniciales $2 antes de verificar si (el ID de usuario) estaba en el toremove.txtarchivo.

Scott
fuente
Es posible que pueda hacer otras cosas inteligentes aquí (solo pensar en voz alta), como analizar la "pieza exacta" de la cadena que no debe coincidir y comparar eso con la matriz asociativa, o no.
rogerdpack
Creo que eso es lo que estoy haciendo. ¿Qué tenías en mente?
Scott
Sí es usted. Me refería a si necesitaba hacer algo más funky, como eliminar la primera mitad de una línea o algo por el estilo (descarte, etc. stackoverflow.com/a/4784647/32453 ) simplemente análisis especializado
rogerdpack
0

OK de una manera rubí: si tiene una lista de cadenas en un archivo y desea eliminar todas las líneas de otro archivo que incluso contenga cualquier cadena en el primer archivo (en este caso, eliminar "archivo2" de "archivo1") :

b=File.read("file2").split # subtract this one out
remove_regex = Regexp.new(b.join('|'))
File.open("file1", "r").each_line do |line|
  if line !~ remove_regex
    puts line
  end
end

desafortunadamente con un gran archivo "para eliminar", esto parece degradar la complejidad a O (N ^ 2) (supongo que la expresión regular tiene mucho trabajo por hacer), pero aún podría ser útil para alguien por ahí (si usted desea más que eliminar líneas completas). Puede ser más rápido en ciertos casos.

Otra opción si va por la velocidad es usar el mismo mecanismo de comprobación de hash, pero "analizar" cuidadosamente la línea en busca de cadenas que puedan coincidir, y luego compararlas con su hash.

En ruby, podría verse así:

b=File.read("file2").split # subtract this one out
hash={}
for line in b
  hash[line] = 1
end

ARGF.each_line do |line|
  ok = true
  for number in line.scan(/\d{9}/)
    if hash.key? number
      ok=false
    end
  end
  if (ok)
    puts line
  end
end

Ver también la respuesta de Scott, es similar a las respuestas awk propuestas aquí, y evita la complejidad O (N ^ 2) (phew).

rogerdpack
fuente