我有一个巨大的制表符分隔文件格式如下
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
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 ENDFILE
,ARGIND
但是任何awk都可以对on FNR==1
和test进行相同的操作END
.
另一种选择是使用rs
:
rs -c' ' -C' ' -T
-c
更改输入列分隔符,-C
更改输出列分隔符,并-T
转置行和列.不要使用,-t
而是-T
使用自动计算的行数和列数通常不正确.rs
,以APL中的reshape函数命名,附带BSD和OS X,但应该可以从其他平台上的包管理器获得.
第二种选择是使用Ruby:
ruby -e'puts readlines.map(&:split).transpose.map{|x|x*" "}'
第三种选择是使用jq
:
jq -R .|jq -sr 'map(./" ")|transpose|map(join(" "))[]'
jq -R .
将每个输入行打印为JSON字符串文字,-s
(--slurp
)在将每行解析为JSON后为输入行创建数组,并且-r
(--raw-output
)输出字符串的内容而不是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上的transpose项目就是一个类似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
看看可以使用的GNU datamashdatamash transpose
.未来版本还将支持交叉制表(数据透视表)
这是一个适度的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.026s vs 0.042s.两者都不是一个问题.
Perl 5.10.1(32位)与awk(版本20040207,给定'-V')的代表性时序对比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
有一个专用的实用程序,
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
假设你的所有行都有相同数量的字段,这个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