Una forma eficiente de transponer un archivo en Bash

Tengo un gran archivo separado por tabulaciones formateado como este

X column1 column2 column3 row1 0 1 2 row2 3 4 5 row3 6 7 8 row4 9 10 11 

Me gustaría transponerlo de una manera eficiente utilizando solo comandos bash (podría escribir unas diez líneas de script Perl para hacer eso, pero debería ser más lento de ejecutar que las funciones bash nativas). Entonces la salida debería verse como

 X row1 row2 row3 row4 column1 0 3 6 9 column2 1 4 7 10 column3 2 5 8 11 

Pensé en una solución como esta

 cols=`head -n 1 input | wc -w` for (( i=1; i > output done 

Pero es lento y no parece la solución más eficiente. He visto una solución para vi en esta publicación , pero aún es demasiado lenta. ¿Alguna idea / sugerencia / idea shiny? 🙂

 awk ' { for (i=1; i<=NF; i++) { a[NR,i] = $i } } NF>p { p = NF } END { for(j=1; j<=p; j++) { str=a[1,j] for(i=2; i<=NR; i++){ str=str" "a[i,j]; } print str } }' file 

salida

 $ more file 0 1 2 3 4 5 6 7 8 9 10 11 $ ./shell.sh 0 3 6 9 1 4 7 10 2 5 8 11 

Rendimiento contra la solución Perl de Jonathan en un archivo de 10000 líneas

 $ head -5 file 1 0 1 2 2 3 4 5 3 6 7 8 4 9 10 11 1 0 1 2 $ wc -l < file 10000 $ time perl test.pl file >/dev/null real 0m0.480s user 0m0.442s sys 0m0.026s $ time awk -f test.awk file >/dev/null real 0m0.382s user 0m0.367s sys 0m0.011s $ time perl test.pl file >/dev/null real 0m0.481s user 0m0.431s sys 0m0.022s $ time awk -f test.awk file >/dev/null real 0m0.390s user 0m0.370s sys 0m0.010s 

EDIT por Ed Morton (@ ghostdog74 no dude en eliminar si no lo aprueba).

Tal vez esta versión con algunos nombres de variables más explícitos ayude a responder algunas de las preguntas a continuación y, en general, aclare qué está haciendo el guión. También utiliza tabs como el separador que el OP había pedido originalmente para manejar los campos vacíos y, casualmente, embellece la salida un poco para este caso en particular.

 $ cat tst.awk BEGIN { FS=OFS="\t" } { for (rowNr=1;rowNr<=NF;rowNr++) { cell[rowNr,NR] = $rowNr } maxRows = (NF > maxRows ? NF : maxRows) maxCols = NR } END { for (rowNr=1;rowNr<=maxRows;rowNr++) { for (colNr=1;colNr<=maxCols;colNr++) { printf "%s%s", cell[rowNr,colNr], (colNr < maxCols ? OFS : ORS) } } } $ awk -f tst.awk file X row1 row2 row3 row4 column1 0 3 6 9 column2 1 4 7 10 column3 2 5 8 11 

Las soluciones anteriores funcionarán en cualquier awk (excepto el viejo y roto truco, por supuesto, hay YMMV).

Sin embargo, las soluciones anteriores leen todo el archivo en la memoria: si los archivos de entrada son demasiado grandes para eso, puede hacer esto:

 $ cat tst.awk BEGIN { FS=OFS="\t" } { printf "%s%s", (FNR>1 ? OFS : ""), $ARGIND } ENDFILE { print "" if (ARGIND < NF) { ARGV[ARGC] = FILENAME ARGC++ } } $ awk -f tst.awk file X row1 row2 row3 row4 column1 0 3 6 9 column2 1 4 7 10 column3 2 5 8 11 

que casi no utiliza memoria, pero lee el archivo de entrada una vez por número de campos en una línea, por lo que será mucho más lento que la versión que lee todo el archivo en la memoria. También asume que el número de campos es el mismo en cada línea y usa GNU awk para ENDFILE y ARGIND pero cualquier awk puede hacer lo mismo con pruebas en FNR==1 y END .

Otra opción es usar rs :

 rs -c' ' -C' ' -T 

-c cambia el separador de columna de entrada, -C cambia el separador de columna de salida y -T transpone filas y columnas. No utilice -t lugar de -T , ya que utiliza un número calculado automáticamente de filas y columnas que no suele ser correcto. rs , que lleva el nombre de la función de remodelación en APL, viene con BSD y OS X, pero debe estar disponible desde los administradores de paquetes en otras plataformas.

Una segunda opción es usar Ruby:

 ruby -e'puts readlines.map(&:split).transpose.map{|x|x*" "}' 

Una tercera opción es usar jq :

 jq -R .|jq -sr 'map(./" ")|transpose|map(join(" "))[]' 

jq -R . imprime cada línea de entrada como un literal de cadena JSON, -s ( --slurp ) crea una matriz para las líneas de entrada después de analizar cada línea como JSON, y -r ( --raw-output ) genera el contenido de las cadenas en lugar de cadena JSON literales. El operador / está sobrecargado para dividir cadenas.

Una solución de Python:

 python -c "import sys; print('\n'.join(' '.join(c) for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip()))))" < input > output 

Lo anterior se basa en lo siguiente:

 import sys for c in zip(*(l.split() for l in sys.stdin.readlines() if l.strip())): print(' '.join(c)) 

Este código asume que cada línea tiene el mismo número de columnas (no se realiza relleno).

el proyecto de transposición en sourceforge es un progtwig de C de tipo coreutil exactamente para eso.

 gcc transpose.c -o transpose ./transpose -t input > output #works with stdin, too. 

Pure BASH, sin proceso adicional. Un buen ejercicio:

 declare -a array=( ) # we build a 1-D-array read -a line < "$1" # read the headline COLS=${#line[@]} # save number of columns index=0 while read -a line ; do for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do array[$index]=${line[$COUNTER]} ((index++)) done done < "$1" for (( ROW = 0; ROW < COLS; ROW++ )); do for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do printf "%s\t" ${array[$COUNTER]} done printf "\n" done 

Aquí hay un script de Perl moderadamente sólido para hacer el trabajo. Hay muchas analogías estructurales con la solución awk @ ghostdog74.

 #!/bin/perl -w # # SO 1729824 use strict; my(%data); # main storage my($maxcol) = 0; my($rownum) = 0; while (<>) { my(@row) = split /\s+/; my($colnum) = 0; foreach my $val (@row) { $data{$rownum}{$colnum++} = $val; } $rownum++; $maxcol = $colnum if $colnum > $maxcol; } my $maxrow = $rownum; for (my $col = 0; $col < $maxcol; $col++) { for (my $row = 0; $row < $maxrow; $row++) { printf "%s%s", ($row == 0) ? "" : "\t", defined $data{$row}{$col} ? $data{$row}{$col} : ""; } print "\n"; } 

Con el tamaño de los datos de muestra, la diferencia de rendimiento entre perl y awk fue insignificante (1 milisegundo de un total de 7). Con un conjunto de datos más grande (matriz de 100x100, entradas de 6 a 8 caracteres cada uno), Perl superó ligeramente a awk - 0.026s frente a 0.042s. Ninguno de los dos será un problema.


Tiempos representativos para Perl 5.10.1 (32 bits) frente a awk (versión 20040207 cuando se le da '-V') frente a gawk 3.1.7 (32 bits) en MacOS X 10.5.8 en un archivo que contiene 10.000 líneas con 5 columnas por línea:

 Osiris JL: time gawk -f tr.awk xxx > /dev/null real 0m0.367s user 0m0.279s sys 0m0.085s Osiris JL: time perl -f transpose.pl xxx > /dev/null real 0m0.138s user 0m0.128s sys 0m0.008s Osiris JL: time awk -f tr.awk xxx > /dev/null real 0m1.891s user 0m0.924s sys 0m0.961s Osiris-2 JL: 

Tenga en cuenta que gawk es mucho más rápido que awk en esta máquina, pero aún más lento que perl. Claramente, su millaje variará.

Eche un vistazo al datamash de GNU que puede usarse como datamash transpose . Una versión futura también admitirá la tabulación cruzada (tablas dinámicas)

Si tiene sc instalado, puede hacer:

 psc -r < inputfile | sc -W% - > outputfile 

Suponiendo que todas sus filas tienen el mismo número de campos, este progtwig awk resuelve el problema:

 {for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]} 

En palabras, mientras recorre las filas, para cada campo f crece un ':' - cadena separada col[f] contiene los elementos de ese campo. Después de que haya terminado con todas las filas, imprima cada una de esas cadenas en una línea separada. A continuación, puede sustituir ':' por el separador que desee (por ejemplo, un espacio) canalizando la salida a través de tr ':' ' ' .

Ejemplo:

 $ echo "1 2 3\n4 5 6" 1 2 3 4 5 6 $ echo "1 2 3\n4 5 6" | awk '{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}' | tr ':' ' ' 1 4 2 5 3 6 

Hay una utilidad diseñada para esto,

Utilidad de datos de GNU

 apt install datamash datamash transpose < yourfile 

Tomado de este sitio, https://www.gnu.org/software/datamash/ y http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods

La única mejora que puedo ver en tu propio ejemplo es usar awk, que reducirá el número de procesos que se ejecutan y la cantidad de datos que se canalizan entre ellos:

 /bin/rm output 2> /dev/null cols=`head -n 1 input | wc -w` for (( i=1; i <= $cols; i++)) do awk '{printf ("%s%s", tab, $'$i'); tab="\t"} END {print ""}' input done >> output 

Una solución de perl hackosa puede ser así. Es agradable porque no carga todo el archivo en la memoria, imprime archivos temporales intermedios y luego usa la maravillosa pasta

 #!/usr/bin/perl use warnings; use strict; my $counter; open INPUT, "<$ARGV[0]" or die ("Unable to open input file!"); while (my $line = ) { chomp $line; my @array = split ("\t",$line); open OUTPUT, ">temp$." or die ("unable to open output file!"); print OUTPUT join ("\n",@array); close OUTPUT; $counter=$.; } close INPUT; # paste files together my $execute = "paste "; foreach (1..$counter) { $execute.="temp$counter "; } $execute.="> $ARGV[1]"; system $execute; 

Usé la solución de fgm (¡gracias fgm!), Pero necesitaba eliminar los caracteres de tabs al final de cada fila, así que modifiqué el script de esta manera:

 #!/bin/bash declare -a array=( ) # we build a 1-D-array read -a line < "$1" # read the headline COLS=${#line[@]} # save number of columns index=0 while read -a line; do for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do array[$index]=${line[$COUNTER]} ((index++)) done done < "$1" for (( ROW = 0; ROW < COLS; ROW++ )); do for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do printf "%s" ${array[$COUNTER]} if [ $COUNTER -lt $(( ${#array[@]} - $COLS )) ] then printf "\t" fi done printf "\n" done 

Solo estaba buscando una transposición de bash similar pero con soporte para relleno. Aquí está el script que escribí basado en la solución de fgm, que parece funcionar. Si puede ser de ayuda …

 #!/bin/bash declare -a array=( ) # we build a 1-D-array declare -a ncols=( ) # we build a 1-D-array containing number of elements of each row SEPARATOR="\t"; PADDING=""; MAXROWS=0; index=0 indexCol=0 while read -a line; do ncols[$indexCol]=${#line[@]}; ((indexCol++)) if [ ${#line[@]} -gt ${MAXROWS} ] then MAXROWS=${#line[@]} fi for (( COUNTER=0; COUNTER<${#line[@]}; COUNTER++ )); do array[$index]=${line[$COUNTER]} ((index++)) done done < "$1" for (( ROW = 0; ROW < MAXROWS; ROW++ )); do COUNTER=$ROW; for (( indexCol=0; indexCol < ${#ncols[@]}; indexCol++ )); do if [ $ROW -ge ${ncols[indexCol]} ] then printf $PADDING else printf "%s" ${array[$COUNTER]} fi if [ $((indexCol+1)) -lt ${#ncols[@]} ] then printf $SEPARATOR fi COUNTER=$(( COUNTER + ncols[indexCol] )) done printf "\n" done 

Estaba buscando una solución para transponer cualquier tipo de matriz (nxn o mxn) con cualquier tipo de datos (números o datos) y obtuve la siguiente solución:

 Row2Trans=number1 Col2Trans=number2 for ((i=1; $i <= Line2Trans; i++));do for ((j=1; $j <=Col2Trans ; j++));do awk -v var1="$i" -v var2="$j" 'BEGIN { FS = "," } ; NR==var1 {print $((var2)) }' $ARCHIVO >> Column_$i done done paste -d',' `ls -mv Column_* | sed 's/,//g'` >> $ARCHIVO 

Normalmente uso este pequeño fragmento de awk para este requisito:

  awk '{for (i=1; i<=NF; i++) a[i,NR]=$i max=(max 

Esto simplemente carga todos los datos en una matriz bidimensional a[line,column] y luego la vuelve a imprimir como a[column,line] , de modo que transpone la entrada dada.

Esto necesita hacer un seguimiento de la cantidad max de columnas que tiene el archivo inicial, de modo que se use como el número de filas para imprimir de nuevo.

Si solo desea tomar una línea única (delimitada por comas) $ N de un archivo y convertirla en una columna:

 head -$N file | tail -1 | tr ',' '\n' 

No es muy elegante, pero este comando de “línea única” resuelve el problema rápidamente:

 cols=4; for((i=1;i<=$cols;i++)); do \ awk '{print $'$i'}' input | tr '\n' ' '; echo; \ done 

Aquí cols es el número de columnas, donde puede reemplazar 4 por head -n 1 input | wc -w head -n 1 input | wc -w .

 #!/bin/bash aline="$(head -n 1 file.txt)" set -- $aline colNum=$# #set -x while read line; do set -- $line for i in $(seq $colNum); do eval col$i="\"\$col$i \$$i\"" done done < file.txt for i in $(seq $colNum); do eval echo \${col$i} done 

otra versión con set eval

Aquí hay una solución Haskell. Cuando se comstack con -O2, se ejecuta un poco más rápido que el awk de ghostdog y es un poco más lento que el python delgado de Stephan envuelto en mi máquina para repetidas líneas de entrada “Hola mundo”. Desafortunadamente, la compatibilidad de GHC para pasar el código de línea de comando no existe, por lo que yo sé, por lo que tendrá que escribirlo en un archivo usted mismo. Truncará las filas a la longitud de la fila más corta.

 transpose :: [[a]] -> [[a]] transpose = foldr (zipWith (:)) (repeat []) main :: IO () main = interact $ unlines . map unwords . transpose . map words . lines 

Una solución awk que almacena toda la matriz en la memoria

  awk '$0!~/^$/{ i++; split($0,arr,FS); for (j in arr) { out[i,j]=arr[j]; if (maxr 

Pero podemos "recorrer" el archivo tantas veces como sean necesarias las filas de salida:

 #!/bin/bash maxf="$(awk '{if (mf 

Que (para un recuento bajo de filas de salida es más rápido que el código anterior).

Algunos * onex linners básicos de utilidades, no se necesitan archivos temporales. NB: el OP quería una solución eficiente (es decir, más rápida), y las respuestas principales suelen ser más rápidas. Estas ideas únicas son para aquellos a los que les gustan las “herramientas de software” de * nix, por las razones que sean. En casos raros (por ejemplo, IO y memoria escasa), estos fragmentos pueden ser más rápidos.

Llame al archivo de entrada foo .

  1. Si sabemos que foo tiene cuatro columnas:

     for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done 
  2. Si no sabemos cuántas columnas tiene foo :

     n=$(head -n 1 foo | wc -w) for f in $(seq 1 $n) ; do cut -d ' ' -f $f foo | xargs echo ; done 

    xargs tiene un límite de tamaño y, por lo tanto, haría el trabajo incompleto con un archivo largo. De qué tamaño depende el sistema, por ejemplo:

     { timeout '.01' xargs --show-limits ; } 2>&1 | grep Max 

    La duración máxima del comando que podríamos usar: 2088944

  3. tr y echo :

     for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | tr '\n\ ' ' ; echo ; done 

    … o si el número de columnas es desconocido:

     n=$(head -n 1 foo | wc -w) for f in $(seq 1 $n); do cut -d ' ' -f $f foo | tr '\n\ ' ' ; echo ; done 
  4. Usando set , que al igual que xargs , tiene limitaciones similares basadas en el tamaño de línea de comando:

     for f in 1 2 3 4 ; do set - $(cut -d ' ' -f $f foo) ; echo $@ ; done 

Aquí hay un Bash one-liner que se basa simplemente en convertir cada línea en una columna y pegarlas juntas:

 echo '' > tmp1; \ cat m.txt | while read l ; \ do paste tmp1 <(echo $l | tr -s ' ' \\n) > tmp2; \ cp tmp2 tmp1; \ done; \ cat tmp1 

m.txt:

 0 1 2 4 5 6 7 8 9 10 11 12 
  1. crea el archivo tmp1 para que no esté vacío.

  2. lee cada línea y la transforma en una columna usando tr

  3. pega la nueva columna al archivo tmp1

  4. copias vuelven a tmp1 .

PD: Realmente quería usar io-descriptors pero no pude hacer que funcionaran.

GNU datamash ( https://www.gnu.org/software/datamash ) se adapta perfectamente a este problema con solo una línea de código y un tamaño de archivo potencialmente arbitrario. datamash -W transpose input_file.txt> input_file_transposed.txt