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

时间:2009-11-13 15:13:47

标签: bash parsing unix transpose

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

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

但它很慢,似乎不是最有效的解决方案。我在this post看到了vi的解决方案,但它仍然过慢。有什么想法/建议/精彩的想法吗? : - )

29 个答案:

答案 0 :(得分:104)

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

它几乎不使用任何内存,但是在一行上每个字段读取一次输入文件,因此它比将整个文件读入内存的版本要慢得多。它还假设每行的字段数相同,它使用ENDFILEARGIND的GNU awk,但任何awk都可以对FNR==1和{{1}上的测试执行相同操作}。

答案 1 :(得分:41)

另一种选择是使用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字符串文字。 /运算符被重载以分割字符串。

答案 2 :(得分:30)

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

此代码假定每行具有相同的列数(不执行填充)。

答案 3 :(得分:20)

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

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

答案 4 :(得分:15)

Pure 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

答案 5 :(得分:13)

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

答案 6 :(得分:9)

这是一个适度的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。两者都不是问题。


在包含10,000行的文件上,MacOS X 10.5.8上的Perl 5.10.1(32位)与awk(版本20040207,给定'-V')和gawk 3.1.7(32位)的代表性时序每行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慢。显然,你的里程会有所不同。

答案 7 :(得分:6)

有一个专用的实用工具,

GNU datamash utility

apt install datamash  

datamash transpose < yourfile

取自本网站https://www.gnu.org/software/datamash/http://www.thelinuxrain.com/articles/transposing-rows-and-columns-3-methods

答案 8 :(得分:6)

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

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

答案 9 :(得分:5)

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

{for (f=1;f<=NF;f++) col[f] = col[f]":"$f} END {for (f=1;f<=NF;f++) print col[f]}

换句话说,当您遍历行时,对于每个字段f,会生成一个包含该字段元素的&#39; - 分隔字符串col[f]。完成所有行后,在单独的行中打印这些字符串中的每一个。然后,您可以替换&#39;:&#39;通过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

答案 10 :(得分:4)

GNU datamash非常适合这个问题,只有一行代码和可能任意大的文件大小!

datamash -W transpose infile > outfile

答案 11 :(得分:3)

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

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

答案 12 :(得分:3)

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

答案 13 :(得分: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

答案 14 :(得分:2)

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

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

答案 15 :(得分: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

答案 16 :(得分:2)

不是很优雅,但这个&#34;单行&#34;命令可以快速解决问题:

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。

答案 17 :(得分:2)

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

答案 18 :(得分: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

答案 19 :(得分: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

答案 20 :(得分:1)

 @model ChartInMvcApplication.Models.ProductModel
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">

    google.load("visualization", "1", { packages: ["corechart"] });
    google.setOnLoadCallback(drawChart);

    function drawChart() {
        // Create and populate the data table.
        var years = [@Model.ProductData.Year];
        var sales = [@Model.ProductData.Sale];
        var Purchase = [@Model.ProductData.Purchase];

        var data = new google.visualization.DataTable();
        data.addColumn('string', '@Model.YearTitle');
        data.addColumn('number', '@Model.SaleTitle');
        data.addColumn('number', '@Model.PurchaseTitle');
        for (i = 0; i < years.length; i++) {
            data.addRow([years[i].toString(), sales[i], Purchase[i]]);
        }
        var options = {
            title: 'Sale and Purchase Compare',
            hAxis: { title: '@Model.YearTitle', titleTextStyle: { color: 'red'} }
        };

        var chart = newgoogle.visualization.ColumnChart(document.getElementById('chartdiv'));
        chart.draw(data, options);
    }
</script>
<div id="chartdiv" style="width: 500px; height: 300px;">
</div>

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

的另一个版本

答案 21 :(得分:1)

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

答案 22 :(得分:1)

某些 * nix 标准util one-liners,不需要临时文件。注意:OP需要高效修复(即更快),而最高答案通常比此答案更快。 这些单行适用于那些喜欢 * nix software tools的用户,无论出于何种原因。在极少数情况下,(例如稀缺的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. tr&amp; echo

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

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

答案 23 :(得分:0)

我以前在下面的两个脚本中使用过类似的操作。第一个在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

答案 24 :(得分:0)

简单的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

答案 25 :(得分:0)

使用R ...的Oneliner ...

  cat file | Rscript -e "d <- read.table(file('stdin'), sep=' ', row.names=1, header=T); write.table(t(d), file=stdout(), quote=F, col.names=NA) "

答案 26 :(得分:0)

将整个数组存储在内存中的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

但我们可能会步行&#34;该文件需要输出行的次数:

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

其中(对于较低的输出行数比前一代码快)。

答案 27 :(得分:0)

这是一个Bash单线程,它基于简单地将每一行转换为一列并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. 将其转换为一列
  4. 将新列粘贴到tmp1文件

  5. 将结果复制回tmp1

  6. PS:我真的很想使用io-descriptors,但却无法让它们发挥作用。

答案 28 :(得分:0)

这是一个Haskell解决方案。当使用-O2进行编译时,它运行速度比ghostdog的awk快一点,并且比我在机器上的Stephan的薄包裹的c python稍微慢一些,用于重复&#34; Hello world&#34 ;输入线。不幸的是,据我所知,GHC对传递命令行代码的支持是不存在的,所以你必须自己将它写入文件。它会将行截断为最短行的长度。

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

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