基于 awk 的解决方案,用于对多个文件的行进行求和

基于 awk 的解决方案,用于对多个文件的行进行求和

我有几个看起来像的文件

文件1.dat:

1 1
1 3 4
5 9 10 11

文件2.dat:

3 0
8 9 0
3 9 2 4

通常有更多的行(每行比前一行少包含一列)。我设计了一个混合 bash/awk 脚本来对每个文件的行进行求和,例如使用上面的示例:

输出数据:

4 1
9 12 4
8 18 12 15

该脚本按预期工作,但速度相当慢。在我的机器上处理 100 个文件,每个文件有 10000 行,需要 30 多分钟。该脚本似乎花费了大部分时间从所有文件中收集第 n 行。有没有一种方法可以通过传递file*.dat给 awk 命令来执行我的操作(见下文)?

#!/bin/bash
ROWS=$1; shift
OUT_FILE=$1; shift
IN_FILE=("$@")

for i in `seq 1 1 ${ROWS}`; do
    # Get ith row from all input files
    for j in "${IN_FILE[@]}"; do
        tail -n+${i} ${j} | head -1 >> "temp.dat"
    done
    # Sum the rows 
    awk '{for (j=1;j<=NF;j++) a[j]+=$j} END {for (j in a) printf a[j] " "}' temp.dat >> ${OUT_FILE}
    echo >> ${OUT_FILE}
    rm temp.dat
done

基于上面例子的脚本用法:./RowSums.sh 3 out.dat file*.dat

答案1

使用任何paste和任何awk

$ cat tst.sh
#!/usr/bin/env bash

paste "${@}" |
awk -v numFiles="$#" '{
    numFldsPerFile = NF / numFiles
    for ( outFldNr=1; outFldNr<=numFldsPerFile; outFldNr++ ) {
        sum = 0
        for ( fileNr=1; fileNr<=numFiles; fileNr++ ) {
            inFldNr = outFldNr + (fileNr - 1) * numFldsPerFile
            sum += $inFldNr
        }
        printf "%d%s", sum, (outFldNr<numFldsPerFile ? OFS : ORS)
    }
}'

$ ./tst.sh file1.dat file2.dat
4 1
9 12 4
8 18 12 15

希望描述性变量名称和显式inFldNr计算能够清楚地表明它在做什么。

答案2

下面的 awk 脚本几乎可以替换整个 shell 脚本:

# cat rowsum.awk
FNR <= rows {
    for (i = 1; i <= NF; i++)
        sum[FNR,i] += $i
}
END {
    for (i = 1; i <= rows; i++) {
        for (j = 1; j <= rows + 1; j++) {
            printf "%s ", sum[i, j]
        }
        printf "\n";
    }
}

例子:

% awk -f rowsum.awk -v rows=2 file*.dat
4 1
9 12 4
% awk -f rowsum.awk -v rows=3 file*.dat
4 1
9 12 4
8 18 12 15

这应该比为每一行一次又一次地检查所有文件要快。

注意:我假设n第 行有n+1列。如果没有,请保存每行的列数(例如cols[FNR]=NF)并在最终循环中使用它。


另一个更节省内存的选项可以是paste从每个文件中获取所有相关行:

% paste -d '\n' file*.dat                                                                                                                                                
1 1
3 0
1 3 4
8 9 0
5 9 10 11
3 9 2 4

然后awk对它们使用:

# cat rowsum-paste.awk
NR > 1 && NF != prevNF {
    for (i = 1; i <= prevNF; i++) {
        printf "%s ", sum[i];
        sum[i] = 0
    };
    printf "\n"
}
{
    for (i = 1; i <= NF; i++)
        sum[i] += $i;
    prevNF = NF
}
% (paste -d '\n' file*.dat; echo) | awk -f rowsum-paste.awk
4 1 
9 12 4 
8 18 12 15 

此 awk 代码对行求和,直到字段数发生变化,然后打印并重置当前总和。额外的echo是更改末尾的字段数量并触发最终打印,这也可以通过在END块中复制打印代码来完成。

答案3

用于awk为所有文件输出制表符分隔的数据集,其中包含前两个字段中的行索引和列索引,以及该位置的数据值作为第三个字段:

awk -v OFS='\t' '{ for (i = 1; i <= NF; ++i) print FNR, i, $i }' file*.dat

对数据进行排序并使用 GNU对上面生成的数据datamash执行操作,对同一(行、列)索引处出现的元素求和(该选项不会输出任何内容来代替丢失的字段):crosstab--filler ''datamashN/A

sort -n | datamash --filler '' crosstab 1,2 sum 3

修剪掉每列上添加的标题以及带有datamash输出行号的初始列:

tail -n +2 | cut -f 2-

考虑到问题中的两个文件,所有这些都与输出一起:

$ awk -v OFS='\t' '{ for (i = 1; i <= NF; ++i) print FNR, i, $i }' file*.dat | sort -n | datamash --filler '' crosstab 1,2 sum 3 | tail -n +2 | cut -f 2-
4       1
9       12      4
8       18      12      15

对此进行基准测试并将其与muru 的解决方案,在两个小数据文件上,速度并没有慢四倍(3.7)。

相关内容