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

139
我有一个格式如下的巨大的制表符分隔文件:
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编辑器的解决方案,但仍然很慢。有什么想法/建议/创意吗? :-)


13
你为什么认为会有一个比Perl脚本更快的bash脚本存在呢?这正是Perl擅长解决的问题类型。 - Mark Pim
2
@mark,如果这是纯Bash,那么它可能比将所有的cut/sed等工具链接在一起更快。但是,如果你将“Bash”定义为组合工具,则编写一个awk脚本与Perl相比在文本处理方面是可比的。 - ghostdog74
再加一个不理解为什么Perl在这里会很慢的人。是写代码慢吗?还是执行慢?我真的不喜欢Perl,但它确实擅长处理这种任务。 - Corey Porter
如果您的列/字段具有固定的大小/宽度,则可以使用Python文件查找来避免将文件读入内存。您的列/字段是否具有固定的大小/宽度? - tommy.carstensen
3
任何认为Shell脚本比awk或perl更快的人需要阅读http://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice,以便了解为什么情况并非如此。请注意,Shell循环处理文本被视为不良实践。 - Ed Morton
33个回答

6

如果您已经安装了sc,您可以执行以下操作:

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

4
请注意,这个程序只支持有限数量的行,因为sc将它的列命名为一个或两个字符的组合。上限是26 + 26^2 = 702 - Thor

5

我通常使用这个小 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

这只是将所有数据加载到二维数组a[line,column]中,然后按a[column,line]的方式打印回来,以便转置所给输入。
这需要跟踪初始文件具有的max列数的最大值,以便将其用作要打印回的行数。

3
一个好的 Perl 解决方案可能是这样的。它很棒,因为它不会在内存中加载所有文件,在输出中打印中间临时文件,然后使用十分神奇的 paste。
#!/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;

使用粘贴和临时文件只是额外的不必要操作。你可以直接在内存中进行操作,例如数组/哈希。 - ghostdog74
3
是的,但这是否意味着需要将所有内容都保留在内存中?我正在处理的文件大约有2-20GB大小。 - Federico Giorgi

3

我能看到你的例子唯一的改进就是使用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

我来晚了,但是这个怎么样:

cat table.tsv | python -c "import pandas as pd, sys; pd.read_csv(sys.stdin, sep='\t').T.to_csv(sys.stdout, sep='\t')"

或者如果它被压缩了就用zcat

这假设你在你的版本的python中安装了pandas


2

这是另一种使用 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中打印结果,该结果将成为第一列中的第一行,第二列中的第二行等等。

输出结果:

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

2

一些*nix标准工具的单行代码,无需临时文件。注意:提问者需要一个高效的解决方案(即更快),而前几个答案通常比这个答案更快。这些单行代码适用于那些喜欢*nix软件工具的人,不管出于什么原因。在罕见情况下(例如稀缺的IO和内存),这些片段实际上可能比一些顶级答案更快。

将输入文件命名为foo

  1. If we know foo has four columns:

    for f in 1 2 3 4 ; do cut -d ' ' -f $f foo | xargs echo ; done
    
  2. If we don't know how many columns foo has:

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

    xargs has a size limit and therefore would make incomplete work with a long file. What size limit is system dependent, e.g.:

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

    Maximum length of command we could actually use: 2088944

  3. tr & echo:

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

    ...or if the # of columns are unknown:

    n=$(head -n 1 foo | wc -w)
    for f in $(seq 1 $n); do 
        cut -d ' ' -f $f foo | tr '\n' ' ' ; echo
    done
    
  4. Using set, which like xargs, has similar command line size based limitations:

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

4
这些方法的速度比awk或perl慢得多,而且容易出错。请阅读http://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice. - Ed Morton
@EdMorton,谢谢,我对我的回答进行了合格的介绍以解决您的速度问题。关于“脆弱”:不是3),当程序员知道数据对于给定技术是安全的时候,其他情况也不是;而且,POSIX兼容的shell代码难道不是比perl更稳定的标准吗? - agc
抱歉,我不太熟悉 Perl。在这种情况下,应使用awk工具。与awk脚本一样,cutheadecho等都不再是 POSIX 兼容的 shell 代码——它们在每个 UNIX 安装中都是标准的。如果你可以使用 awk,就没有理由使用一组工具,这些工具的组合需要你在输入文件和执行脚本的目录的内容方面小心谨慎,而结果会更快、更健壮。 - Ed Morton
请注意,我并不反对使用awk,但是情况各异。原因之一:for f in cut head xargs seq awk ; do wc -c $(which $f) ; done 当存储速度过慢或IO过低时,更大的解释器会使事情变得更糟,无论它们在更理想的情况下有多好。原因之二:awk(或大多数语言)也比专门设计用于完成一项任务的小型工具具有更陡峭的学习曲线。当运行时间比编码人员的工时更便宜时,使用“软件工具”进行简单编码可以节省成本。 - agc

2

我使用了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

我正在寻找一种解决方案,可以对任何类型的矩阵(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

如果你只想从文件中抓取单个(逗号分隔)的第N行并将其转换为列:

head -$N file | tail -1 | tr ',' '\n'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接