关于bash:在一个文件中找到不在另一个文件中的行的快速方法?

Fast way of finding lines in one file that are not in another?

我有两个大文件(文件名集)。 每个文件大约有30,000行。 我试图找到一种快速查找file1中不存在于file2中的行的方法。

例如,如果这是file1:

1
2
3
line1
line2
line3

这是file2:

1
2
3
line1
line4
line5

然后我的结果/输出应该是:

1
2
line2
line3

这有效:

grep -v -f file2 file1

但是在我的大文件上使用它时非常非常慢。

我怀疑有一个很好的方法来使用diff(),但输出应该只是行,没有别的,我似乎无法找到一个开关。

任何人都可以帮我找到一个快速的方法,使用bash和基本的Linux二进制文件?

编辑:为了跟进我自己的问题,这是我到目前为止使用diff()找到的最好方法:

1
diff file2 file1 | grep '^>' | sed 's/^>\ //'

当然,必须有更好的方法吗?


comm命令("common"的缩写)可能很有用comm - compare two sorted files line by line

1
2
3
4
5
6
7
8
#find lines only in file1
comm -23 file1 file2

#find lines only in file2
comm -13 file1 file2

#find lines common to both files
comm -12 file1 file2

man文件实际上非常易读。


您可以通过控制GNU diff输出中旧/新/未更改行的格式来实现此目的:

1
diff --new-line-format="" --unchanged-line-format=""  file1 file2

应对输入文件进行排序,以使其正常工作。使用bash(和zsh),您可以使用进程替换<( )进行就地排序:

1
diff --new-line-format="" --unchanged-line-format="" <(sort file1) <(sort file2)

在上面新的和未改变的行被抑制,因此只输出(即你的情况下删除的行)。您也可以使用其他解决方案未提供的一些diff选项,例如-i忽略大小写,或者使用各种空格选项(-E-b-v等)进行不太严格的匹配。

说明

使用选项--new-line-format--old-line-format--unchanged-line-format可以控制diff格式化差异的方式,类似于printf格式说明符。这些选项分别格式化新(添加),旧(删除)和未更改的行。将一个设置为空"阻止输出那种线。

如果您熟悉统一差异格式,则可以使用以下部分重新创建它:

1
2
diff --old-line-format="-%L" --unchanged-line-format=" %L" \
     --new-line-format="+%L" file1 file2

%L说明符是有问题的行,我们在前面加上"+"" -"或"",如diff -u
(请注意,它只输出差异,它缺少每个分组更改顶部的--- +++@@行。
您也可以使用它来执行其他有用的操作,例如使用%dn为每一行编号。

diff方法(以及其他建议commjoin)仅生成带有排序输入的预期输出,但您可以使用<(sort ...)进行排序。这是一个简单的awk(nawk)脚本(受Konsolebox答案中链接的脚本的启发),它接受任意排序的输入文件,并按照它们在file1中出现的顺序输出缺失的行。

1
2
3
4
5
6
7
# output lines in file1 that are not in file2
BEGIN { FS="" }                         # preserve whitespace
(NR==FNR) { ll1[FNR]=$0; nl1=FNR; }     # file1, index by lineno
(NR!=FNR) { ss2[$0]++; }                # file2, index by string
END {
    for (ll=1; ll<=nl1; ll++) if (!(ll1[ll] in ss2)) print ll1[ll]
}

这将在行号索引数组ll1[]中逐行存储file1的全部内容,并在行内容索引关联数组ss2[]中逐行存储file2的全部内容。读取两个文件后,迭代ll1并使用in运算符确定file1中的行是否存在于file2中。 (如果有重复,这将与diff方法有不同的输出。)

如果文件足够大以至于存储它们都会导致内存问题,则可以通过仅存储file1并在读取file2时删除匹配来交换CPU以获取内存。

1
2
3
4
5
6
7
8
9
10
BEGIN { FS="" }
(NR==FNR) {  # file1, index by lineno and string
  ll1[FNR]=$0; ss1[$0]=FNR; nl1=FNR;
}
(NR!=FNR) {  # file2
  if ($0 in ss1) { delete ll1[ss1[$0]]; delete ss1[$0]; }
}
END {
  for (ll=1; ll<=nl1; ll++) if (ll in ll1) print ll1[ll]
}

上面将file1的全部内容存储在两个数组中,一个由行号ll1[]索引,一个由行内容ss1[]索引。然后,当读取file2时,将从ll1[]ss1[]中删除每个匹配行。最后输出file1的剩余行,保留原始顺序。

在这种情况下,如上所述的问题,你也可以使用GNU split进行分割和征服(过滤是一个GNU扩展),每次重复运行file1块并完全读取file2:

1
split -l 20000 --filter='gawk -f linesnotin.awk - file2' < file1

请注意在gawk命令行上使用和放置-含义stdin。这是由文件1中的split以20000行每次调用的块提供的。

对于非GNU系统上的用户,几乎可以肯定你可以获得一个GNU coreutils包,包括在OSX上作为Apple Xcode工具的一部分提供GNU diffawk,尽管只有POSIX / BSD split而不是GNU版本。


像konsolebox建议,海报grep解决方案

1
grep -v -f file2 file1

如果只是添加-F选项,将模式视为固定字符串而不是正则表达式,实际上效果很好(快)。我在一对必须比较的~1000行文件列表中验证了这一点。使用-F时,它需要0.031秒(实际),而没有花费2.278秒(实际),将grep输出重定向到wc -l

这些测试还包括-x开关,它是解决方案的必要部分,以确保在file2包含与file1中的一行或多行匹配但不是全部的行的情况下完全准确。

因此,不需要对输入进行排序的解决方案是快速,灵活(区分大小写等),并且(我认为)适用于任何POSIX系统:

1
grep -F -x -v -f file2 file1


什么是排序和差异的速度?

1
2
3
sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted


如果你缺少"花哨的工具",例如在一些最小的Linux发行版中,有一个只有catsortuniq的解决方案:

1
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

测试:

1
2
3
4
5
6
7
seq 1 1 7 | sort --random-sort > includes.txt
seq 3 1 9 | sort --random-sort > excludes.txt
cat includes.txt excludes.txt excludes.txt | sort | uniq --unique

# Output:
1
2

grep相比,这也相对较快。


1
2
3
$ join -v 1 -t '' file1 file2
line2
line3

-t确保它比较整行,如果你在某些行中有空格的话。


你可以使用Python:

1
2
3
4
5
6
7
8
9
10
11
python -c '
lines_to_remove = set()
with open("file2","r") as f:
    for line in f.readlines():
        lines_to_remove.add(line.strip())

with open("f1","r") as f:
    for line in f.readlines():
        if line.strip() not in lines_to_remove:
            print(line.strip())
'

使用fgrep或向grep添加-F选项可能会有所帮助。但是为了更快的计算,你可以使用Awk。

您可以尝试以下Awk方法之一:

http://www.linuxquestions.org/questions/programming-9/grep-for-huge-files-826030/#post4066219


我通常这样做的方法是使用--suppress-common-lines标志,但请注意,这仅适用于并排格式的情况。

diff -y --suppress-common-lines file1.txt file2.txt


我发现对我来说使用正常的if和for循环语句非常有效。

1
for i in $(cat file2);do if [ $(grep -i $i file1) ];then echo"$i found">>Matching_lines.txt;else echo"$i missing">>missing_lines.txt ;fi;done