3.4 查找并删除重复文件

重复文件是同一个文件的多个副本。有时候,我们需要删除重复的文件,只保留单个副本。通过查看文件内容来识别重复文件是件挺有意思的任务。可以结合多种shell工具来完成这项任务。在这则攻略中,我们讨论的是查找重复文件并基于查找结果执行相关的操作。

3.4.1 预备知识

重复文件指的是那些虽然名字不同但内容却一模一样的文件。我们可以通过比较文件内容来识别它们。校验和是依据文件内容来计算的,内容相同的文件自然会生成相同的校验和,因此,我们可以通过比较校验和来删除重复文件。

3.4.2 实战演练

按照下面的方法创建一些测试文件:

  1. $ echo "hello" > test ; cp test test_copy1 ; cp test test_copy2;
  2. $ echo "next" > other;
  3. # test_copy1和test_copy2都是test文件的副本

删除重复文件的脚本代码如下:

  1. #!/bin/bash
  2. #文件名: remove_duplicates.sh
  3. #用途: 查找并删除重复文件,每一个文件只保留一个样本
  4. ls -lS | awk 'BEGIN {
  5. getline;getline;
  6. name1=$8; size=$5
  7. }
  8. { name2=$8;
  9. if (size==$5)
  10. {
  11. "md5sum "name1 | getline; csum1=$1;
  12. "md5sum "name2 | getline; csum2=$1;
  13. if ( csum1==csum2 )
  14. {print name1; print name2 }
  15. };
  16. size=$5; name1=name2;
  17. }' | sort -u > duplicate_files
  18. cat duplicate_files | xargs -I {} md5sum {} | sort | uniq -w 32 | awk
  19. '{ print "^"$2"$" }' | sort -u > duplicate_sample
  20. echo Removing..
  21. comm duplicate_files duplicate_sample -2 -3 | tee /dev/stderr | xargs
  22. rm
  23. echo Removed duplicates files successfully.

执行方式:

  1. $ ./remove_duplicates.sh

3.4.3 工作原理

前文中的shell脚本会找出某个目录中同一文件的所有副本,然后保留单个副本的同时删除其他副本。让我们研究一下这个脚本的工作原理。ls -lS对当前目录下的所有文件按照文件大小进行排序,并列出文件的详细信息。awk读取ls -lS的输出,对行列进行比较,找出重复文件。

下面是代码的执行逻辑。

  • 我们将文件依据大小排序并列出,这样大小接近的文件就会排列在一起。识别大小相同的文件是我们查找重复文件的第一步。接下来,计算这些文件的校验和。如果校验和相同,那么这些文件就是重复文件,将被删除。

  • 在从文件中读取文本行之前,首先要执行awkBEGIN{}语句块。读取文本行的工作在{}语句块中进行,读取并处理完所有的行之后,执行END{}语句块。ls -lS的输出如下:

  1. total 16
  2. 4 -rw-r--r-- 1 slynux slynux 5 2010-06-29 11:50 other
  3. 4 -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test
  4. 4 -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test_copy1
  5. 4 -rw-r--r-- 1 slynux slynux 6 2010-06-29 11:50 test_copy2
  • 第1行输出告诉我们文件数量,这个信息在本例中没什么用处。我们用getline读取第1行,然后丢弃。由于需要对每一行及其下一行来进行文件大小比对,因此用getline读取长文件列表的第一行,并存储文件名和大小(它们分别是第8列和第5列)。这样我们就提前读取到了一行。接下来,awk进入{}语句块(在这个语句块中读取余下的文本行),对于读取到的每一行文本,都会执行该语句块。它将从当前行中读取到的文件大小与之前存储在变量size中的值进行比较。如果相等,那就意味着两个文件至少在大小上是相同的,随后再用md5sum执行进一步的检查。

至此,我们已试着采用了一些技巧来逐步解决问题。

awk中,外部命令的输出可以用下面的方法读取:

  1. "cmd" | getline

随后,我们就可以在$0中获取命令的输出,在$1,$2,…$n中获取命令输出中的每一列。我们将文件的md5sum保存在变量csum1csum2中。变量name1name2保存文件列表中位置连续的文件名。如果两个文件的校验和相同,那它们肯定是重复文件,而它们的文件名会被打印出来。

我们需要从每组重复文件中找出一个文件,使得在保留一个副本的同时,能够删除其他重复的文件。我们计算重复文件的 md5sum,从每一组重复文件中打印出一个文件。这是通过-w 32比较每一行的md5sum(md5sum输出中的前32个字符,md5sum的输出通常由32个字符的散列值和文件名组成),然后找出那些不相同的行。因此,每组重复文件中的一个采样就被写入duplicate_sample

现在需要将duplicate_files中列出的且未被列于duplicate_sample之内的全部文件删除。comm命令打印出未在duplicate_sample中列出且被duplicate_files包含的文件。

因此,我们可以使用差集操作(请参考3.3节)。

comm通常只接受排序过的文件。所以,在重定向到duplicate_filesduplicate_sample之前,首先用sort -u作为一个过滤器。

tee命令在这里有一个妙用:它在将文件名传递给rm命令的同时,也起到了print的功能。tee将来自stdin的行写入文件,并将其发送到stdout。我们也可以将文本重定向到stderr来实现终端打印功能。/dev/stderr是对应于stderr(标准错误)的设备。通过重定向到stderr设备文件,来自stdin的文本将会以标准错误的形式出现在终端中。

3.4.4 参考

  • 4.7节讲解了awk命令。

  • 2.7节讲解了md5sum命令。