我的一个巨大(最大 2 GiB)文本文件包含大约 100 个每行的精确重复项(在我的例子中没用,因为该文件是一个类似 CSV 的数据表)。
我需要的是删除所有重复,同时(最好是这样,但这可能会为了显着的性能提升而牺牲)保持原始序列顺序。在结果中,每一行都是唯一的。如果有 100 条相同的行(通常重复的行分布在文件中并且不会是邻居),则只会留下其中的一种。
我用 Scala 编写了一个程序(如果您不了解 Scala,请考虑使用 Java)来实现此功能。但也许有更快的 C 编写的本机工具能够更快地完成此操作?
更新:awk '!seen[$0]++' filename
只要文件接近 2 GiB 或更小,该解决方案似乎对我来说就很好用,但现在当我要清理 8 GiB 文件时,它不再起作用了。在具有 4 GiB RAM 的 Mac 和具有 4 GiB RAM 和 6 GiB 交换的 64 位 Windows 7 PC 上,似乎无穷无尽,只是内存不足。考虑到这次经历,我对在具有 4 GiB RAM 的 Linux 上尝试它并不热心。
答案1
awk
#bash (Freenode) 上看到的解决方案:
awk '!seen[$0]++' filename
如果要就地编辑文件,可以使用以下命令(前提是您使用实现此扩展的 GNU awk 版本):
awk -i inplace '!seen[$0]++' filename
答案2
有一个使用标准实用程序的简单(并不是说显而易见)的方法,除了运行之外不需要大量内存sort
,在大多数实现中,它对大文件有特定的优化(一个很好的外部排序算法)。此方法的优点是它仅循环专用实用程序内的所有行,而不会循环遍历解释语言内的所有行。
<input nl -b a -s : | # number the lines
sort -t : -k 2 -u | # sort and uniquify ignoring the line numbers
sort -t : -k 1n | # sort according to the line numbers
cut -d : -f 2- >output # remove the line numbers
如果所有行都以非空白字符开头,则可以省略一些选项:
<input nl | sort -k 2 -u | sort -k 1n | cut -f 2- >output
对于大量重复,仅需要在内存中存储每行的单个副本的方法会表现更好。通过一些解释开销,有一个非常简洁的 awk 脚本(已经由 enzotib 发表):
<input awk '!seen[$0]++'
不太简洁:!seen[$0] {print} {seen[$0] += 1}
,即如果尚未看到当前行,则打印当前行,然后增加seen
该行的计数器(未初始化的变量或数组元素的数值为 0)。
对于长行,您可以通过仅保留每行的不可欺骗的校验和(例如加密摘要)来节省内存。例如,使用 SHA-1,每行只需要 20 个字节加上恒定的开销。但计算摘要的速度相当慢;仅当您拥有快速 CPU(尤其是具有硬件加速器来计算摘要的 CPU)并且相对于文件大小和足够长的行而言内存不多时,此方法才会获胜。没有基本的实用程序可以让您计算每行的校验和;你必须承担 Perl/Python/Ruby/… 的解释开销,或者编写专用的编译程序。
<input perl -MDigest::MD5 -ne '$seen{Digest::MD5::md5($_)}++ or print' >output
答案3
sort -u big-csv-file.csv > duplicates-removed.csv
请注意,输出文件将被排序。
答案4
gawk -i inplace '!a[$0]++' SOME_FILE [SOME_OTHER_FILES...]
此命令会过滤掉重复的行,同时保留其顺序,并将文件保存在适当的位置。
它通过保留所有唯一行的缓存并仅打印每行一次来完成此任务。
确切的算法可以分解为:
- 将当前行存储在变量中
$0
- 检查关联数组是否
a
有键$0
,如果没有,则创建键并将其值初始化为0
- 比较键的值,
0
如果为 true,则打印当前行 - 将键的值增加
1
- 获取下一行并转到步骤 1。直到到达 EOF
或作为伪代码:
while read $line
do
$0 := $line
if not a.has_key($0) :
a[$0] := 0
if a[$0] == 0 :
print($line)
a[$0] := a[$0] + 1
done
注意:该命令需要 2013 年或更高版本的 GNU AWK 版本 4.1