web-dev-qa-db-fra.com

Un moyen efficace de transposer un fichier dans Bash

J'ai un énorme fichier séparé par des tabulations 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 le transposer de manière efficace en n'utilisant que des commandes bash (je pourrais écrire un script Perl d'une dizaine de lignes pour le faire, mais cela devrait être plus lent à exécuter que les fonctions natives bash). 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 c'est toujours trop lent. Des pensées/suggestions/idées brillantes? :-)

103
Federico Giorgi
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 par Jonathan de la solution Perl sur un fichier de 10000 lignes

$ 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 de Ed Morton (@ ghostdog74 n’hésitez pas à supprimer si vous désapprouvez).

Peut-être que cette version avec des noms de variables plus explicites aidera à répondre à certaines des questions ci-dessous et à clarifier ce que fait le script. Il utilise également des tabulations comme séparateur demandé à l'origine par l'OP, afin de pouvoir gérer les champs vides et, comme par hasard, améliore légèrement la sortie 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, awk cassé bien sûr - il y a YMMV).

Les solutions ci-dessus lisent cependant l'intégralité du fichier en mémoire. Si les fichiers d'entrée sont trop volumineux, procédez comme suit:

$ 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 n’utilise presque pas de mémoire mais lit le fichier d’entrée une fois par nombre de champs sur une ligne, ce qui le rend beaucoup plus lent que la version qui lit tout le fichier en mémoire. Il suppose également que le nombre de champs est le même sur chaque ligne et utilise GNU awk pour ENDFILE et ARGIND, mais tout awk peut faire la même chose avec des tests.) sur FNR==1 et END.

105
ghostdog74

Une autre option consiste à utiliser rs:

rs -c' ' -C' ' -T

-c change le séparateur de colonnes en entrée, -C change le séparateur de colonnes en sortie et -T transpose des lignes et des colonnes. Ne pas utiliser -t au lieu de -T, car il utilise un nombre de lignes et de colonnes calculé automatiquement qui n’est généralement pas correct. rs, qui porte le nom de la fonction de remodelage dans APL, est fourni avec les BSD et OS X, mais il devrait être disponible auprès des gestionnaires de paquets d'autres plates-formes.

Une deuxième option consiste à utiliser Ruby:

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

Une troisième option consiste à utiliser jq:

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

jq -R . imprime chaque ligne en entrée sous forme de littéral chaîne JSON, -s (--Slurp) crée un tableau pour les lignes d’entrée après l’analyse de chaque ligne au format JSON, et -r (--raw-output) affiche le contenu des chaînes au lieu des littéraux JSON. Le / l'opérateur est surchargé pour scinder les chaînes.

43
nisetama

A 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

Ce qui précède 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 (aucun remplissage n'est effectué).

30
Stephan202

le transpose projet sur sourceforge est un programme C similaire à Coreutil, conçu pour cela.

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

Pure BASH, pas de processus supplémentaire. Un bel exercice:

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
15
Fritz G. Mehner

Jetez un oeil à GNU datamash qui peut être utilisé comme datamash transpose. Une future version supportera également la tabulation croisée (tableaux croisés dynamiques)

14
pixelbeat

Voici un script Perl moyennement 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 des données de l'échantillon, la différence de performance entre Perl et awk était négligeable (1 milliseconde sur un total de 7). Avec un ensemble de données plus volumineux (matrice 100x100, entrées de 6 à 8 caractères chacune), Perl a légèrement surperformé awk - 0.026s contre 0.042s. Ni est susceptible d'être un problème.


Timings représentatifs pour Perl 5.10.1 (32 bits) et awk (version 20040207 avec la mention '-V') vs gawk 3.1.7 (32 bits) sous MacOS X 10.5.8 sur un fichier contenant 10 000 lignes à 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 variera.

9
Jonathan Leffler

Si vous avez sc installé, vous pouvez faire:

psc -r < inputfile | sc -W% - > outputfile
6
Dennis Williamson

Il y a un utilitaire construit à cet effet,

tilitaire GNU datamash

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-méthodes

6
nelaaro

En supposant que toutes vos lignes aient 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, lorsque vous parcourez les lignes, pour chaque champ f, développez une chaîne séparée par un ':' 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 sur une ligne distincte. Vous pouvez ensuite substituer ':' au séparateur souhaité (par exemple, un espace) en dirigeant 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
5
Guilherme Freitas

GNU datamash est parfaitement adapté à ce problème avec une seule ligne de code et une taille de fichier potentiellement importante!

datamash -W transpose infile > outfile
4
Pal

Une solution Perl bidon peut être comme ça. C'est sympa, car il ne charge pas tout le fichier en mémoire, n'imprime que des fichiers temporaires, puis utilise le merveilleux collage.

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

J'utilise normalement ce petit extrait 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

Cela charge simplement toutes les données dans un tableau bidimensionnel a[line,column] puis l’imprime en tant que a[column,line], de sorte qu'il transpose l'entrée donnée.

Ceci doit garder trace de la quantité max maximale des colonnes du fichier initial, afin qu'il soit utilisé comme nombre de lignes à imprimer.

3
fedorqui

La seule amélioration que je puisse voir dans votre exemple consiste à utiliser awk, ce qui réduira le nombre de processus exécutés et la quantité de données 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
Simon C

Je cherchais simplement une transposition similaire, mais avec un support pour le rembourrage. Voici le script que j'ai écrit basé sur la solution fgm, qui semble fonctionner. Si cela peut être utile ...

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

Je recherchais 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
Another.Chemist

Pas très élégant, mais cette commande "monoligne" 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
Felipe

Une autre solution awk et une entrée limitée avec la taille de la mémoire dont vous disposez.

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

Cela associe chaque position de numéro de fichier classé ensemble et dans END affiche le résultat qui serait la première ligne de la première colonne, la deuxième ligne de la deuxième colonne, etc.

X row1 row2 row3 row4
column1 0 3 6 9
column2 1 4 7 10
column3 2 5 8 11
2
αғsнιη

J'ai utilisé la solution de fgm (merci fgm!), Mais je devais éliminer les caractères de tabulation à la fin de chaque ligne, modifiez donc le script de la manière suivante:

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

Si vous souhaitez uniquement extraire une ligne (séparée par des virgules) $ N d'un fichier et la transformer en colonne:

head -$N file | tail -1 | tr ',' '\n'
2
allanbcampbell
#!/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 seteval

1
Dyno Fu

Certains * nix one-liners standard, aucun fichier temporaire n’est nécessaire. NB: le PO souhaitait un correctif efficace , (c'est-à-dire plus rapide), et les réponses les plus fréquentes sont généralement plus rapides que cette réponse. Ces one-liners sont destinés à ceux qui aiment * nix outils logiciels , pour quelque raison que ce soit. Dans de rares cas, (, par exemple, rare IO & mémoire), ces extraits peuvent être plus rapides que certains des meilleurs réponses.

Appelez le fichier d'entrée foo .

  1. Si nous connaissons , 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 taille limite et rendrait donc le travail incomplet avec un fichier long. Quelle taille limite dépend du système, par exemple:

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

    Longueur maximale de commande actuellement utilisable: 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. Utiliser set, qui, comme xargs, a des limitations similaires en fonction de la taille de la ligne de commande:

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

Voici une solution Haskell. Lorsqu'il est compilé avec -O2, il tourne légèrement plus vite que awk et légèrement plus lentement que celui de Stephan. finement emballé c python sur ma machine pour les lignes d'entrée répétées "Hello world". Malheureusement, le support de GHC pour le passage du code de ligne de commande est inexistant à ce que je sache, vous devrez donc l'écrire dans un fichier vous-même.Il tronquera les lignes à la longueur de la ligne la plus courte.

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

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines
0
stelleg

Une solution awk qui stocke tout le tableau en mémoire

    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

Mais nous pouvons "parcourir" le fichier autant de fois que de 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

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

0
user2350426

Voici un Bash one-liner basé sur la conversion simple de chaque ligne en colonne et sur paste- 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 pour qu'il ne soit pas vide.

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

  3. colle la nouvelle colonne dans le tmp1 fichier

  4. les copies résultent dans tmp1.

PS: Je voulais vraiment utiliser les io-descripteurs mais je ne pouvais pas les faire fonctionner.

0
kirill_igum