Un moyen efficace de transposer un fichier dans Bash
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? : -)
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é).
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,
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 max
imum 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 .
-
Si nous savons foo a 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 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
-
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
-
Utilisation
set
, qui, commexargs
, 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
-
crée un fichier de
tmp1
il est donc pas vide. -
lit chaque ligne et la transforme en une colonne en utilisant
tr
-
colle la nouvelle colonne dans le fichier
tmp1
-
copies résultat dans
tmp1
.
PS. Je voulais vraiment utiliser io-descripteurs mais n'a pas pu les amener à travailler