Pergunta

Eu tenho um enorme arquivo separado por tabulações formatado assim

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

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

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

pensei em uma solução assim

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

Mas é lento e não parece a solução mais eficiente.Eu vi uma solução para vi em esta postagem, mas ainda é muito lento.Algum pensamento/sugestão/ideia brilhante?:-)

Foi útil?

Solução

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 em relação à solução Perl de Jonathan em um arquivo de 10.000 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

EDITAR por Ed Morton (@ghostdog74 sinta-se à vontade para excluir se você desaprovar).

Talvez esta versão com alguns nomes de variáveis ​​mais explícitos ajude a responder algumas das perguntas abaixo e a esclarecer de maneira geral o que o script está fazendo.Ele também usa guias como separador que o OP havia solicitado originalmente para lidar com campos vazios e, coincidentemente, embelezar um pouco a saída para este caso específico.

$ 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 funcionarão em qualquer awk (exceto o awk antigo e quebrado, é claro - lá YMMV).

As soluções acima leem o arquivo inteiro na memória - se os arquivos de entrada forem muito grandes para isso, você poderá fazer o seguinte:

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

Outras dicas

Outra opção é usar rs:

rs -c' ' -C' ' -T

-c Altera o separador da coluna de entrada, -C Altera o separador de coluna de saída e -T transpõe linhas e colunas. Não use -t ao invés de -T, porque ele usa um número calculado automaticamente de linhas e colunas que geralmente não estão corretas. rs, que recebeu o nome da função de remodelamento no APL, vem com BSDs e OS X, mas deve estar disponível nos gerentes de pacotes em outras plataformas.

Uma segunda opção é usar Ruby:

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

Uma terceira opção é usar jq:

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

jq -R . imprime cada linha de entrada como uma string json literal, -s (--slurp) cria uma matriz para as linhas de entrada depois de analisar cada linha como JSON, e -r (--raw-output) produz o conteúdo das cordas em vez de literais de cordas JSON. o / O operador está sobrecarregado para dividir strings.

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))

Este código assume que toda linha possui o mesmo número de colunas (nenhum preenchimento é executado).

a transpor O Project on Sourceforge é um programa C do tipo CoreUtil para 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

Dê uma olhada em GNU DataMash que pode ser usado como datamash transpose. Uma versão futura também suportará a tabulação cruzada (tabelas de pivô)

Aqui está um script Perl moderadamente sólido para fazer o trabalho. Existem muitas analogias estruturais com @ghostdog74's awk solução.

#!/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 em 7 no total). Com um conjunto de dados maior (matriz 100x100, entradas de 6 a 8 caracteres cada), o Perl superou o AWK - 0,026S vs 0,042s. Nem é provável que seja um problema.


Times representativos para Perl 5.10.1 (32 bits) vs awk (versão 20040207 quando dado '-v') vs 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: 

Observe que o GAWK é muito mais rápido que o Awk nesta máquina, mas ainda mais lento que o Perl. Claramente, sua milhagem variará.

Se você tem sc instalado, você pode fazer:

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

Existe um utilitário construído para isso,

GNU DataMash Utility

apt install datamash  

datamash transpose < yourfile

Tirado deste site, https://www.gnu.org/software/datamash/ e http://www.thelinuxrain.com/articles/transPosing-lows-and-Columns-3-methods

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, enquanto você faz um pau sobre as fileiras, para cada campo f crescer a ':'-corda separada col[f] contendo os elementos desse campo. Depois de terminar com todas as linhas, imprima cada uma dessas cordas em uma linha separada. Você pode então substituir ':' para o separador que deseja (digamos, um espaço), canalizando a saída através 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

GNU DataMash é perfeitamente adequado para esse problema com apenas uma linha de código e potencialmente arbitrariamente grande tamanho de arquivos!

datamash -W transpose infile > outfile

Uma solução Hackish Perl pode ser assim. É bom porque não carrega todo o arquivo na memória, imprime os arquivos de temperatura intermediária e depois usa a pasta que

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

A única melhoria que posso ver no seu próprio exemplo é usar o AWK, que reduzirá o número de processos que são executados e a quantidade de dados que são canalizados 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

Eu normalmente uso este pequeno awk Snippet 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

Isso apenas carrega todos os dados em uma matriz bidimensional a[line,column] e então imprime -o de volta como a[column,line], para que isso transponha a entrada fornecida.

Isso precisa acompanhar o maxQuantidade imum de colunas O arquivo inicial possui, para que seja usado como o número de linhas para imprimir novamente.

Eu usei a solução da FGM (obrigado FGM!), Mas precisava eliminar os caracteres da guia 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 por transferência semelhante, mas com apoio ao preenchimento. Aqui está o roteiro que escrevi com base na solução da FGM, que parece funcionar. Se pode ajudar ...

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

Se você deseja apenas pegar uma única linha (delimitada de vírgula) $ n de um arquivo e transformá -lo em uma coluna:

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

Não é muito elegante, mas este comando de "linha única" 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 substituir 4 por head -n 1 input | wc -w.

Outro awk solução e entrada limitada com o tamanho da memória que você possui.

awk '{ for (i=1; i<=NF; i++) RtoC[i]= (RtoC[i]? RtoC[i] FS $i: $i) }
    END{ for (i in RtoC) print RtoC[i] }' infile

Isso se junta a cada mesmo número arquivado positivo para juntos e em END Imprime o resultado que seria a primeira linha na primeira coluna, segunda linha na segunda coluna, etc. será lançada:

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11
#!/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

Algum *nix Liners de uma linha padrão, não são necessários arquivos de temperatura. NB: O OP queria um eficiente Corrigir (ou seja, mais rápido) e as respostas superiores geralmente são mais rápidas que esta resposta. Esses Liners são para quem gosta *nix Ferramentas de software, por qualquer motivo. Em casos raros, (por exemplo Escasso IO e memória), esses trechos podem realmente ser mais rápidos do que algumas das melhores respostas.

Ligue para o arquivo de entrada foo.

  1. Se soubermos 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 possui um limite de tamanho e, portanto, tornaria o trabalho incompleto com um arquivo longo. Que limite de tamanho é dependente do sistema, por exemplo:

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

    Comprimento máximo de comando que poderíamos realmente 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 é 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, o que gosta xargs, possui limitações baseadas em tamanho de linha de comando:

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

Aqui está uma solução Haskell. Quando compilado com -o2, ele corre um pouco mais rápido que o Awk de Ghostdog e um pouco mais lento que o de Stephan embrulhado fino c Python na minha máquina para repetidas linhas de entrada "Hello World". Infelizmente, o apoio do GHC para a passagem do código da linha de comando é inexistente, tanto quanto eu sei, então você terá que escrevê-lo em um arquivo. Ele truncará as linhas ao 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 memória

    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

Mas podemos "andar" no arquivo quantas vezes são necessárias linhas de saída:

#!/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 uma baixa contagem de linhas de saída é mais rápida que o código anterior).

Aqui está uma frase de folha que se baseia em simplesmente converter cada linha em uma coluna e paste-ing eles juntos:

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 tmp1 Arquivo para que não esteja vazio.

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

  3. pastas a nova coluna para o tmp1 Arquivo

  4. Cópias resultam em tmp1.

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

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top