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

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

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 

但是速度很慢,似乎并不是最有效的解决方案。 在这篇文章中 ,我已经看到了一个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 

在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 

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上进行相同的测试。

另一种选择是使用rs

 rs -c' ' -C' ' -T 

-c更改输入列分隔符, -C更改输出列分隔符, -T转换行和列。 不要使用-t ,因为它使用自动计算的行数和列数,通常不正确。 rs ,以APL中的reshape函数命名,带有BSD和OS X,但是应该可以从其他平台的软件包管理器中获得。

第二个选择是使用Ruby:

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

第三个选项是使用jq

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

jq -R . 将每个输入行打印为JSON字符串, -s在将每行解析为JSON之后为输入行创建一个数组,而-r输出原始字符串而不是JSON字符串。 /运算符被重载以拆分字符串。

一个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)) 

这段代码确实假设每一行都有相同的列数(不执行填充)。

sourceforge上的转置项目就是一个类似于coreutil的C程序。

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

纯粹的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 

这是一个适当的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毫秒)。 随着更大的数据集(100×100矩阵,每个条目6-8个字符),perl略胜过awk – 0.026s vs 0.042s。 这也不是一个问题。


Perl 5.10.1(32位)与awk(给定'-V'时的版本20040207)与MacOS X 10.5.8上的gawk 3.1.7(32位)在包含10,000行,每行5列线:

 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慢。 显然,你的里程会有所不同。

如果你安装了sc ,你可以这样做:

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

看看可以像datamash transpose一样使用的GNU datamash 。 未来的版本也将支持交叉表(数据透视表)

我可以看到你自己的例子的唯一改进是使用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 

假设你所有的行都有相同数量的字段,这个awk程序解决了这个问题:

 {for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[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 

一个黑客的解决方案可以是这样的。 这很好,因为它不会加载内存中的所有文件,打印中间临时文件,然后使用所有美妙的粘贴

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

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

我只是寻找类似的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 

我正在寻找一种解决方案来转置任何种类的数据(数字或数据)矩阵(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 

我通常使用这个小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列数量,以便将其用作要打印的行数。

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

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

不是很优雅,但是这个“单线”命令很快解决了这个问题:

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

这里cols是列数,你可以用head -n 1 input | wc -w替换4 head -n 1 input | wc -w

有一个为此建立的实用工具,

GNU datamash工具

 apt install datamash datamash transpose < yourfile 

采取从这个网站, https://www.gnu.org/software/datamash/和http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods

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

另一个版本与set eval

这是一个Haskell解决方案。 当使用-O2编译时,它比ghostdog的awk运行得稍快,比我的机器上Stephan的重复的“Hello world”输入行上的薄薄的c python稍慢。 不幸的是,就我所知,GHC对传递命令行代码的支持是不存在的,所以你必须自己把它写到一个文件中。 它会将行截断为最短行的长度。

 transpose :: [[a]] -> [[a]] transpose = foldr (zipWith (:)) (repeat []) main :: IO () main = interact $ unlines . map unwords . transpose . map words . lines 

将整个数组存储在内存中的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 -vi="$i" -F " " '{printf("%s\t ", $i)}' infile echo done 

其中(对于较少的输出行比以前的代码更快)。

一些* nix标准的util单行程,不需要临时文件。 注意:OP想要一个有效的解决方法,(即更快),最好的答案通常更快。 这些单线是为了那些喜欢* nix'软件工具'的人,无论出于何种原因。 在极少数情况下(如IO和内存稀少),这些片段实际上可能会更快。

调用输入文件foo

  1. 如果我们知道foo有四列:

     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. xargs一样使用set命令行大小也有类似的限制:

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

这里是一个Bash单线程,它基于将每一行简单地转换成列paste它们paste在一起:

 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描述符,但无法让它们工作。

GNU datamash( https://www.gnu.org/software/datamash )非常适合这个问题,只有一行代码和可能任意大的文件大小! datamash -W转置input_file.txt> input_file_transposed.txt