bash - 在 Bash 中转置文件的有效方法

标签 bash parsing unix transpose

我有一个巨大的制表符分隔文件,格式如下

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

我想只使用 bash 命令以一种有效的方式转置它(我可以编写十行左右的 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

但它很慢而且似乎不是最有效的解决方案。我在 this post 中看到了 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

关于bash - 在 Bash 中转置文件的有效方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1729824/

相关文章:

r - 将其他语言与 R 混合

c - 以编程方式确定文件系统 block 大小

c - 有什么方法可以区分规范模式下的 EOL 和 EOF 吗?

bash - 在 Bash 中,如何判断我当前是否在终端中

bash - 在 'source' 的 Dockerfile 中使用 RUN 指令不起作用

Bash 命令行颜色

php - 如何防止依赖XPath的爬虫获取页面内容

linux - 如何将用户名附加到文件名中

HTML DOM 验证器 - PHP 或 JavaScript

java - 使用 GSON 解析带有键和选项卡的 JSON 文件