我有大约 30k 个文件。每个文件包含约 100k 行。一行不包含空格。单个文件中的行已排序并且不会重复。
我的目标:我想找到所有全部两个或多个文件中的重复行以及包含重复条目的文件的名称。
一个简单的解决方案是这样的:
cat *.words | sort | uniq -c | grep -v -F '1 '
然后我会运行:
grep 'duplicated entry' *.words
您看到更有效的方法吗?
答案1
由于所有输入文件都已排序,因此我们可以绕过实际的排序步骤,仅sort -m
用于合并文件放在一起。
在一些 Unix 系统上(据我所知仅有的Linux),这可能就足够了
sort -m *.words | uniq -d >dupes.txt
将重复的行写入文件dupes.txt
。
要查找这些行来自哪些文件,您可以执行以下操作
grep -Fx -f dupes.txt *.words
这将指示将( )grep
中的行视为dupes.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 次单独运行内部脚本(find
/xargs
将自动执行此操作)。
“内部”sh
脚本,
if [ -f tmpfile ]; then
sort -o tmpfile -m tmpfile "$@"
else
sort -o tmpfile -m "$@"
fi
用于sort -o tmpfile
输出到(即使这也是 的输入,也tmpfile
不会覆盖)和进行合并。在两个分支中,将扩展为从或传递到脚本的单独引用的文件名列表。tmpfile
sort
-m
"$@"
find
xargs
然后,继续运行uniq -d
以tmpfile
获取所有重复的行:
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
这很重要,因为发送到from 的最后一个文件名块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
将确保所有找到的路径名都以\0
( nul
) 字符作为分隔符输出。这是一个在 Unix 路径中无效的字符,它使我们能够处理路径名,即使它们包含换行符(或其他奇怪的东西)。
find
将其输出通过管道传输到xargs
.
xargs -0
将读取\0
以 - 分隔的路径名列表,并使用其中的块重复执行给定的实用程序,确保使用足够的参数执行该实用程序,以免导致 shell 抱怨参数列表太长,直到没有更多输入从find
。
调用的实用程序xargs
是sh
使用其标志在命令行上以字符串形式给出的脚本-c
。
当使用sh -c '...some script...'
后面的参数调用时,这些参数将可供脚本使用$@
,除了第一个参数,它将被放置在$0
(这是您可能会发现的“命令名称”,例如,top
如果您足够快的话)。这就是为什么我们sh
在实际脚本末尾插入字符串作为第一个参数。该字符串sh
是一个虚拟论证and 可以是任何单个单词(有些人似乎更喜欢_
或sh-find
)。
答案2
单个文件中的行已排序并且不会重复。
这意味着您可能会发现以下用途sort -m
:
-m, --merge
merge already sorted files; do not sort
另一个明显的替代方法是简单地awk
收集数组中的行,并对它们进行计数。但作为@戴夫·汤普森_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 重复行。
答案4
comm
是用于此类任务的另一种工具,唯一需要注意的是它需要预先排序的数据源。<(...)
语法在大多数现代 shell 中都可用。
# suppress common lines
comm -3 <(echo "1\n2") <(echo "3\n1"| sort)
2
3
# display common lines
comm -12 <(echo "1\n2") <(echo "1\n3")
1