减去相应的行

Subtract corresponding lines

我有两个文件,file1.csv

3 1009
7 1012
2 1013
8 1014

和file2.csv

5 1009
3 1010
1 1013

在shell中,我想根据第二列中的标识符从第一个文件中的第一列中减去第二个文件中第一列中的计数。如果第二列中缺少标识符,则假定计数为 0。

结果会是

-2 1009
-3 1010 
7 1012
1 1013
8 1014

文件很大(几 GB)。第二列已排序。

我如何在 shell 中有效地做到这一点?

假设这是一个用空格分隔的 csv,如果这是一个“,”使用参数 -F ','

awk 'FNR==NR {Inits[]=; ids[]++; next}
             {Discounts[]=; ids[]++}
     END     { for (id in ids) print Inits[ id] - Discounts[ id] " " id}
    ' file1.csv file2.csv

内存问题(可能在 1 系列管道中,但更喜欢使用临时文件)

awk 'FNR==NR{print;next}{print -1 *  " " }' file1 file2 \
 | sort -k2 \
 > file.tmp
awk 'Last !=  { 
        if (NR != 1) print Result " "Last
        Last = ; Result = 
        }
    Last ==  { Result+= ; next}
    END { print Result " " }
    ' file.tmp
rm file.tmp
$ awk 'NR==FNR { a[]=; next }
               { a[]-= }
           END { for(i in a) print a[i],i }' file1 file2
7 1012
1 1013
8 1014
-2 1009
-3 1010

它读取内存中的第一个文件,因此您应该有足够的可用内存。如果你没有内存,我可能会先 sort -k2 文件,然后 sort -m (合并)它们并继续输出:

$ sort -m -k2 -k3 <(sed 's/$/ 1/' file1|sort -k2) <(sed 's/$/ 2/' file2|sort -k2) # | awk ...
3 1009 1
5 1009 2  # previous  = current  -> subtract
3 1010 2  # previous  =/= current and current =2 print -
7 1012 1
2 1013 1  # previous  =/= current and current =1 print prev 
1 1013 2
8 1014 1

(暂时没时间了,以后可能会补完)

埃德·莫顿编辑 希望您不介意我添加我正在做的事情而不是发布我自己的极其相似的答案,请随时修改或删除它:

$ cat tst.awk
{ split(prev,p) }
 == p[2] {
    print p[1] - , p[2]
    prev = ""
    next
}
p[2] != "" {
    print (p[3] == 1 ? p[1] : 0-p[1]), p[2]
}
{ prev = [=12=] }
END {
    split(prev,p)
    print (p[3] == 1 ? p[1] : 0-p[1]), p[2]
}

$ sort -m -k2 <(sed 's/$/ 1/' file1) <(sed 's/$/ 2/' file2) | awk -f tst.awk
-2 1009
-3 1010
7 1012
1 1013
8 1014

假设两个文件都按第二列排序:

$ join -j2 -a1 -a2 -oauto -e0 file1 file2 | awk '{print  - , }'
-2 1009
-3 1010
7 1012
1 1013
8 1014

join 将加入已排序的文件。
-j2 将加入第二个列。
-a1 将打印 file1 的记录,即使 file2 中没有相应的行。
-a2-a1 相同,但申请了文件 2。
-oauto 在这种情况下与 -o1.2,1.1,2.1 相同,它将打印连接的列,然后是 file1 和 file2 的剩余列。
-e0 将插入 0 而不是空列。这适用于 -a1-a2.

join 的输出是三列,如:

1009 3 5
1010 0 3
1012 7 0
1013 2 1
1014 8 0

通过管道传输到 awk,从第 2 列减去第 3 列,然后重新格式化。

由于文件已排序¹,您可以使用 coreutils 中的 join 实用程序逐行合并它们:

$ join -j2 -o auto -e 0 -a 1 -a 2 41144043-a 41144043-b
1009 3 5
1010 0 3
1012 7 0
1013 2 1
1014 8 0

所有这些选项都是必需的:

  • -j2表示根据每个文件的第二列加入
  • -o auto 表示让每一行都具有相同的格式,从连接键开始
  • -e 0 表示缺失值应替换为零
  • -a 1-a 2 包括一个文件或另一个文件中不存在的行
  • 文件名(我在这里使用了基于问题编号的名称)

现在我们有了那种格式的输出流,我们可以在每一行上做减法。我使用这个 GNU sed 命令将上面的输出转换为 dc 程序:

sed -re 's/.*/c&-n[ ]np/e'

这将获取每行的三个值并将它们重新排列为一个 dc 命令以进行减法,然后执行它。例如,第一行变为(为清楚起见添加了 spaces)

c 1009 3 5 -n [ ]n p

从 3 中减去 5,打印它,然后打印 space,然后打印 1009 和一个换行符,给出

-2 1009

根据需要。

然后我们可以将所有这些行通过管道传输到 dc,从而得到我们想要的输出文件:

$ join -o auto -j2 -e 0 -a 1 -a 2 41144043-a 41144043-b \
>   | sed -e 's/.*/c& -n[ ]np/' \
>   | dc
-2 1009
-3 1010
7 1012
1 1013
8 1014

¹ 排序需要与 LC_COLLATE 语言环境设置一致。如果字段始终是数字,那不太可能成为问题。


TL;DR

完整的命令是:

join -o auto -j2 -e 0 -a 1 -a 2 "$file1" "$file2" | sed -e 's/.*/c& -n[ ]np/' | dc

它一次运行一行,并且只启动您看到的三个进程,因此在内存和 CPU 方面应该相当高效。