Seleccione líneas del archivo de texto que tengan identificadores en otro archivo

13

Utilizo una gran cantidad de grep awk en mi shell de Unix para trabajar con archivos de texto de columna separados por tabulaciones de tamaño mediano (alrededor de 10M-100M líneas). A este respecto, Unix Shell es mi hoja de cálculo.

Pero tengo un gran problema: seleccionar registros con una lista de ID.

Al tener un table.csvarchivo con formato id\tfoo\tbar...y un ids.csvarchivo con la lista de identificadores, solo seleccione los registros table.csvcon el ID presente ids.csv.

tipo de /programming/13732295/extract-all-lines-from-text-file-based-on-a-given-list-of-ids pero con shell, no perl.

grep -Fobviamente produce falsos positivos si los identificadores son de ancho variable. joines una utilidad que nunca podría entender. En primer lugar, requiere una clasificación alfabética (mis archivos generalmente están ordenados numéricamente), pero aun así no puedo hacer que funcione sin quejarme por un orden incorrecto y omitir algunos registros. Entonces no me gusta. grep -f contra el archivo con ^id\t-s es muy lento cuando el número de identificadores es grande. awkes engorroso

¿Hay alguna buena solución para esto? ¿Alguna herramienta específica para archivos separados por tabulaciones? La funcionalidad adicional también será bienvenida.

UPD: corregido sort->join

alamar
fuente
Si grep -fes demasiado lento, mantener esta estrategia parece más problemas de lo que vale: las variaciones probablemente serán víctimas de los mismos problemas de rendimiento O (N * M). Tal vez su tiempo se gastaría mejor aprendiendo cómo usar una base de datos SQL normalizada ...
goldilocks
1
¿Por qué no usar el script Perl de la pregunta que vinculaste? Alternativamente, debería ser posible escribir un script similar en awk.
cjm
Bash 4 tiene matrices asociativas, que es lo que necesita para sortear los bucles anidados al ejemplo de perl.
Ricitos
1
sortPuede hacer todo tipo de clasificación, numérica, alfabética y otras. Ver man sort.
terdon
Tengo una consulta aquí, ¿cómo podemos hacer lo mismo si el archivo de origen desde donde queremos extraer los datos es un archivo no delimitado

Respuestas:

19

Supongo que querías decir que grep -fno, grep -Fpero en realidad necesitas una combinación de ambos y -w:

grep -Fwf ids.csv table.csv

La razón por la que estaba obteniendo falsos positivos es (supongo que no lo explicó) porque si una identificación puede estar contenida en otra, entonces ambas se imprimirán. -welimina este problema y -Fse asegura de que sus patrones se traten como cadenas, no como expresiones regulares. De man grep:

   -F, --fixed-strings
          Interpret PATTERN as a  list  of  fixed  strings,  separated  by
          newlines,  any  of  which is to be matched.  (-F is specified by
          POSIX.)
   -w, --word-regexp
          Select  only  those  lines  containing  matches  that form whole
          words.  The test is that the matching substring must  either  be
          at  the  beginning  of  the  line,  or  preceded  by  a non-word
          constituent character.  Similarly, it must be either at the  end
          of  the  line  or  followed by a non-word constituent character.
          Word-constituent  characters  are  letters,  digits,   and   the
          underscore.

   -f FILE, --file=FILE
          Obtain  patterns  from  FILE,  one  per  line.   The  empty file
          contains zero patterns, and therefore matches nothing.   (-f  is
          specified by POSIX.)

Si sus falsos positivos se deben a que una ID puede estar presente en un campo que no es ID, recorra su archivo en su lugar:

while read pat; do grep -w "^$pat" table.csv; done < ids.csv

o, más rápido:

xargs -I {} grep "^{}" table.csv < ids.csv

Sin perlembargo , personalmente haría esto en :

perl -lane 'BEGIN{open(A,"ids.csv"); while(<A>){chomp; $k{$_}++}} 
            print $_ if defined($k{$F[0]}); ' table.csv
terdon
fuente
1
+1 Pero: ¿Qué pasa si hay posibles falsos positivos que coinciden con la identificación exactamente en términos de palabras, pero no en la columna de identificación? Si no puede usar ^con -F, no puede apuntar específicamente a la primera columna.
Ricitos
@goldilocks si coinciden exactamente, no son falsos positivos. Entiendo lo que quieres decir, pero en ese caso, el OP debería mostrar sus archivos de entrada.
terdon
El ^id\tbit del OP implica que idpodría ocurrir en otra columna. Si no, esto no importa.
Ricitos
@goldilocks punto justo, respuesta editada.
terdon
La forma en que solíamos hacer esto era crear archivos temporales (usando awk o sed) que agregaban un carácter único (digamos, control-A) delimitando el campo que queríamos buscar, luego usar grep -F -f temppatternfile temptargetfile | tr -d '\ 001'
Mark Plotnick el
7

La joinutilidad es lo que quieres. Requiere que los archivos de entrada estén ordenados léxicamente.

Asumiendo que su shell es bash o ksh:

join -t $'\t' <(sort ids.csv) <(sort table.csv)

Sin necesidad de ordenar, la solución awk habitual es

awk -F '\t' 'NR==FNR {id[$1]; next} $1 in id' ids.csv table.csv
Glenn Jackman
fuente
Como intenté pero finalmente no pude transmitir, unirse es un error. No me funciona tan bien.
alamar
1
joinno es un error: tus palabras fueron las que no pudiste resolver. Abre tu mente y aprende. ¿Qué resultado obtuviste y cómo difiere eso de lo que esperas?
Glenn Jackman
+1, este es un trabajo para join.
don_crissti 01 de
La awksolución aquí es muy rápida y eficiente para mis propósitos (estoy extrayendo subconjuntos de unos cientos de archivos con ~ 100 millones de líneas)
Lucas
2

Las respuestas a esta pregunta SO me ayudaron a superar los inconvenientes con join. Esencialmente, cuando ordena el archivo en preparación para enviarlo a unirse, debe asegurarse de que está ordenando según la columna a la que se está uniendo. Entonces, si ese es el primero, debe decirle cuál es el carácter separador en el archivo y que desea que se ordene en el primer campo (y solo en el primer campo). De lo contrario, si el primer campo tiene anchos variables (por ejemplo), sus separadores y posiblemente otros campos pueden comenzar a afectar el orden de clasificación.

Por lo tanto, use la opción -t de clasificación para especificar su carácter de separación, y use la opción -k para especificar el campo (recordando que necesita un campo inicial y final, incluso si es el mismo) o se ordenará a partir de ese carácter hasta el final de la línea).

Entonces, para un archivo separado por tabulaciones como en esta pregunta, lo siguiente debería funcionar (gracias a la respuesta de Glenn para la estructura):

join -t$'\t' <(sort -d ids.csv) <(sort -d -t$'\t' -k1,1 table.csv) > output.csv

(Como referencia, el indicador -d significa clasificación del diccionario. También puede utilizar el indicador -b para ignorar los espacios en blanco iniciales, ver man sorty man join).

Como ejemplo más general, suponga que está uniendo dos archivos separados por comas: input1.csven la tercera columna y input2.csven la cuarta. Podrías usar

join -t, -1 3 -2 4 <(sort -d -t, -k3,3 input2.csv) <(sort -d -t, -k4,4 input2.csv) > output.csv

Aquí las opciones -1y -2especifican en qué campos unirse en los archivos de entrada primero y segundo respectivamente.

LangeHaare
fuente
0

También puedes usar ruby ​​para hacer algo similar:

ruby -pe 'File.open("id.csv").each { |i| puts i if i =~ /\$\_/ }' table.csv
Arrendajo
fuente