1. ホーム
  2. bash

[解決済み] Bashでファイルを転置する効率的な方法

2022-08-24 21:25:05

質問

次のような形式の巨大なタブ区切りファイルを持っています。

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

私は 転置 をbashコマンドだけで効率的に行いたい(これを行うために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のための解決策を このポスト でviの解決策を見ましたが、やはり遅すぎです。どんな考え/提案/素晴らしいアイデアでも?:-)

どのように解決するのですか?

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

Jonathanによる10000行のファイルでの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

Ed Morton氏による編集(@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

はほとんどメモリを使いませんが、入力ファイルを行のフィールド数だけ読み込むので、ファイル全体をメモリに読み込むバージョンよりずっと遅くなります。また、各行のフィールド数が同じであることを仮定しており、GNU awk を使って ENDFILEARGIND をテストすることができますが、どんな awk でも同じことができます。 FNR==1END .