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

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

行Perl脚本来执行此操作,但是执行起来应该比本地bash函数要慢)。所以输出看起来应该像

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


我想到了这样的解决方案
似乎不是最有效的解决方案。我在这篇文章中已经看到了vi的解决方案,但是它仍然过慢。有什么想法/建议/好主意吗? :-)

评论

是什么让您认为存在一个bash脚本要比Perl脚本快?这正是Perl解决的问题。

@mark,如果纯粹是bash,它可能比将所有cut / sed等工具链接在一起的速度更快。但是话又说回来,如果您在组合工具中定义“ bash”,那么仅编写awk脚本就可以与Perl wrt文本处理媲美。
添加另一个原因是不了解Perl在这里的运行速度。编写代码慢吗?执行慢吗?我确实不喜欢perl,但是在这种任务上确实很出色。

如果您的列/字段具有固定的大小/宽度,则可以使用Python文件搜寻来避免将文件读入内存。您有固定的列/字段大小/宽度吗?

任何认为shell脚本比awk或perl都要快的人都需要阅读unix.stackexchange.com/questions/169716/…,这样他们才能理解为什么不是这种情况。

#1 楼

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)。为此,可以这样做:而不是将整个文件读入内存的版本。它还假定每行的字段数相同,并且对ENDFILEARGIND使用GNU awk,但是任何awk都可以对FNR==1END进行测试。

评论


现在也可以处理行和列标签吗?

–乔纳森·莱弗勒(Jonathan Leffler)
09年11月13日在15:54

好-您是正确的;您的示例数据与问题的示例数据不匹配,但是您的代码在问题的示例数据上运行良好,并提供了所需的输出(给出或取空白与制表符间距)。主要是我的错误。

–乔纳森·莱弗勒(Jonathan Leffler)
09年11月13日在17:20

有趣的时机-我同意您会在awk中看到性能提升。我使用的是MacOS X 10.5.8,不使用“ gawk”;我使用的是Perl 5.10.1(32位版本)。我收集到您的数据是10000行,每行4列?无论如何,这并不重要。 awk和perl都是可行的解决方案(而awk解决方案则更整洁-我的Perl中的“定义”检查对于严格/警告下的免费运行是必需的),而且都不是懈怠,而且两者都可能比原始方法快外壳脚本解决方案。

–乔纳森·莱弗勒(Jonathan Leffler)
09年11月16日在9:43

在我最初的2.2GB矩阵上,perl解决方案比awk略快-350.103s与我使用perl 5.8.8 64bit的369.410s

– Federico Giorgi
09年11月16日在10:18

@ zx8754最大字段数仅适用于旧的非POSIX awk。可能不幸的是被命名为“ nawk”。它不适用于gawk或其他现代awks。

–埃德·莫顿(Ed Morton)
16年4月10日在14:50

#2 楼

rs
rs带有BSD和macOS,但可以从其他平台上的程序包管理器中获得。它以APL中的“重塑”功能命名。
使用空格和制表符序列作为列分隔符:
 rs -T
 

使用制表符作为列分隔符:
 rs -c -C -T
 

将逗号用作列分隔符:
 rs -c, -C, -T
 

-c更改输入列分隔符,而-C更改输出列分隔符。仅-c-C会将分隔符设置为制表符。 -T转置行和列。
不要使用-t而不是-T,因为它使用自动选择的列数通常是不正确的,因为选择了列数以使输出行填充显示的宽度(默认情况下为80个字符,但可以使用-w进行更改。)一个警告是,当使用-C指定输出列分隔符时,会在每行的末尾添加一个额外的列分隔符,但是可以使用诸如sed 's/.$//'之类的字符删除多余的字符:最后一个或多个空列,因为列数是根据第一行的列数确定的:
 $ 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
 

Ruby
 $ rs -C, -c, -T<<<$'1,\n3,4'
1,3,4,
 

$ ruby -e'puts readlines.map{|x|x.chomp.split(",",-1)}.transpose.map{|x|x*","}'<<<$'1,\n3,4' 1,3 ,4 -1参数不会在末尾丢弃空字段:
 split 

函数形式:
 $ ruby -e'p"a,,".split(",")'
["a"]
$ ruby -e'p"a,,".split(",",-1)'
["a", "", ""]
 

jq
 $ tp(){ ruby -e'puts STDIN.read.split("\n").map{|x|x.split(ARGV[0],-1)}.transpose.map{|x|x*ARGV[0]}' -- "${1-$'\t'}";}
$ seq 4|paste - -|tp|sed -n l
1\t3$
2\t4$
  
jq -R .|jq -sr 'map(./"\t")|transpose|map(join("\t"))[]' 将每条输入行打印为JSON字符串文字,jq -R .-s)在将每一行解析为JSON之后为输入行创建一个数组,而--slurp-r)输出字符串的内容而不是JSON字符串文字。 --raw-output运算符已重载以分割字符串。
函数形式:
 / 


评论


我对rs不熟悉-感谢您的指导! (链接指向Debian;上游似乎是mirbsd.org/MirOS/dist/mir/rs)

–tripleee
2015年11月26日13:00



@lalebarde至少在OS X附带的rs的实现中,-c单独将输入列分隔符设置为选项卡。

– nisetama
16 Mar 5 '16 at 12:20

@lalebarde,尝试使用bash的ANSI-C引号获取制表符:$'\ t'

–格伦·杰克曼
16年4月10日在11:51

这是一个极端的情况,但是对于具有很多行(如TTC TTA TTC TTC TTC TTT)的非常大的文件,运行rs -c''-C''-T cols.seq会得到rs:没有内存:无法分配记忆。这是一个运行FreeBSD 11.0-RELEASE且系统内存为32 GB的系统。因此,我的猜测是rs将所有内容都放入RAM,这对于提高速度很有好处,但对大数据却不利。

– jrm
17年7月6日在9:35



jq在766MB的文件上使用了21Gb的ram。 40分钟后我将其杀死,但没有任何输出。

–Glubbdrubb
18年3月20日在9:50

#3 楼

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


以上内容基于以下内容:每行具有相同的列数(不执行填充)。

评论


这里有一个小问题:将l.split()替换为l.strip()。split()(Python 2.7),否则输出的最后一行会残缺。适用于任意列分隔符,如果分隔符存储在变量sep中,则使用l.strip()。split(sep)和sep.join(c)。

– krlmlr
2012年10月2日,下午4:18

#4 楼

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

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


评论


感谢您的链接。但是,在处理大型矩阵/文件时,它需要太多内存。

–tommy.carstensen
13年4月8日在9:41

它具有用于块大小和字段大小的参数:尝试调整-b和-f参数。

–飞羊
13年4月8日在14:54

默认块大小(--block或-b)为10kb,默认字段大小(--fieldmax或-f)为64,所以不能这样。我试过了。 (还是)感谢你的建议。

–tommy.carstensen
13年4月10日在16:27

与大小为2 GB的csv一起工作良好。

–纪律
16年11月8日,3:10

对于尺寸大约为11k x 5k的矩阵文件,我发现transpose.c的速度比ghostdog74的第一个awk解决方案快约7倍,内存效率高约5倍。另外,我发现ghostdog74的“几乎不使用内存” awk代码无法正常工作。另外,请注意transpose.c程序中的--limit标志,默认情况下会将输出限制为1k x 1k。

–ncemami
16-11-28在6:40



#5 楼

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

#6 楼

纯BASH,无需其他过程。一个不错的练习:

declare -a array=( )                      # we build a 1-D-array

read -a line < ""                       # 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 < ""

for (( ROW = 0; ROW < COLS; ROW++ )); do
  for (( COUNTER = ROW; COUNTER < ${#array[@]}; COUNTER += COLS )); do
    printf "%s\t" ${array[$COUNTER]}
  done
  printf "\n" 
done


评论


这对我的文件有用,尽管有趣的是它为表的第一行打印了一个目录列表。我不知道足够的BASH找出原因。

–面包
2013年6月19日19:50

@bugloaf,您的桌子的角落有一个*。

– Hello71
2014年8月27日23:26



@bugloaf:正确地引用变量应该可以防止这种情况:printf“%s \ t”“ $ {array [$ COUNTER]}”

–丹尼斯·威廉姆森
2014年11月26日在16:12

#7 楼

这是完成此工作的中等可靠的Perl脚本。 @ ghostdog74的awk解决方案有许多结构类比。 )。对于较大的数据集(100x100矩阵,每个条目6-8个字符),perl的效果稍差于awk-0.026s和0.042s。两者都可能不会造成问题。 MacOS X 10.5.8上的32位),文件包含10,000行,每行5列: ,但仍比perl慢。显然,您的里程会有所不同。

评论


在我的系统上,gawk优于perl。您可以在编辑后的帖子中看到我的结果

–ghostdog74
09年11月16日在9:34

得出的结论是:不同的平台,不同的软件版本,不同的结果。

–ghostdog74
09年11月16日在16:11

#8 楼

GNU datamash仅需一行代码并可能具有任意大文件大小,因此非常适合此问题!


#9 楼

为此有一个专门构建的实用程序,

GNU datamash实用程序

apt install datamash  

datamash transpose < yourfile


来自此站点https://www.gnu。 org / software / datamash /和http://www.thelinuxrain.com/articles/transposed-rows-and-columns-3-methods

#10 楼

如果安装了sc,则可以执行以下操作:

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


评论


请注意,这支持有限的行数,因为sc将其列命名为一个字符或两个字符的组合。限制为26 + 26 ^ 2 = 702。

–雷神
2012年11月8日上午10:38

#11 楼

假设您所有的行都具有相同数量的字段,那么这个awk程序可以解决以下问题:

一个':'分隔的字符串f,其中包含该字段的元素。完成所有行之后,将这些字符串中的每个字符串打印在单独的一行中。然后,通过将输出通过col[f]传递给管道,可以用':'代替所需的分隔符(例如,空格)。

示例:

#12 楼

骇人的perl解决方案可以是这样的。很好,因为它不会加载内存中的所有文件,不会打印中间的临时文件,然后使用所有精彩的粘贴信息。

评论


使用粘贴和临时文件只是多余的操作。您可以在内存本身内部进行操作,例如数组/哈希

–ghostdog74
09年11月13日在17:11

是的,但这不是意味着将所有内容都保留在内存中吗?我正在处理的文件大小约为2-20GB。

– Federico Giorgi
09年11月16日在11:49

#13 楼

对于您自己的示例,我可以看到的唯一改进是使用awk,它将减少运行的进程数以及在它们之间传递的数据量:


#14 楼

我通常使用这个小的awk代码段来满足此要求:转置给定的输入。

这需要跟踪初始文件的最大列数,以便将其用作要打印回的行数。

#15 楼

一些* nix标准的util一线式,不需要临时文件。注意:OP需要一个有效的解决方案(即更快),并且最常见的答案通常比该答案更快。这些单行代码是出于各种原因而喜欢* nix软件工具的用户的。在极少数情况下(例如,IO和内存不足),这些摘要实际上可能比某些顶级答案要快。

将输入文件命名为foo。 >
如果我们知道foo有四列:

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



如果我们不知道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



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



使用set(与xargs类似)具有类似的基于命令行大小的限制:

评论


所有这些都将比awk或perl解决方案慢几个数量级并且脆弱。阅读unix.stackexchange.com/questions/169716/…。

–埃德·莫顿(Ed Morton)
16-4-10的15:25

@EdMorton,谢谢,成功回答了我对您的速度问题的回答。关于“脆弱”:不是3),并且当程序员知道数据对于给定技术是安全的时,也不是其他。难道POSIX兼容的shell代码不是比perl更稳定的标准吗?

–agc
16年4月10日在18:17

抱歉,我对perl非常了解。在这种情况下,使用的工具将是awk。 cut,head,echo等与awk脚本相比,与POSIX兼容的外壳代码没有更多,它们都是UNIX安装中的标准配置。完全没有理由使用一组工具,这些工具组合在一起时,您只需要使用awk时,就需要小心输入文件的内容和执行脚本的目录,并且最终结果更快且更可靠。

–埃德·莫顿(Ed Morton)
16-4-10在19:12



拜托,我不是抗辩,但条件各不相同。原因1:对于在切割头xargs seq awk中的f;做wc -c $(哪个$ f);完成当存储速度太慢或IO太低时,更大的解释器会使情况变得更糟,无论在更理想的情况下它们的性能如何。原因2:awk(或大多数语言)也比设计成一件事的小型utils承受着更陡峭的学习曲线。如果运行时间比编码器工时便宜,那么使用“软件工具”进行简单编码就可以节省成本。

–agc
16-4-10在20:30



#16 楼

我使用了fgm的解决方案(感谢fgm!),但是需要消除每行末尾的制表符,因此对脚本进行了如下修改:

#17 楼

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

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


#18 楼

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


#19 楼

如果您只想从文件中抓取一个单行(以逗号分隔)$ N并将其变成一列:

#20 楼

不是很优雅,但是这个“单行”命令可以快速解决问题:

#21 楼

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


#22 楼

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

评论


阅读unix.stackexchange.com/questions/169716/…,以了解该解决方案的部分但不是全部问题。

–埃德·莫顿(Ed Morton)
16年4月10日在15:43

#23 楼

另一个bash变体

$ cat file 
XXXX    col1    col2    col3
row1    0       1       2
row2    3       4       5
row3    6       7       8
row4    9       10      11


脚本

#!/bin/bash

I=0
while read line; do
    i=0
    for item in $line; { printf -v A$I[$i] $item; ((i++)); }
    ((I++))
done < file
indexes=$(seq 0 $i)

for i in $indexes; {
    J=0
    while ((J<I)); do
        arr="A$J[$i]"
        printf "${!arr}\t"
        ((J++))
    done
    echo
}


输出

$ ./test 
XXXX    row1    row2    row3    row4    
col1    0       3       6       9   
col2    1       4       7       10  
col3    2       5       8       11


#24 楼

这是Haskell解决方案。用-O2编译时,对于重复的“ Hello world”输入行,它的运行速度比ghostdog的awk略快,并且比Stephan的薄包装c python略慢。不幸的是,据我所知,GHC不支持传递命令行代码,因此您必须自己将其写入文件。它将截断行到最短行的长度。

transpose :: [[a]] -> [[a]]
transpose = foldr (zipWith (:)) (repeat [])

main :: IO ()
main = interact $ unlines . map unwords . transpose . map words . lines


#25 楼

一种将整个数组存储在内存中的awk解决方案

    awk '
#!/bin/bash
maxf="$(awk '{if (mf<NF); mf=NF}; END{print mf}' infile)"
rowcount=maxf
for (( i=1; i<=rowcount; i++ )); do
    awk -v i="$i" -F " " '{printf("%s\t ", $i)}' infile
    echo
done
!~/^$/{ i++; split(q4312078q,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


但是我们可以根据需要的输出行“遍历”文件多次: /> q4312078q

(对于较少的输出行,它比以前的代码要快)。

#26 楼

这是一个Bash单行代码,其基础是将每行简单地转换为一列,然后对其进行paste -inging:

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



创建tmp1文件,因此它不为空。
读取每一行,并使用tr将其转换为列。 tmp1文件
将结果复制回tmp1中。

PS:我真的很想使用io描述符,但无法使它们工作。

评论


如果要在大文件上执行闹钟,请确保设置闹钟。阅读unix.stackexchange.com/questions/169716/…以了解该方法的部分但不是全部问题。

–埃德·莫顿(Ed Morton)
16年4月10日在15:46

#27 楼

使用R ...的内衬...

#28 楼

我以前在下面两个脚本中使用过类似的操作。第一个在awk中,比第二个在“纯” bash中快得多。您可能可以使其适应您自己的应用程序。

awk '
{
    for (i = 1; i <= NF; i++) {
        s[i] = s[i]?s[i] FS $i:$i
    }
}
END {
    for (i in s) {
        print s[i]
    }
}' file.txt

declare -a arr

while IFS= read -r line
do
    i=0
    for word in $line
    do
        [[ ${arr[$i]} ]] && arr[$i]="${arr[$i]} $word" || arr[$i]=$word
        ((i++))
    done
done < file.txt

for ((i=0; i < ${#arr[@]}; i++))
do
    echo ${arr[i]}
done


#29 楼

简单的4行答案,保持可读性。
col="$(head -1 file.txt | wc -w)"
for i in $(seq 1 $col); do
    awk '{ print $'$i' }' file.txt | paste -s -d "\t"
done