在一个文件中查找不在另一个文件中的行的快速方法?

Posted

技术标签:

【中文标题】在一个文件中查找不在另一个文件中的行的快速方法?【英文标题】:Fast way of finding lines in one file that are not in another? 【发布时间】:2013-08-14 19:43:38 【问题描述】:

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

例如,如果这是 file1:

line1
line2
line3

这是 file2:

line1
line4
line5

那么我的结果/输出应该是:

line2
line3

这行得通:

grep -v -f file2 file1

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

我怀疑有一个使用 diff() 的好方法,但输出应该是 行,没有别的,而且我似乎找不到开关。 p>

谁能帮我找到一种快速的方法,使用 bash 和基本的 Linux 二进制文件?

编辑:要跟进我自己的问题,这是迄今为止我使用diff() 找到的最佳方法:

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

当然,一定有更好的方法吗?

【问题讨论】:

如果速度更快,你可以试试这个:awk 'NR==FNRa[$0];next!($0 in a)' file2 file1 > out.txt 无快速要求:***.com/questions/4366533/… 感谢您介绍 grep -v -f file2 file1 参见:Fastest way to find lines of a file from another larger file in Bash。 简化工具集的简单方法:cat file1 file2 file2 | sort | uniq --unique,请参阅下面的答案。 【参考方案1】:

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

#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 文件实际上是非常可读的。

【讨论】:

在 OSX 上完美运行。 或许应该突出显示排序输入的要求。 comm 也有一个选项来验证输入是否已排序,--check-order(它似乎无论如何都会这样做,但这个选项会导致它出错而不是继续)。但是要对文件进行排序,只需执行:com -23 <(sort file1) <(sort file2) 等等 我将在 Windows 中生成的文件与在 Linux 中生成的文件进行比较,似乎 comm 根本不起作用。我花了一段时间才弄清楚这是关于行尾的:即使看起来相同的行如果有不同的行尾,也会被认为是不同的。命令dos2unix 可用于将 CRLF 行尾转换为仅 LF。 警告“这不适用于具有 DOS 行结尾的文件”必须或多或少地添加到每个 shell 脚本答案中。这是一个常见的常见问题解答;见***.com/questions/39527571/…【参考方案2】:

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

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

输入文件应该被排序,这样才能正常工作。使用bash(和zsh),您可以使用进程替换<( )就地排序:

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

在上面的 newunchanged 行被抑制,所以只有 changed (即在您的情况下删除的行)被输出。您还可以使用一些其他解决方案不提供的diff 选项,例如-i 来忽略大小写,或者使用各种空格选项(-E-b-v 等)来实现不太严格的匹配。


说明

--new-line-format--old-line-format--unchanged-line-format 选项让您控制diff 格式化差异的方式,类似于printf 格式说明符。这些选项分别格式化 new(添加)、old(删除)和 unchanged 行。将一个设置为空 "" 会阻止输出那种行。

如果您熟悉 unified diff 格式,您可以部分地重新创建它:

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

%L 说明符是有问题的行,我们用“+”“-”或“”作为前缀,例如 diff -u (请注意,它只输出差异,它缺少每个分组更改顶部的 --- +++@@ 行)。 您还可以使用它来做其他有用的事情,例如 number each line 和 %dn


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

# 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]

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

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

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 扩展),重复运行文件 1 块并每次完全读取文件 2时间:

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

注意- 的使用和位置,意思是stdingawk 命令行上。这是由来自 file1 的 split 以每次调用 20000 行的块提供的。

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

【讨论】:

这正是我所需要的,只需要巨大的 grep 所花费的时间的一小部分。谢谢! 找到这个gnu manpage 我们中的一些人不在 gnu [OS X bsd here...] :) 我假设您的意思是 diff:通常输入文件会有所不同,在这种情况下,diff 返回 1。认为这是一个奖励 ;-) 如果您在 shell 脚本中进行测试,0 和 1 是预期的退出代码,2 表示有问题。 @mr.spuratic 啊,是的,现在我在man diff 中找到了它。谢谢!【参考方案3】:

就像 konsolebox 建议的那样,海报的 grep 解决方案

grep -v -f file2 file1

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

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

因此,不需要对输入进行排序、快速、灵活(区分大小写等)的解决方案是:

grep -F -x -v -f file2 file1

这不适用于所有版本的 grep,例如它在 macOS 中失败,其中文件 1 中的一行将显示为文件 2 中不存在,即使它与另一行匹配它的子串。或者,您可以install GNU grep on macOS 以使用此解决方案。

【讨论】:

是的,它可以工作,但即使使用 -F 这也不能很好地扩展。 这不是那么快,我等了 5 分钟才放弃了 2 个约 500k 行的文件 其实这种方式还是比comm方式慢,因为这种方式可以处理未排序的文件,所以被unsorting拖了下来,comm利用了排序的优势 @workplaylifecycle 您需要添加排序时间,这可能是非常大的file2 的瓶颈。 但是,带有-x 选项的grep 显然会占用更多内存。 file2 包含 6-10 字节的 180M 字,我的进程在 32GB RAM 机器上得到了 Killed...【参考方案4】:

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

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

测试:

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 相比,这也相对快。

【讨论】:

注意——某些实现将无法识别--unique 选项。您应该可以为此使用standardized POSIX option:| uniq -u 在示例中,“2”是从哪里来的? @Niels2000, seq 1 1 7 从 1 创建数字,增量为 1,直到 7,即 1 2 3 4 5 6 7。然后就是你的 2!【参考方案5】:

使用来自moreutils 包的combine,这是一个支持notandorxor 操作的集合实用程序

combine file1 not file2

即给我在 file1 但不在 file2 中的行

或者给我 file1 中的行减去 file2 中的行

注意: combine 在执行任何操作之前会在两个文件中排序并查找唯一行,但 diff 不会。因此,您可能会发现diffcombine 的输出之间存在差异。

所以实际上你是在说

在 file1 和 file2 中找到不同的行,然后给我 file1 中的行减去 file2 中的行

根据我的经验,它比其他选项快得多

【讨论】:

【参考方案6】:

sort 和 diff 的速度是多少?

sort file1 -u > file1.sorted
sort file2 -u > file2.sorted
diff file1.sorted file2.sorted

【讨论】:

感谢您提醒我在进行 diff 之前需要对文件进行排序。 sort + diff 更快。 一个衬里 ;-) diff 【参考方案7】:
$ join -v 1 -t '' file1 file2
line2
line3

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

【讨论】:

commjoin 要求在您正在执行连接操作的字段上对两个输入行进行排序。【参考方案8】:

你可以使用 Python:

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())
'

【讨论】:

【参考方案9】:

这对我来说似乎很快:

comm -1 -3 <(sort file1.txt) <(sort file2.txt) > output.txt

【讨论】:

太棒了,但目标问题只是comm file1 file2,因为看起来像提供的排序列表【参考方案10】:

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

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

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

【讨论】:

+1 这是唯一不需要对输入进行排序的答案。虽然 OP 显然对这个要求很满意,但在许多现实世界的场景中这是一个不可接受的约束。【参考方案11】:

我通常这样做的方式是使用--suppress-common-lines 标志,但请注意,这只有在您以并排格式执行时才有效。

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

【讨论】:

【参考方案12】:

我发现使用普通的 if 和 for 循环语句对我来说效果很好。

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

【讨论】:

见DontReadLinesWithFor。此外,如果您的任何 grep 结果扩展到多个单词,或者如果您的任何 file2 条目可以被 shell 视为一个 glob,则此代码的行为将非常糟糕。

以上是关于在一个文件中查找不在另一个文件中的行的快速方法?的主要内容,如果未能解决你的问题,请参考以下文章

SQL删除不在另一个表中的行

Bash:使用另一个文件的行查找和替换文件中的行

在 Bash 中从另一个较大文件中查找文件行的最快方法

请问如何获得GridView选中行的每一列的信息?

Unix命令在一个文件中查找行,而在另一个文件中找不到该键

从另一个 Dataframe 中的一个 Dataframe 中查找元素并返回其索引的快速方法