web-dev-qa-db-ja.com

Bashでファイルを転置する効率的な方法

このようにフォーマットされた巨大なタブ区切りファイルがあります

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

私はtransposebashコマンドのみを使用して効率的な方法でそれをしたいと思います(それを行うために10行程度のPerlスクリプトを書くことができますが、ネイティブのbash関数よりも実行速度が遅くなります)。したがって、出力は次のようになります。

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

私はこのような解決策を考えました

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

しかし、それは遅く、最も効率的な解決策ではないようです。 この投稿 でviの解決策を見てきましたが、それでもまだ遅いです。何か考え/提案/素晴らしいアイデア? :-)

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

出力

$ 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

10000行ファイルでのJonathanによるPerlソリューションに対するパフォーマンス

$ 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

エド・モートンによる編集(@ ghostdog74は、あなたが不承認になっても自由に削除できます)。

より明確な変数名を含むこのバージョンは、以下の質問のいくつかに答え、スクリプトが何をしているのかを一般的に明らかにするのに役立つかもしれません。また、OPが最初に要求した区切り文字としてタブを使用するため、空のフィールドを処理し、この特定の場合に偶然に出力を少し見栄え良くします。

$ 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

上記の解決策は、すべてのawkで機能します(もちろん、壊れたawkを除きます-YMMVがあります)。

上記のソリューションは、ファイル全体をメモリに読み込みます-入力ファイルが大きすぎる場合は、これを行うことができます:

$ 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

メモリをほとんど使用しませんが、行のフィールド数ごとに1回入力ファイルを読み取るため、ファイル全体をメモリに読み込むバージョンよりもはるかに遅くなります。また、フィールドの数は各行で同じであり、ENDFILEおよびARGINDに対してGNU awkを使用しますが、awkはFNR==1およびENDのテストでも同じことができます。

105
ghostdog74

別のオプションは、rsを使用することです。

rs -c' ' -C' ' -T

-cは入力列の区切り文字を変更し、-Cは出力列の区切り文字を変更し、-Tは行と列を入れ替えます。 -tの代わりに-Tを使用しないでください。通常は正しくない行と列の数を自動的に計算して使用するためです。 rsは、APLのreshape関数にちなんで命名されており、BSDおよびOS Xに付属していますが、他のプラットフォームのパッケージマネージャーから入手できるはずです。

2番目のオプションは、Rubyを使用することです。

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

3番目のオプションは、jqを使用することです。

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

jq -R .は各入力行をJSON文字列リテラルとして出力し、-s--Slurp)は各行をJSONとして解析した後に入力行の配列を作成し、-r--raw-output)は、JSON文字列リテラルの代わりに文字列の内容を出力します。 /演算子は、文字列を分割するためにオーバーロードされます。

43
nisetama

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

上記は以下に基づいています。

import sys

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

このコードは、すべての行の列数が同じであると想定しています(パディングは実行されません)。

30
Stephan202

sourceforgeの transpose プロジェクトは、まさにそのためのcoreutilのようなCプログラムです。

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

純粋な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\t" ${array[$COUNTER]}
  done
  printf "\n" 
done
15
Fritz G. Mehner

datamash transposeのように使用できる GNU datamash をご覧ください。将来のバージョンでは、クロス集計もサポートされる予定です(ピボットテーブル)

14
pixelbeat

これを行うための適度に堅実なPerlスクリプトを次に示します。 @ ghostdog74のawkソリューションには多くの構造上の類似点があります。

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

サンプルデータサイズでは、Perlとawkのパフォーマンスの違いはごくわずかでした(合計7つのうち1ミリ秒)。より大きなデータセット(100x100マトリックス、エントリ6〜8文字)を使用すると、Perlはawkをわずかに上回りました-0.026秒と0.042秒。どちらも問題になる可能性はありません。


Perl 5.10.1(32ビット)対awk( '-V'を指定した場合はバージョン20040207)vs gawk 3.1.7(32ビット)の代表的なタイミングライン:

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: 

このマシンでは、gawkはawkよりも非常に高速ですが、Perlよりも遅いことに注意してください。明らかに、走行距離は異なります。

9

sc がインストールされている場合、次のことができます。

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

これには専用のユーティリティがあり、

GNU datamashユーティリティ

apt install datamash  

datamash transpose < yourfile

このサイトから取得 https://www.gnu.org/software/datamash/ および http://www.thelinuxrain.com/articles/transposing-rows-and-columns -3-メソッド

6
nelaaro

すべての行に同じ数のフィールドがあると仮定すると、このawkプログラムは問題を解決します。

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

つまり、行をループすると、フィールドごとにfが、そのフィールドの要素を含む ':'で区切られた文字列col[f]を成長させます。すべての行の処理が完了したら、これらの文字列をそれぞれ個別の行に出力します。次に、tr ':' ' 'を介して出力をパイピングすることにより、必要な区切り文字(スペースなど)を「:」に置き換えることができます。

例:

$ 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

GNU datamash は、1行のコードと潜在的に任意の大きなファイルサイズでこの問題に最適です!

datamash -W transpose infile > outfile
4
Pal

ハック的なPerlソリューションは次のようになります。メモリ内のすべてのファイルをロードせず、中間の一時ファイルを印刷し、すべてが素晴らしいペーストを使用するため、素晴らしい

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

通常、この要件にはこの小さなawkスニペットを使用します。

  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

これは、すべてのデータを2次元配列a[line,column]にロードし、それをa[column,line]として出力して、指定された入力を転置します。

これは、最初のファイルにあるmaximum列の量を追跡する必要があるため、印刷する行数として使用されます。

3
fedorqui

私があなた自身の例で見ることができる唯一の改善は、実行されるプロセスの数とそれらの間でパイプされるデータの量を減らすawkの使用です:

/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

同様のbash転置を探していましたが、パディングがサポートされていました。ここに、fgmソリューションに基づいて書いたスクリプトがありますが、うまくいくようです。それが助けになる場合...

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

私は、あらゆる種類の行列(nxnまたはmxn)をあらゆる種類のデータ(数値またはデータ)に転置する解決策を探していましたが、次の解決策を得ました。

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

それほどエレガントではありませんが、この「単一行」コマンドは問題を迅速に解決します。

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

ここで、colsは列の数で、4をhead -n 1 input | wc -wに置き換えることができます。

2
Felipe

別のawkソリューションと、使用するメモリのサイズに制限のある入力。

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

これは、それぞれ同じフィールド番号を一緒に結合し、ENDに、最初の列の最初の行、2番目の列の2番目の行などの結果を出力します。

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

Fgmのソリューションを使用しました(fgm!に感謝します)が、各行の最後にあるタブ文字を削除する必要があるため、スクリプトを次のように変更しました。

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

ファイルから単一の(コンマ区切り)行$ Nのみを取得して、列に変換する場合:

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

setevalを含む別のバージョン

1
Dyno Fu

いくつかの* nix標準ユーティリティワンライナー、一時ファイルは不要です。注:OPは効率的な修正(つまり、より高速)を望んでおり、通常、上位の回答はこの回答よりも高速です。 これらのワンライナーは* nixが好きな人向けです ソフトウェアツール 、何らかの理由で。まれに、(e.g。希少なIOおよびメモリ)、これらのスニペットは実際にいくつかの上位の回答よりも高速です。

入力ファイルを呼び出しますfoo

  1. fooに4つの列がある場合:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
    
  2. カラムfooの数がわからない場合:

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

    xargsにはサイズ制限があるため、長いファイルでは不完全な作業になります。システムに依存するサイズ制限、たとえば:

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

    実際に使用できるコマンドの最大長:2088944

  3. trecho

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

    ...または列の数が不明な場合:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
    
  4. setのようなxargsを使用すると、同様のコマンドラインサイズに基づく制限があります。

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

これがHaskellソリューションです。 -O2でコンパイルすると、ghostdogのawkよりもわずかに速く、Stephanのawkよりも少し遅くなります。 薄く包まれたc マシン上のpythonは、「Hello world」の入力行を繰り返します。残念ながら、コマンドラインコードを渡すためのGHCのサポートは、私が知る限り存在しないため、自分でファイルに書き込む必要があります。行を最も短い行の長さに切り捨てます。

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

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

配列全体をメモリに保存するawkソリューション

    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

ただし、出力行が必要な回数だけファイルを「歩く」ことができます。

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

これ(出力行の数が少ない場合は、前のコードよりも高速です)。

0
user2350426

以下は、各行を列に変換し、paste- ingするだけのBashワンライナーです。

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. tmp1ファイルを作成して、空にならないようにします。

  2. 各行を読み取り、trを使用して列に変換します

  3. 新しい列をtmp1ファイルに貼り付けます

  4. 結果をtmp1にコピーします。

PS:io記述子を使いたかったのですが、機能させることができませんでした。

0
kirill_igum