bash - texto - mostrar columnas awk
Una forma eficiente de transponer un archivo en Bash (24)
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 <= $cols; i++))
do cut -f $i input | tr $''/n'' $''/t'' | sed -e "s//t$//n/g" >> 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 brillante? :-)
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 .
Si sabemos que foo tiene cuatro columnas:
for f in 1 2 3 4 ; do cut -d '' '' -f $f foo | xargs echo ; done
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
tr
yecho
: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
Usando
set
, que al igual quexargs
, 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
crea el archivo
tmp1
para que no esté vacío.lee cada línea y la transforma en una columna usando
tr
pega la nueva columna al archivo
tmp1
copias vuelven a
tmp1
.
PD: Realmente quería usar io-descriptors pero no pude hacer que funcionaran.
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á.
Aquí hay una solución Haskell. Cuando se compila 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
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)
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
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
Hay una utilidad diseñada para esto,
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
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
.
Normalmente uso este pequeño fragmento de awk
para este requisito:
awk ''{for (i=1; i<=NF; i++) a[i,NR]=$i
max=(max<NF?NF:max)}
END {for (i=1; i<=max; i++)
{for (j=1; j<=NR; j++)
printf "%s%s", a[i,j], (j==NR?RS:FS)
}
}'' file
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.
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.
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
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''
Si tiene sc
instalado, puede hacer:
psc -r < inputfile | sc -W% - > outputfile
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
Suponiendo que todas sus filas tienen el mismo número de campos, este programa 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
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<j){ maxr=j} # max number of output rows.
}
}
END {
maxc=i # max number of output columns.
for (j=1; j<=maxr; j++) {
for (i=1; i<=maxc; i++) {
printf( "%s:", out[i,j])
}
printf( "%s/n","" )
}
}'' infile
Pero podemos "recorrer" el archivo tantas veces como sean necesarias las filas de salida:
#!/bin/bash
maxf="$(awk ''{if (mf<NF); mf=NF}; END{print mf}'' infile)"
rowcount=maxf
for (( i=1; i<=rowcount; i++ )); do
awk -v i="$i" -F " " ''{printf("%s/t ", $i)}'' infile
echo
done
Que (para un recuento bajo de filas de salida es más rápido que el código anterior).
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).
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 = <INPUT>) {
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 pestañas 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
el proyecto de transpose en sourceforge es un programa de C de tipo coreutil exactamente para eso.
gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.
#!/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
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 pestañas 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
.