bash agrega / agrega nuevas columnas de otros archivos
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
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
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.
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.
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.
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.
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
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"