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

139
我有一个格式如下的巨大的制表符分隔文件:
X column1 column2 column3
row1 0 1 2
row2 3 4 5
row3 6 7 8
row4 9 10 11

我希望您能用bash命令高效地进行转换(我可以编写大约10行的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编辑器的解决方案,但仍然很慢。有什么想法/建议/创意吗? :-)


13
你为什么认为会有一个比Perl脚本更快的bash脚本存在呢?这正是Perl擅长解决的问题类型。 - Mark Pim
2
@mark,如果这是纯Bash,那么它可能比将所有的cut/sed等工具链接在一起更快。但是,如果你将“Bash”定义为组合工具,则编写一个awk脚本与Perl相比在文本处理方面是可比的。 - ghostdog74
再加一个不理解为什么Perl在这里会很慢的人。是写代码慢吗?还是执行慢?我真的不喜欢Perl,但它确实擅长处理这种任务。 - Corey Porter
如果您的列/字段具有固定的大小/宽度,则可以使用Python文件查找来避免将文件读入内存。您的列/字段是否具有固定的大小/宽度? - tommy.carstensen
3
任何认为Shell脚本比awk或perl更快的人需要阅读http://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice,以便了解为什么情况并非如此。请注意,Shell循环处理文本被视为不良实践。 - Ed Morton
33个回答

2

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

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。


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

1

这是一个非常简单的 Ruby 代码:

ruby -lane 'BEGIN{lines=[]} 
    lines<<$F 
END{lines.transpose.each{|row| puts row.join("\t")}}' file

或者您可以自己控制分割而不是依赖于-lan命令行开关:

ruby -e '$<.read.
            split(/\R+/).
            map(&:split).
            transpose.each{|row| puts row.join("\t")}' file

或者您可以使用 Ruby 的 CSV 模块:

ruby -r csv -e '
tbl=CSV.parse($<.read, **{:headers=>false, :col_sep=>" "}) # or use :col_sep=>"\t" for tab columns 
tbl.transpose.each{|row| puts row.join("\t")}
' file 

任何打印:

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

1
这里有一个基于将每行转换为列并使用“paste”命令拼接的Bash一行命令:
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

注:我真的想使用io-descriptors,但无法让它们正常工作。


如果你要在大文件上执行这个操作,请确保设置一个闹钟。阅读http://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice 了解一些问题,但并非全部。 - Ed Morton
这是一行代码吗? - johny why

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

另一个版本,使用seteval

阅读http://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice以了解该解决方案存在的一些问题,但并非全部。 - Ed Morton

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

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

但是我们可以“遍历”文件,直到需要输出行数为止:

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

对于输出行数较少的情况,这比以前的代码更快。


0
for i in $(seq $(head -n1 file.txt | tr ' ' '\n' | wc -l))
do
  cut -d' ' -f"$i" file.txt | paste -s -d' ' -
done

或者

seq $(head -n1 file.txt | tr " " "\n" | wc -l) | xargs -I{} sh -c 'cut -d" " -f"{}" file.txt | paste -s -d" " -'

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

0
这是一个 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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接