csvtk
-t
使用制表符作为分隔符,-d
指定分隔符:
$ seq 4|paste - -|csvtk -t transpose
1 3
2 4
$ seq 4|paste -d, - -|csvtk -d, transpose
1,3
2,4
-l
(--lazy-quotes
) 允许在未被双引号包围的字段中使用双引号,但我认为没有办法禁用输出中的 CSV 格式转义双引号:
$ csvtk -t transpose<<<$'aa"bb\t2\n3\t4'
[ERRO] parse error on line 1, column 3: bare " in non-quoted-field
$ csvtk -lt transpose<<<$'aa"bb\t2\n3\t4'
"aa""bb" 3
2 4
另一个需要注意的是,
-l
并不能阻止删除不需要在 CSV 中加引号的字段周围的双引号:
$ csvtk -lt transpose<<<$'"1"\t2\n3\t4'
1 3
2 4
awk
Gawk版本使用数组的数组:
tp(){ awk '{for(i=1;i<=NF;i++)a[i][NR]=$i}END{for(i in a)for(j in a[i])printf"%s"(j==NR?RS:FS),a[i][j]}' "${1+FS=$1}";}
普通的awk版本使用多维数组(在我的基准测试中,这个版本运行时间大约是两倍):
tp(){ awk '{for(i=1;i<=NF;i++)a[i,NR]=$i}END{for(i=1;i<=NF;i++)for(j=1;j<=NR;j++)printf"%s"(j==NR?RS:FS),a[i,j]}' "${1+FS=$1}";}
macOS自带的Brian Kerningham的nawk
版本来自2007年,不支持数组的数组。
如果要使用空格作为字段分隔符而不会折叠多个空格序列,请使用FS='[ ]'
。
rs
rs
是一种BSD实用程序,也随macOS一起提供,但应该可以从其他平台的包管理器中获得。它以APL中的reshape函数命名。
使用空格和制表符的序列作为列分隔符:
rs -T
使用制表符作为列分隔符:
rs -c -C -T
使用逗号作为列分隔符:
rs -c, -C, -T
-c
改变输入列分隔符,-C
更改输出列分隔符。单独使用 -c
或 -C
将分隔符设置为制表符。 -T
转置行和列。
不要使用 -t
代替 -T
,因为它选择输出列的数量,使得输出行填满显示器的宽度(默认情况下为80个字符,但可以使用 -w
进行更改)。
当使用 -C
指定输出列分隔符时,每行末尾都会添加一个额外的列分隔符字符,但您可以使用 sed
将其删除:
$ seq 4|paste -d, - -|rs -c, -C, -T
1,3,
2,4,
$ seq 4|paste -d, - -|rs -c, -C, -T|sed s/.\$//
1,3
2,4
rs -T
根据第一行的列数确定列数,因此当第一行以一个或多个空列结束时,它会产生错误的结果:
$ rs -c, -C, -T<<<$'1,\n3,4'
1,3,4,
R
t
函数可以转置矩阵或数据框:
Rscript -e 'write.table(t(read.table("stdin",sep=",",quote="",comment.char="")),sep=",",quote=F,col.names=F,row.names=F)'
如果将Rscript -e
替换为R -e
,则会将正在运行的代码回显到STDOUT,并且如果R命令后跟着像head -n1
这样的命令,在读取整个STDIN之前退出,则会导致错误ignoring SIGPIPE signal
。
如果输入不包含双引号或单引号,则可以删除quote=""
,如果输入不包含以井号字符开头的行,则可以删除comment.char=""
。
对于大型输入,data.table::fread
通常比read.table
更快,data.table::fwrite
比write.table
更快:
$ seq 1e6|awk 'ORS=NR%1e3?FS:RS'>a
$ time Rscript --no-init-file -e 'write.table(t(read.table("a")),quote=F,col.names=F,row.names=F)'>/dev/null
real 0m1.061s
user 0m0.983s
sys 0m0.074s
$ time Rscript --no-init-file -e 'write.table(t(data.table::fread("a")),quote=F,col.names=F,row.names=F)'>/dev/null
real 0m0.599s
user 0m0.535s
sys 0m0.048s
$ time Rscript --no-init-file -e 'data.table::fwrite(t(data.table::fread("a")),sep=" ",col.names=F)'>/dev/null
x being coerced from class: matrix to data.table
real 0m0.375s
user 0m0.296s
sys 0m0.073s
jq
tp(){ jq -R .|jq --arg x "${1-$'\t'}" -sr 'map(./$x)|transpose|map(join($x))[]';}
jq -R .
将每个输入行作为 JSON 字符串文字打印出来,-s
(--slurp
)在解析每行 JSON 后为输入行创建一个数组,并且 -r
(--raw-output
)输出字符串内容而不是 JSON 字符串文字。 /
运算符被重载以拆分字符串。
Ruby
ruby -e'STDIN.map{|x|x.chomp.split(",",-1)}.transpose.each{|x|puts x*","}'
< p >
split
的参数
-1
禁用了在末尾丢弃空字段的功能:
$ ruby -e'p"a,,".split(",")'
["a"]
$ ruby -e'p"a,,".split(",",-1)'
["a", "", ""]
函数形式:
$ tp(){ ruby -e's=ARGV[0];STDIN.map{|x|x.chomp.split(s==" "?/ /:s,-1)}.transpose.each{|x|puts x*s}' -- "${1-$'\t'}";}
$ seq 4|paste -d, - -|tp ,
1,3
2,4
上面的函数使用
s==" "?/ /:s
,因为当
split
函数的参数是单个空格时,它会启用类似awk的特殊行为,其中字符串基于连续的空格和制表符进行拆分:
$ ruby -e'p" a \tb ".split(" ",-1)'
["a", "b", ""]
$ ruby -e'p" a \tb ".split(/ /,-1)'
["", "a", "", "\tb", ""]
C++
使用cpp -O3 -o tp tp.cpp
进行编译:
#include<iostream>
#include<vector>
#include<sstream>
using namespace std;
int main(int argc,char**argv){
vector<vector<string> >table;
vector<string>row;
string line,field;
char sep=argc==1?'\t':argv[1][0];
while(getline(cin,line)){
row.clear();
stringstream str(line);
while(getline(str,field,sep))row.push_back(field);
table.push_back(row);
}
int rows=table.size(),cols=table[0].size();
for(int i=0;i<cols;i++){
cout<<table[0][i];
for(int j=1;j<rows;j++)cout<<sep<<table[j][i];
cout<<endl;
}
}
基准测试
csvtk
是最快的,jq
是最慢的:
$ seq 1e6|awk 'ORS=NR%1e3?"\t":"\n"'>test
$ TIMEFORMAT=%R
$ time ./tp<test>/dev/null # first run of C++ program is slower
0.895
$ time ./tp<test>/dev/null
0.520
$ time csvtk -t transpose<test>/dev/null
0.142
$ time rs -c -C -T<test|sed $'s/\t$//'>/dev/null
0.587
$ time gawk -F\\t '{for(i=1;i<=NF;i++)a[i][NR]=$i}END{for(i in a)for(j in a[i])printf"%s"(j==NR?RS:FS),a[i][j]}' test>/dev/null
1.119
$ time awk -F\\t '{for(i=1;i<=NF;i++)a[i,NR]=$i}END{for(i=1;i<=NF;i++)for(j=1;j<=NR;j++)printf"%s"(j==NR?RS:FS),a[i,j]}'<test>/dev/null
1.858
$ time jq -R . test|jq --arg x "${1-$'\t'}" -sr 'map(./$x)|transpose|map(join($x))[]'>/dev/null
3.604
$ time ruby -e'STDIN.map{|x|x.chomp.split("\t",-1)}.transpose.each{|x|puts x*"\t"}'<test>/dev/null
0.492