Une manière efficace de transposer un fichier en Bash

j'ai un énorme fichier séparé des onglets formaté comme ceci

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

je voudrais transposer de manière efficace en utilisant seulement des commandes de bash (je pourrais écrire un script Perl d'une dizaine de lignes pour le faire, mais il devrait être plus lent à exécuter que les fonctions de bash natives). Donc la sortie devrait ressembler à

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

j'ai pensé à une solution comme celle-ci

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

mais c'est lent et ne semble pas la solution la plus efficace. J'ai vu une solution pour vi dans ce post , mais il est toujours trop lent. Des idées/suggestions/idées brillantes? :- )

93
demandé sur Community 2009-11-13 18:13:46

25 réponses

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

sortie

$ 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

Performance à l'égard de Perl solution par Jonathan sur un 10000 lignes du fichier

$ 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

édité par Ed Morton (@ghostdog74 n'hésitez pas à supprimer si vous désapprouvez).

peut-être que cette version avec des noms de variables plus explicites permettra de répondre à certaines des questions ci-dessous et de clarifier en général ce que fait le script. Il utilise également des onglets comme séparateur que L'OP avait demandé à l'origine pour qu'il gère les champs vides et il pretties coïncidence la sortie un peu pour ce cas particulier.

$ 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

les solutions ci - dessus fonctionneront dans n'importe quel awk (sauf vieux, cassé awk bien sûr-il y y y YMV).

les solutions ci-dessus ne lisent le fichier entier dans la mémoire cependant - si les fichiers d'entrée sont trop grands pour cela, alors vous pouvez le faire:

$ 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

qui utilise presque pas de mémoire mais lit le fichier d'entrée une fois par nombre de champs sur une ligne de sorte qu'il sera beaucoup plus lent que la version qui lit le fichier entier dans la mémoire. Il suppose également que le nombre de champs est le même sur chaque ligne et il utilise GNU awk pour ENDFILE et ARGIND mais tout awk peut faire la même chose avec les tests sur FNR==1 et END .

93
répondu ghostdog74 2016-04-10 15:39:03

une autre option est d'utiliser rs :

rs -c' ' -C' ' -T

-c modifie le séparateur de colonne d'entrée, -C modifie le séparateur de colonne de sortie, et -T transpose les lignes et les colonnes. N'utilisez pas -t au lieu de -T , car il utilise un nombre calculé automatiquement de lignes et de colonnes qui n'est généralement pas correct. rs , qui est nommé d'après la fonction reshape dans APL, vient avec BSDs et OS X, mais il devrait être disponible auprès des gestionnaires de paquets sur d'autres plateformes.

une deuxième option est D'utiliser Ruby:

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

une troisième option est d'utiliser jq :

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

jq -R . imprime chaque ligne d'entrée comme une chaîne JSON littérale, -s ( --slurp ) crée un tableau pour les lignes d'ENTRÉE après avoir analysé chaque ligne comme JSON, et -r ( --raw-output ) sort le contenu des chaînes au lieu de JSON string literals. L'opérateur / est surchargé pour séparer les chaînes.

39
répondu user4669748 2018-03-22 08:10:50

une solution 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

ci-dessus est basé sur les éléments suivants:

import sys

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

Ce code suppose que chaque ligne a le même nombre de colonnes (sans rembourrage).

28
répondu Stephan202 2009-11-13 17:21:00

le transposer projet sur sourceforge est un programme C coreutil-like Pour exactement cela.

gcc transpose.c -o transpose
./transpose -t input > output #works with stdin, too.
20
répondu flying sheep 2013-02-08 17:36:25

pur BASH, pas de procédé supplémentaire. Un bel exercice:

declare -a array=( )                      # we build a 1-D-array

read -a line < ""                       # 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 < ""

for (( ROW = 0; ROW < COLS; ROW++ )); do
  for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do
    printf "%s\t" ${array[$COUNTER]}
  done
  printf "\n" 
done
14
répondu Fritz G. Mehner 2009-11-19 17:51:31

voici un script Perl modérément solide pour faire le travail. Il existe de nombreuses analogies structurelles avec la solution awk de @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";
}

avec la taille de l'échantillon, la différence de performance entre perl et awk était négligeable (1 milliseconde sur 7 au total). Avec un ensemble de données plus grand (matrice 100x100, entrées 6-8 caractères chacun), perl a légèrement dépassé awk - 0.026 s vs 0.042 s. Aucun des deux n'est susceptible d'être un problème.


timings représentatifs pour Perl 5.10.1 (32-bit) vs awk (version 20040207 when given '- V') vs gawk 3.1.7 (32-bit) sur MacOS X 10.5.8 sur un fichier contenant 10.000 lignes avec 5 colonnes par ligne:

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: 

notez que gawk est beaucoup plus rapide que awk sur cette machine, mais toujours plus lent que perl. Clairement, votre kilométrage peut varier.

9
répondu Jonathan Leffler 2009-11-16 15:35:28

regardez GNU datamash qui peut être utilisé comme datamash transpose . Une version future prendra également en charge la tabulation croisée (pivot tables)

7
répondu pixelbeat 2016-01-07 09:08:01

si vous avez sc installé, vous pouvez faire:

psc -r < inputfile | sc -W% - > outputfile
6
répondu Dennis Williamson 2009-11-13 16:54:28

en supposant que toutes vos lignes ont le même nombre de champs, ce programme awk résout le problème:

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

en mots, pendant que vous bouclez la boucle sur les lignes, pour chaque champ f cultivez un ':'-chaîne séparée col[f] contenant les éléments de ce champ. Une fois que vous avez terminé avec toutes les lignes, Imprimez chacune de ces chaînes dans une ligne séparée. Vous pouvez alors substituer': 'pour le séparateur que vous voulez (par exemple, un espace) en pipant la sortie par tr ':' ' ' .

exemple:

$ 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
4
répondu Guilherme Freitas 2015-06-10 19:31:16

Il y a un utilitaire pour cela,

GNU datamash utility

apt install datamash  

datamash transpose < yourfile

tiré de ce site, https://www.gnu.org/software/datamash / et http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods

4
répondu nelaaro 2017-04-07 09:00:40

la seule amélioration que je peux voir à votre propre exemple est d'utiliser awk qui réduira le nombre de processus qui sont exécutés et la quantité de données qui est acheminée entre eux:

/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
3
répondu Simon C 2009-11-13 16:08:51

une solution hackish perl peut être comme ceci. Il est agréable parce qu'il ne charge pas tous les fichiers en mémoire, imprime des fichiers intermédiaires temp, puis utilise le tout-wonderful paste

#!/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;
2
répondu Federico Giorgi 2009-11-13 15:49:11

j'ai utilisé la solution de fgm (merci fgm!), mais nécessaire pour éliminer les caractères de l'onglet à la fin de chaque ligne, donc modifié le script ainsi:

#!/bin/bash 
declare -a array=( )                      # we build a 1-D-array

read -a line < ""                       # 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 < ""

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
2
répondu dtw 2010-03-21 22:39:57

j'étais juste à la recherche de bash tranpose similaire, mais avec un support pour le rembourrage. Voici le script que j'ai écrit basé sur la solution de la MGF, qui semble fonctionner. Si cela peut vous aider...

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

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
2
répondu user3251704 2014-01-30 05:27:17

je cherchais une solution pour transposer n'importe quel type de matrice (nxn ou mxn) avec n'importe quel type de données (nombres ou données) et j'ai obtenu la solution suivante:

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
2
répondu Another.Chemist 2014-08-06 04:41:33

j'utilise normalement ce petit awk pour cette exigence:

  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

il suffit de charger toutes les données dans un tableau bidimensionnel a[line,column] et ensuite l'imprime en arrière comme a[column,line] , de sorte qu'il transpose l'entrée donnée.

cela doit garder la trace du max imum quantité de colonnes que le fichier initial a, de sorte qu'il est utilisé comme le nombre de lignes à imprimer en arrière.

2
répondu fedorqui 2015-05-12 07:48:30

si vous voulez seulement saisir une ligne simple (délimitée par une virgule) $N hors d'un fichier et la transformer en une colonne:

head -$N file | tail -1 | tr ',' '\n'
2
répondu allanbcampbell 2015-11-26 13:03:55

pas très élégant, mais cette commande "single-line" résout le problème rapidement:

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

Ici cols est le nombre de colonnes, où vous pouvez remplacer 4 par head -n 1 input | wc -w .

2
répondu Felipe 2016-07-01 03:47:20
#!/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

une autre version avec set eval

1
répondu Dyno Fu 2015-08-19 07:43:32

une autre awk solution et entrée limitée avec la taille de la mémoire que vous avez.

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

il s'agit de la combinaison de chaque numéro positon déposé et de l'impression du résultat dans END qui serait la première ligne dans la première colonne, la deuxième ligne dans la deuxième colonne, etc. Sortie Will:

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11
1
répondu αғsнιη 2018-09-19 16:51:14

Voici une solution de Haskell. Lorsqu'il est compilé avec-O2, il court un peu plus vite que le awk de ghostdog et un peu plus lentement que les lignes d'entrée "Hello world" répétées de Stephan thinly wrapped c python sur ma machine. Malheureusement, le support de GHC pour passer du code de ligne de commande est inexistant autant que je puisse le dire, donc vous devrez l'écrire vous-même dans un fichier. Il tronquera les rangées à la longueur de la rangée la plus courte.

transpose :: [[a]] -> [[a]]
transpose = foldr (zipWith (:)) (repeat [])

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines
0
répondu stelleg 2014-08-26 03:03:40

Un awk solution que de stocker l'ensemble du tableau dans la mémoire

    awk '"151900920"!~/^$/{    i++;
                  split("151900920",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

mais nous pouvons "marcher" le fichier autant de fois que les lignes de sortie sont nécessaires:

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

Qui (pour un faible nombre de lignes de sortie est plus rapide que le code précédent).

0
répondu 2016-01-28 22:46:04

Certains *nix standard util one-liners, pas de fichiers temporaires nécessaires. NB: L'OP voulait un efficace fix, (i.e. plus rapide), et les réponses les plus rapides sont généralement. Ces Uni-liners sont pour ceux qui aiment les "outils logiciels" *nix, pour n'importe quelles raisons. Dans les cas rares (par exemple IO et mémoire rares), ces bribes peuvent en fait être plus rapides.

Appeler le fichier d'entrée foo .

  1. Si nous savons foo comporte quatre colonnes:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
    
  2. si nous ne savons pas combien de colonnes foo a:

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

    xargs a une limite de taille et ferait donc un travail incomplet avec un long fichier. Quelle est la limite de taille qui dépend du système, par exemple:

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

    durée maximale de la commande nous pourrions en fait utiliser: 2088944

  3. tr & echo :

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

    ...ou si le nombre de colonnes est inconnu:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do cut -d ' ' -f $f foo | tr '\n\ ' ' ; echo ; done
    
  4. utilisant set , qui comme xargs , a des limitations similaires basées sur la taille de la ligne de commande:

    for f in 1 2 3 4 ; do set - $(cut -d ' ' -f $f foo) ; echo $@ ; done
    
0
répondu agc 2017-04-21 15:33:26

voici un bash one-liner qui est basé sur la conversion simple de chaque ligne en une colonne et paste - les ensemble:

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. crée tmp1 fichier donc il n'est pas vide.

  2. lit chaque ligne et la transforme en une colonne en utilisant tr

  3. inscrit la nouvelle colonne le tmp1 fichier

  4. copies résultat dans tmp1 .

PS: je voulais vraiment utiliser les io-descripteurs mais je n'ai pas pu les faire fonctionner.

0
répondu kirill_igum 2017-06-19 07:35:47

GNU datamash ( https://www.gnu.org/software/datamash ) est parfaitement adapté pour ce problème avec une seule ligne de code et potentiellement arbitrairement gros filesize! datamash-W transposer input_file.txt > input_file_transposed.txt

0
répondu Pal 2017-09-07 15:00:34