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? :-)
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
.
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.
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é).
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.
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
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)
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.
Si vous avez sc
installé, vous pouvez faire:
psc -r < inputfile | sc -W% - > outputfile
Il y a un utilitaire construit à cet effet,
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
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
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
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;
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.
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
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
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
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
.
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
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
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'
#!/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 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 .
Si nous connaissons , foo comporte quatre colonnes:
for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
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
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
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
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
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).
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
crée tmp1
fichier pour qu'il ne soit pas vide.
lit chaque ligne et la transforme en colonne en utilisant tr
colle la nouvelle colonne dans le tmp1
fichier
les copies résultent dans tmp1
.
PS: Je voulais vraiment utiliser les io-descripteurs mais je ne pouvais pas les faire fonctionner.