如何在许多大文件中找到重复的行?


9

我有约3万个文件。每个文件包含约10万行。一行不包含空格。单个文件中的行已排序并免费重复。

我的目标:我想找到两个或多个文件中所有重复的行,以及包含重复条目的文件的名称。

一个简单的解决方案是:

cat *.words | sort | uniq -c | grep -v -F '1 '

然后我会运行:

grep 'duplicated entry' *.words

您看到更有效的方法了吗?

Answers:


13

由于所有输入文件均已排序,因此我们可以绕过实际的排序步骤,而仅sort -m用于文件合并在一起。

在某些Unix系统上(据我所知 Linux),这可能足以完成

sort -m *.words | uniq -d >dupes.txt

将重复的行写入文件dupes.txt

要查找这些行来自什么文件,您可以执行以下操作

grep -Fx -f dupes.txt *.words

这将指示grepdupes.txt-f dupes.txt)中的行视为固定字符串模式-F)。grep还将要求整行从头到尾完全匹配(-x)。它将文件名和行打印到终端。

非Linux Unices(甚至更多文件)

在某些Unix系统上,30000个文件名将扩展为一个字符串,该字符串太长而无法传递给单个实用程序(这sort -m *.words将导致失败Argument list too long,它在OpenBSD系统上会失败)。如果文件数量大得多,甚至Linux都会抱怨这一点。

寻找骗子

这意味着,在一般情况下(这也将与工作很多不止30000文件),一个具有“块”的排序:

rm -f tmpfile
find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh 

或者,tmpfile不创建以下内容xargs

rm -f tmpfile
find . -type f -name '*.words' -exec sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh {} +

这将在当前目录(或以下)中找到名称匹配的所有文件*.words。对于一次适当大小的这些名称块,其大小由xargs/ 确定find,它将它们合并在一起成为排序的tmpfile文件。如果tmpfile已经存在(除了第一个块以外的所有块),该文件还将与当前块中的其他文件合并。根据文件名的长度以及命令行的最大允许长度,这可能需要内部脚本运行10次以上(findxargs自动运行)。

“内部” sh脚本,

if [ -f tmpfile ]; then
    sort -o tmpfile -m tmpfile "$@"
else
    sort -o tmpfile -m "$@"
fi

用于sort -o tmpfile输出到tmpfiletmpfile即使这也是的输入也不会覆盖sort)并-m进行合并。在这两个分支中,"$@"将扩展为从find或传递到脚本的单独引用文件名的列表xargs

然后,只需运行uniq -dtmpfile得到是重复的所有行:

uniq -d tmpfile >dupes.txt

如果您喜欢“ DRY”原则(“不要重复自己”),则可以将内部脚本编写为

if [ -f tmpfile ]; then
    t=tmpfile
else
    t=/dev/null
fi

sort -o tmpfile -m "$t" "$@"

要么

t=tmpfile
[ ! -f "$t" ] && t=/dev/null
sort -o tmpfile -m "$t" "$@"

哪儿来的呢?

出于与上述相同的原因,我们无法使用grep -Fx -f dupes.txt *.words这些重复项的来源,因此我们find再次使用:

find . -type f -name '*.words' \
    -exec grep -Fx -f dupes.txt {} +

由于没有要执行的“复杂”处理,因此我们可以grep直接从调用-exec。该-exec选项接受一个实用程序命令,并将找到的名称放在中{}。随着+末,find将放置在发生的许多论点{}作为实用程序的每个调用当前shell支持。

完全正确的,一个可能希望用

find . -type f -name '*.words' \
    -exec grep -H -Fx -f dupes.txt {} +

要么

find . -type f -name '*.words' \
    -exec grep -Fx -f dupes.txt /dev/null {} +

确保文件名始终包含在的输出中grep

第一个变体用于grep -H始终输出匹配的文件名。最后一个变种使用的事实是,grep如果在命令行上给出多个文件,则将包括匹配文件的名称。

这很重要,因为grep从中发送的最后一部分文件名find实际上可能只包含一个文件名,在这种情况下grep,结果中不会提及该文件名。


奖励材料:

剖析find+ xargs+ sh命令:

find . -type f -name '*.words' -print0 |
xargs -0 sh -c '
    if [ -f tmpfile ]; then
        sort -o tmpfile -m tmpfile "$@"
    else
        sort -o tmpfile -m "$@"
    fi' sh 

find . -type f -name '*.words'会从当前目录(或以下)简单地生成一个路径名列表,其中每个路径名都是常规文件-type f)的路径名,并且末尾具有与匹配的文件名部分*.words。如果仅要搜索当前目录,则可以-maxdepth 1.之前,之后添加一个-type f

-print0将确保以\0nul)字符作为分隔符输出所有找到的路径名。这是一个在Unix路径中无效的字符,它使我们能够处理路径名,即使它们包含换行符(或其他奇怪的东西)也是如此。

find将其输出传递到xargs

xargs -0将读取\0路径名称的- 分隔列表,并将使用其中的大块重复执行给定的实用程序,确保使用足够多的参数执行该实用程序,以免外壳程序抱怨过长的参数列表,直到没有更多输入为止来自find

通过调用该实用程序xargssh在命令行作为使用其一个串上给定的脚本-c标志。

sh -c '...some script...'后面跟随参数调用时,参数将可用于中的脚本$@但第一个参数除外,该参数将放置在其中$0(这是您可能会发现的“命令名称”,例如,top如果您足够快的话)。这就是为什么我们sh在实际脚本的末尾插入字符串作为第一个参数的原因。字符串sh是一个伪参数,可以是任何单个单词(有些似乎更喜欢_sh-find)。


在您的第一个shell脚本块的末尾,的用途是fi' sh什么?

@danielAzuelos这fiif“内部” shshell脚本中语句的结尾。'shell脚本的结尾(整个脚本是一个单引号的字符串)。在sh将被传递给在内部脚本$0(不是部分$@,其中将包含文件名)。在这种情况下,该sh字符串实际上可以是任何单词。如果sh最后遗漏,则第一个文件名将被传入,$0并且不会成为内部shell脚本正在执行的处理的一部分。
库萨兰达

8

单个文件中的行已排序并免费重复。

这意味着您可能会发现以下用途sort -m

 -m, --merge
        merge already sorted files; do not sort

另一个明显的替代方法是将awk数组中的行收集起来并计数。但是,正如@ dave_thompson_085所评论的那样,这30亿行(或者有很多独特的行)可能会占用相当多的内存,因此可能无法很好地工作。


3

使用awk,您可以通过一个短命令获得所有文件中所有重复的行:

$ awk '_[$0]++' *.words

但是,如果一条线存在3次或更多次,它将重复行。
有一种解决方案仅获取第一个重复项:

$ awk '_[$0]++==1' *.words

它应该很快(如果重复次数很少),但是会吃掉很多内存以将所有行保留在内存中。也许,根据您的实际文件和重复文件,请先尝试3个或4个文件。

$ awk '_[$0]++==1' [123]*.words

否则,您可以执行以下操作:

$ sort -m *.words | uniq -d

它将打印uniq重复行。


2
+1 forsort -m * | uniq -d
Jeff Schaller

awk可以避免重复,'x[$0]++==1'但是确实需要很多内存;如果3G行有1G不同的值,并且如果awk需要说50个字节的哈希数组条目将一个(可能是短的)字符串映射到uninit值,则为50GB。对于已排序的输入,您可以uniq -d手动执行,awk '$0==p&&n++==1;$0!=p{p=$0;n=1}'但是为什么要打扰呢?
dave_thompson_085

@ dave_thompson_085感谢的概念==1,很棒的主意。
艾萨克(Isaac)

假设有30000个文件,其中100000行每行80个字符且没有重复,这将需要awk存储2.4E11字节(223 GiB)。
库萨兰达

sort -m *.words | uniq -d很棒!完成该过程后,我运行grep查找包含重复条目的文件。您是否看到一种打印至少一个包含重复条目的文件名的方法?
拉尔斯·施耐德

3

优化sort+ uniq解决方案:

sort --parallel=30000 *.words | uniq -d
  • --parallel=N -将同时运行的排序数更改为 N
  • -d, --repeated -仅打印重复的行,每组一行
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.