bash agrega / agrega nuevas columnas de otros archivos

Nov 24 2020

Tengo un archivo name.txt de una columna, por ejemplo

A
B
C
D
E
F

Entonces tengo muchos archivos, egxtxt, y.txt y z.txt

x.txt tiene

A 1
C 3
D 2

y.txt tiene

A 1
B 4
E 3

z.txt tiene

B 2
D 2
F 1

La salida deseable es (completando 0 si no hay mapeo)

A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

¿Es posible hacerlo con bash? (¿quizás awk?)
¡¡¡Muchas gracias !!!


primeras ediciones: mis esfuerzos tentativos
Como soy bastante nuevo en bash, es muy difícil para mí encontrar una posible solución con awk. Estoy más familiarizado con R, en el que esto se puede lograr mediante

namematrix[namematrix[,1]==xmatrix[,1],]

Considerándolo todo, realmente agradezco la amable ayuda a continuación que me ayudó a aprender más sobre awky join!


Ediciones por segunda vez: ¡un enfoque súper eficiente descubierto!

Afortunadamente, inspirado por algunas respuestas realmente brillantes a continuación, he resuelto una forma muy eficiente desde el punto de vista computacional como a continuación. Esto puede resultar útil para otras personas que se encuentren con preguntas similares, en particular si se trata de una gran cantidad de archivos de gran tamaño.

Primero toque un join_awk.bash

#!/bin/bash
join -oauto -e0 -a1 $1 $2 | awk '{print $2}'

Por ejemplo, ejecute este script bash para name.txt y x.txt

join_awk.bash name.txt x.txt

generaría

1
0
3
2
0
0

Tenga en cuenta que aquí solo guardo la segunda columna para ahorrar espacio en el disco, porque en mi conjunto de datos, las primeras columnas son nombres muy largos que ocuparían mucho espacio en el disco.

Entonces simplemente implemente

parallel join_awk.bash name.txt {} \> outdir/output.{} ::: {a,b,c}.txt

Esto está inspirado en la brillante respuesta a continuación usando GNU paralelo y join. La diferencia es que la respuesta a continuación tiene que especificar j1para paralleldebido a su lógica de Anexión de serie, lo que hace que no sea realmente "paralelo". Además, la velocidad será cada vez más lenta a medida que continúe la agregación de serie. Por el contrario, aquí manipulamos cada archivo por separado en paralelo. Puede ser extremadamente rápido cuando trabajamos con una gran cantidad de archivos de gran tamaño con varias CPU.

Finalmente, simplemente combine todos los archivos de salida de una sola columna juntos por

cd outdir
paste output* > merged.txt

Esto también será muy rápido ya que pastees inherentemente paralelo.

Respuestas

12 anubhava Nov 24 2020 at 13:42

Puedes usar esto awk:

awk 'NF == 2 {
   map[FILENAME,$1] = $2
   next
}
{
   printf "%s", $1 for (f=1; f<ARGC-1; ++f) printf "%s", OFS map[ARGV[f],$1]+0
   print ""
}' {x,y,z}.txt name.txt
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1
9 RavinderSingh13 Nov 24 2020 at 14:15

Añadiendo una forma más de hacerlo. ¿Podría intentar seguir, escribir y probar con las muestras que se muestran? En mi humilde opinión, debería funcionar en cualquiera awk, aunque solo tengo la versión 3.1 de GNU awk. Esta es una forma muy simple y habitual, cree una matriz en la primera lectura (principal) del Input_file y luego, más adelante, en cada archivo, agregue el 0elemento de esa matriz que NO se encuentre en ese Input_file específico, probado solo con muestras pequeñas.

awk '
function checkArray(array){
  for(i in array){
    if(!(i in found)){ array[i]=array[i] OFS "0" }
  }
}
FNR==NR{
  arr[$0] next } foundCheck && FNR==1{ checkArray(arr) delete found foundCheck="" } { if($1 in arr){
    arr[$1]=(arr[$1] OFS $2) found[$1]
    foundCheck=1
    next
  }
}
END{
  checkArray(arr)
  for(key in arr){
    print key,arr[key]
  }
}
' name.txt x.txt y.txt  z.txt

Explicación: agregando una explicación detallada de lo anterior.

awk '                               ##Starting awk program from here.
function checkArray(array){         ##Creating a function named checkArray from here.
  for(i in array){                  ##CTraversing through array here.
    if(!(i in found)){ array[i]=array[i] OFS "0" }   ##Checking condition if key is NOT in found then append a 0 in that specific value.
  }
}
FNR==NR{                            ##Checking condition if FNR==NR which will be TRUE when names.txt is being read.
  arr[$0] ##Creating array with name arr with index of current line. next ##next will skip all further statements from here. } foundCheck && FNR==1{ ##Checking condition if foundCheck is SET and this is first line of Input_file. checkArray(arr) ##Calling function checkArray by passing arr array name in it. delete found ##Deleting found array to get rid of previous values. foundCheck="" ##Nullifying foundCheck here. } { if($1 in arr){                    ##Checking condition if 1st field is present in arr.
    arr[$1]=(arr[$1] OFS $2) ##Appening 2nd field value to arr with index of $1.
    found[$1]                       ##Adding 1st field to found as an index here.
    foundCheck=1                    ##Setting foundCheck here.
    next                            ##next will skip all further statements from here.
  }
}
END{                                ##Starting END block of this program from here.
  checkArray(arr)                   ##Calling function checkArray by passing arr array name in it.
  for(key in arr){                  ##Traversing thorugh arr here.
    print key,arr[key]              ##Printing index and its value here.
  }
}
' name.txt x.txt y.txt z.txt        ##Mentioning Input_file names here.
6 DavidC.Rankin Nov 24 2020 at 13:35

Sí, puedes hacerlo, y sí, awkes la herramienta. Usando matrices y su número de línea de archivo normal ( FNR número de archivo de registros ) y líneas totales ( NR registros ) puede leer todas las letras de names.txtla a[]matriz, luego realizar un seguimiento del número de archivo en la variable fno, puede agregar todas las adiciones de x.txty luego antes de procesar la primera línea del siguiente archivo ( y.txt), recorra todas las letras que se ven en el en el último archivo, y para las que no se ven, coloque a 0, luego continúe procesando normalmente. Repita para cada archivo adicional.

En los comentarios se muestran más explicaciones línea por línea:

awk '
    FNR==NR {                           # first file
        a[$1] = "" # fill array with letters as index fno = 1 # set file number counter next # get next record (line) } FNR == 1 { fno++ } # first line in file, increment file count fno > 2 && FNR == 1 { # file no. 3+ (not run on x.txt) for (i in a) # loop over letters if (!(i in seen)) # if not in seen array a[i] = a[i]" "0 # append 0 delete seen # delete seen array } $1 in a {                           # if line begins with letter in array
        a[$1] = a[$1]" "$2 # append second field seen[$1]++                      # add letter to seen array
    }
END {
    for (i in a)                        # place zeros for last column
        if (!(i in seen))
            a[i] = a[i]" "0
    for (i in a)                        # print results
        print i a[i]
}' name.txt x.txt y.txt z.txt

Ejemplo de uso / salida

Simplemente copie lo anterior y péguelo con el botón central del mouse en un xterm con el directorio actual que contiene sus archivos y recibirá:

A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

Creación de un script autónomo

Si desea crear un script para ejecutar en lugar de pegarlo en la línea de comando, simplemente incluya el contenido (sin rodearlo entre comillas simples) y luego haga que el archivo sea ejecutable. Por ejemplo, incluye el intérprete como primera línea y el contenido de la siguiente manera:

#!/usr/bin/awk -f

FNR==NR {                           # first file
    a[$1] = "" # fill array with letters as index fno = 1 # set file number counter next # get next record (line) } FNR == 1 { fno++ } # first line in file, increment file count fno > 2 && FNR == 1 { # file no. 3+ (not run on x.txt) for (i in a) # loop over letters if (!(i in seen)) # if not in seen array a[i] = a[i]" "0 # append 0 delete seen # delete seen array } $1 in a {                           # if line begins with letter in array
    a[$1] = a[$1]" "$2 # append second field seen[$1]++                      # add letter to seen array
}
END {
    for (i in a)                    # place zeros for last column
        if (!(i in seen))
            a[i] = a[i]" "0
    for (i in a)                    # print results
        print i a[i]
}

awk procesará los nombres de archivo dados como argumentos en el orden indicado.

Ejemplo de uso / salida

Usando el archivo de script (lo puse names.awky luego lo usé chmod +x names.awkpara hacerlo ejecutable), entonces haría:

$ ./names.awk name.txt x.txt y.txt z.txt
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

Avísame si tienes más preguntas.

4 Sundeep Nov 24 2020 at 14:40

Otro enfoque con GNU awk

$ cat script.awk NF == 1 { name[$1] = $1 for (i = 1; i < ARGC - 1; i++) { name[$1] = name[$1] " 0" } next } { name[$1] = gensub(/ ./, " " $2, ARGIND - 1, name[$1])
}

END {
    for (k in name) {
        print name[k]
    }
}

Llamando al guión:

$ awk -f script.awk name.txt {x,y,z}.txt
A 1 1 0
B 0 4 2
C 3 0 0
D 2 0 2
E 0 3 0
F 0 0 1

La salida muestra el mismo orden que name.txt, pero no creo que sea cierto para todo tipo de entrada.

3 potong Nov 24 2020 at 19:47

Esto podría funcionar para usted (GNU paralelo y unirse):

cp name.txt out && t=$(mktemp) && parallel -j1 join -oauto -e0 -a1 out {} \> $t \&\& mv $t out ::: {x,y,z}.txt

La salida estará en archivo out.

2 DiegoTorresMilano Nov 24 2020 at 15:12

Puedes usar join

join -a1 -e0 -o '0,2.2' name.txt x.txt | join -a1 -e0 -o '0,1.2,2.2' - y.txt | join -a1 -e0 -o '0,1.2,1.3,2.2' - z.txt
1 tshiono Nov 24 2020 at 13:48

Con bashque tal:

#!/bin/bash

declare -A hash                                 # use an associative array
for f in "x.txt" "y.txt" "z.txt"; do            # loop over these files
    while read -r key val; do                   # read key and val pairs
        hash[$f,$key]=$val # assign the hash to val done < "$f"
done

while read -r key; do
    echo -n "$key" # print the 1st column for f in "x.txt" "y.txt" "z.txt"; do # loop over the filenames echo -n " ${hash[$f,$key]:-0}"          # print the associated value or "0" if undefined
    done
    echo                                        # put a newline
done < "name.txt"