Question

J'ai un énorme fichier séparé par des tabulations au format comme celui-ci

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

Je voudrais Transpose il d'une manière efficace en utilisant les commandes que bash (je pourrais écrire une dizaine de lignes de script Perl pour le faire, mais il devrait être plus lente à exécuter que le bash natif les fonctions). Ainsi, la sortie devrait ressembler à

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

Je pensais à une telle solution

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 il est lent et ne semble pas la solution la plus efficace. Je l'ai vu une solution pour vi dans ce poste , mais il est encore trop lent. Toutes les pensées / suggestions / idées brillantes? : -)

Était-ce utile?

La solution

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

output

$ 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 contre solution Perl par Jonathan sur un fichier 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 par Ed Morton (@ ghostdog74 ne hésitez pas à supprimer si vous désapprouvez).

Peut-être que cette version avec quelques noms de variables plus explicites répondra à certaines des questions ci-dessous et de préciser généralement ce que fait le script. Il utilise également des onglets comme séparateur qui l'OP avait initialement demandé si ce serait traiter les champs vides et il Pretties par 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 travailleront en toute awk. (Sauf vieux, awk cassé bien sûr - il YMMV)

Les solutions ci-dessus lisent tout le fichier en mémoire si - si les fichiers d'entrée sont trop grands pour que vous pouvez faire ceci:

$ 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 en mémoire. Il assume également le nombre de champs est le même sur chaque ligne et utilise GNU awk pour ENDFILE et ARGIND mais toute awk peut faire la même chose avec des tests sur FNR==1 et END.

Autres conseils

Une autre option consiste à utiliser rs:

rs -c' ' -C' ' -T

-c modifie le séparateur de colonne d'entrée, -C modifie le séparateur de colonnes de sortie, et -T transpose les lignes et les colonnes. Ne pas utiliser -t au lieu de -T, car elle utilise un nombre calculé automatiquement des lignes et des colonnes qui ne sont généralement pas correct. rs, qui porte le nom de la fonction Reshape dans APL, est livré avec BSDs et OS X, mais il devrait être disponible auprès des gestionnaires de paquets sur 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 d'entrée comme une chaîne JSON littérale, -s (--slurp) crée un tableau pour les lignes d'entrée après l'analyse de chaque ligne comme JSON, et -r (--raw-output) délivre en sortie le contenu de chaînes au lieu de chaînes littérales JSON. L'opérateur / est surchargé de diviser les chaînes.

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

Le ci-dessus est basé sur les points suivants:

import sys

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

Ce code ne suppose que chaque ligne a le même nombre de colonnes (pas de remplissage est effectué).

projet sur Transpose SourceForge est un programme C comme coreutil exactement cela.

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

BASH pur, aucun 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

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

Voici un script Perl modérément solide pour faire le travail. Il y a beaucoup d'analogies structurelles avec la solution de 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";
}

Avec l'échantillon de taille de données, 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 grand (100x100 matrice, les entrées 6-8 caractères), perl awk légèrement dépassé - 0.026s vs 0.042s. Ceci ne devrait pas être un problème.


minutage représentatives pour Perl 5.10.1 (32 bits) vs awk (version 20040207 lorsqu'on les administre '-V') vs gawk 3.1.7 (32 bits) sur MacOS X 10.5.8 sur un fichier contenant 10000 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 encore plus lent que Perl. De toute évidence, votre kilométrage varie.

Si vous avez sc installé, vous pouvez faire:

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

Il y a un utilitaire construit à cet effet pour cela,

GNU datamash utilitaire

apt install datamash  

datamash transpose < yourfile

Tiré de ce site, https://www.gnu.org/software/datamash/ et http://www.thelinuxrain.com/articles/ transposition rangées-colonnes et-3-méthodes

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, comme vous en boucle sur les lignes, pour chaque f sur le terrain se développer 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, imprimer chacune de ces chaînes dans une ligne distincte. Vous pouvez ensuite remplacer « : ». Pour le séparateur que vous voulez (par exemple, un espace) en redirigeant 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

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

datamash -W transpose infile > outfile

Une solution de Perl peut être hackish comme ça. Il est agréable, car il ne se charge pas tout le fichier en mémoire, imprime des fichiers temporaires intermédiaires, et utilise ensuite la pâte tout merveilleux

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

La seule amélioration que je peux voir à votre propre exemple utilise awk ce qui réduira le nombre de processus qui sont exécutés et la quantité de données qui est canalisé 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

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

Cette charge simplement toutes les données dans un tableau à deux dimensions a[line,column] puis il imprime en arrière comme a[column,line], de sorte qu'il transpose l'entrée donnée.

Il a besoin de garder une trace du montant de maximum des colonnes du fichier initial, de sorte qu'il est utilisé comme le nombre de lignes à imprimer en arrière.

je solution de FGM (merci de FGM!), Mais avait besoin d'éliminer les caractères de tabulation à la fin de chaque ligne, ainsi modifié le script ainsi:

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

Je cherchais juste transposez bash similaire, mais avec le soutien pour le rembourrage. Voici le script que j'ai écrit basé sur la solution de la FGM, qui semblent 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

Je cherchais une solution pour transposer tout type de matrice (nxn ou MXN) avec tout type de données (chiffres ou données) et a 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

Si vous voulez seulement saisir une seule ligne $ N (délimité par des virgules) d'un fichier et la transformer en une colonne:

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

Pas très élégant, mais cette commande « une seule ligne » permet de résoudre rapidement le problème:

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

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

Une autre solution awk et entrée limitée à 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

Cette joint chaque numéro même déposé positon en ensemble et END imprime le résultat qui serait la première ligne dans la première colonne, deuxième ligne dans la deuxième colonne, etc. Will sortie:

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

une autre version avec set eval

Certains * nix norme util one-liners, aucun fichier temp nécessaire. NB: l'OP voulait un efficace fix, (à savoir plus rapide), et les réponses de tête sont généralement plus rapide que cette réponse. Ces one-liners sont pour ceux qui aiment * nix noreferrer outils logiciels , pour une raison quelconque. Dans de rares cas, ( par exemple. rare IO et mémoire), ces extraits peuvent effectivement être plus rapide que certains des meilleurs réponses.

Appelez le fichier d'entrée foo .

  1. Si nous savons foo a 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 rendrait donc un travail incomplet avec un fichier long. Quelle est la limite de la taille dépend du système, par exemple .:

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

    La longueur maximale de commande qui pourrait être utilisée: 2.088.944

  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 sont inconnues:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
    
  4. Utilisation set, qui, comme xargs, a une taille de ligne de commande similaire limites fondées:

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

Voici une solution Haskell. Lorsque compilé avec -O2, il court un peu plus vite que la awk de ghostdog et un peu plus lent que Stephan finement enveloppé de c python sur ma machine pour répétées « Bonjour tout le monde » lignes d'entrée. Malheureusement, le soutien de GHC pour passer le code de ligne de commande est inexistante pour autant que je peux dire, vous devrez l'écrire dans un fichier vous-même. Il tronquer les lignes à la longueur de la plus courte ligne.

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

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

Une solution de awk qui stocke la totalité du 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 on peut « 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

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

Voici un bash one-liner qui est basée sur la simple conversion de chaque ligne dans une colonne et les paste-ing 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 un fichier de tmp1 il est donc pas vide.

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

  3. colle la nouvelle colonne dans le fichier tmp1

  4. copies résultat dans tmp1.

PS. Je voulais vraiment utiliser io-descripteurs mais n'a pas pu les amener à travailler

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top