我有几个看起来像的文件
文件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 ''
datamash
N/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)。