Uma maneira eficiente de transpor um arquivo no Bash

Eu tenho um enorme arquivo separado por tabulação formatado como este

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

Eu gostaria de transpor isso de uma maneira eficiente usando apenas comandos bash (eu poderia escrever um script Perl de dez ou mais linhas para fazer isso, mas deveria ser mais lento para executar do que as funções bash nativas). Então a saída deve parecer

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

Eu pensei em uma solução como esta

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

Mas é lento e não parece a solução mais eficiente. Eu vi uma solução para o vi neste post , mas ainda é muito lento. Quaisquer pensamentos / sugestões / ideias shinys? 🙂

 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 

saída

 $ 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 

Desempenho contra a solução Perl por Jonathan em um arquivo de 10000 linhas

 $ 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 sinta-se livre para excluir se você desaprovar).

Talvez essa versão com nomes de variables ​​mais explícitas ajude a responder algumas das perguntas abaixo e, em geral, esclareça o que o script está fazendo. Ele também usa abas como o separador que o OP originalmente pediu para manipular campos vazios e coincidentemente torna a saída um pouco para este caso em 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 

As soluções acima irão funcionar em qualquer awk (exceto antigo, quebrado awk claro - há YMMV).

As soluções acima lêem todo o arquivo na memory - se os arquivos de input são muito grandes para isso, você pode fazer isso:

 $ 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 usa quase nenhuma memory, mas lê o arquivo de input uma vez por número de campos em uma linha, então será muito mais lento que a versão que lê o arquivo inteiro na memory. Ele também assume que o número de campos é o mesmo em cada linha e usa o GNU awk para ENDFILE e ARGIND mas qualquer awk pode fazer o mesmo com testes em FNR==1 e END .

Outra opção é usar rs :

 rs -c' ' -C' ' -T 

-c altera o separador de colunas de input, -C altera o separador de colunas de saída e -T transpõe linhas e colunas. Não use -t vez de -T , porque ele usa um número calculado automaticamente de linhas e colunas que geralmente não está correto. rs , que é nomeado após a function reshape no APL, vem com BSDs e OS X, mas deve estar disponível a partir de gerenciadores de pacotes em outras plataformas.

Uma segunda opção é usar o Ruby:

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

Uma terceira opção é usar o jq :

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

jq -R . imprime cada linha de input como um literal de string JSON, -s ( --slurp ) cria um array para as linhas de input depois de analisar cada linha como JSON e -r ( --raw-output ) gera o conteúdo de strings em vez de string JSON literais. O operador / está sobrecarregado para dividir as cadeias.

Uma solução 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 

O acima é baseado no seguinte:

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

Esse código supõe que cada linha tenha o mesmo número de colunas (nenhum preenchimento é executado).

o projeto de transposição no sourceforge é um programa C do tipo coreutil exatamente isso.

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

BASH puro, sem processo adicional. Um bom exercício:

 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 

Aqui está um script Perl moderadamente sólido para fazer o trabalho. Existem muitas analogias estruturais com a solução awk do @ 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"; } 

Com o tamanho dos dados da amostra, a diferença de desempenho entre perl e awk foi insignificante (1 milissegundo de um total de 7). Com um dataset maior (matriz 100x100, inputs de 6-8 caracteres cada), o perl superou ligeiramente o awk - 0,026s versus 0,042s. Nem é provável que seja um problema.


Timings representativos para Perl 5.10.1 (32-bit) vs awk (versão 20040207 quando fornecido '-V') versus gawk 3.1.7 (32 bits) no MacOS X 10.5.8 em um arquivo contendo 10.000 linhas com 5 colunas por linha:

 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: 

Note que o gawk é muito mais rápido que awk nesta máquina, mas ainda mais lento que o perl. Claramente, sua milhagem irá variar.

Se você tiver o sc instalado, você pode fazer:

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

Dê uma olhada no datamash do GNU, que pode ser usado como datamash transpose . Uma versão futura também suportará tabulação cruzada (tabelas dinâmicas)

Supondo que todas as suas linhas tenham o mesmo número de campos, este programa awk resolve o problema:

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

Em palavras, à medida que você faz um loop sobre as linhas, para cada campo f crescer uma string separada, com os elementos desse campo. Depois de concluir todas as linhas, imprima cada uma dessas cadeias em uma linha separada. Você pode então replace ':' pelo separador que você quer (digamos, um espaço) passando a saída através de tr ':' ' ' .

Exemplo:

 $ 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 

A única melhoria que posso ver no seu próprio exemplo é usar o awk, que reduzirá o número de processos executados e a quantidade de dados canalizada entre eles:

 /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 

Existe um utilitário construído para isso,

Utilitário datamash do GNU

 apt install datamash datamash transpose < yourfile 

Extraído deste site, https://www.gnu.org/software/datamash/ e http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods

Uma solução de perl hackeada pode ser assim. É legal porque não carrega todo o arquivo na memory, imprime arquivos temporários intermediários e, em seguida, usa a pasta maravilhosa

 #!/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; 

Eu usei a solução fgm (obrigado fgm!), Mas precisava eliminar os caracteres de tabulação no final de cada linha, então modifiquei o script assim:

 #!/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 

Eu estava apenas procurando uma ordem bash semelhante, mas com suporte para preenchimento. Aqui está o script que eu escrevi baseado na solução do fgm, que parece funcionar. Se isso pode ser de ajuda …

 #!/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 

Eu estava procurando uma solução para transpor qualquer tipo de matriz (nxn ou mxn) com qualquer tipo de dado (números ou dados) e obtive a seguinte solução:

 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 

Eu normalmente uso este pequeno fragment de awk para este requisito:

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

Isso apenas carrega todos os dados em um array bidimensional a[line,column] e depois o imprime como a[column,line] , de modo que transpõe a input dada.

Isso precisa manter o controle da quantidade máxima de colunas que o arquivo inicial possui, para que seja usado como o número de linhas a serem impressas.

Se você quiser apenas pegar uma única linha (delimitada por vírgulas) $ N fora de um arquivo e transformá-lo em uma coluna:

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

Não é muito elegante, mas este comando “single-line” resolve o problema rapidamente:

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

Aqui cols é o número de colunas, onde você pode replace 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 

outra versão com set eval

Aqui está uma solução Haskell. Quando compilado com -O2, ele roda um pouco mais rápido que o awk do ghostdog e um pouco mais lento que o python c fino de Stephan na minha máquina para as linhas de input “Hello world” repetidas. Infelizmente, o suporte do GHC para passar o código de linha de comando é inexistente, tanto quanto eu posso dizer, então você terá que escrevê-lo em um arquivo você mesmo. Ele irá truncar as linhas para o comprimento da linha mais curta.

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

Uma solução awk que armazena toda a matriz na memory

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

Mas podemos "percorrer" o arquivo quantas vezes forem necessárias as linhas de saída:

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

Qual (para uma contagem baixa de linhas de saída é mais rápida que o código anterior).

Algumas * one-liners de utilitários padrão nix, nenhum arquivo temporário necessário. NB: o OP queria uma correção eficiente (ou seja, mais rápido), e as respostas principais são geralmente mais rápidas. Estes one-liners são para aqueles que gostam de * nix ‘ferramentas de software’, por qualquer motivo. Em casos raros (por exemplo, IO escasso e memory), esses fragments podem ser realmente mais rápidos.

Chame o arquivo de input foo .

  1. Se sabemos que foo tem quatro colunas:

     for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done 
  2. Se não sabemos quantas colunas foo tem:

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

    xargs tem um limite de tamanho e, portanto, faria um trabalho incompleto com um arquivo longo. Qual limite de tamanho depende do sistema, por exemplo:

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

    Comprimento máximo de comando que poderíamos usar: 2088944

  3. tr & echo :

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

    … ou se o número de colunas for desconhecido:

     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, como o xargs , tem limitações de tamanho de linha de comando semelhantes:

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

Aqui está um one-liner Bash que é baseado simplesmente em converter cada linha em uma coluna e paste las 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. cria o arquivo tmp1 portanto não está vazio.

  2. lê cada linha e a transforma em uma coluna usando tr

  3. cola a nova coluna no arquivo tmp1

  4. copia o resultado de volta no tmp1 .

PS: Eu realmente queria usar descritores io, mas não consegui fazê-los funcionar.

O datamash GNU ( https://www.gnu.org/software/datamash ) é perfeitamente adequado para este problema com apenas uma linha de código e tamanho de arquivo arbitrariamente grande! datamash -W transpõe input_file.txt> input_file_transposed.txt