使用Perl清理具有一个或多个重复项的文件系统

时间:2009-06-08 19:39:22

标签: perl hash backup duplicate-data

我有两个磁盘,一个是临时备份磁盘,到处都是重复的混乱,而我的笔记本电脑中的另一个磁盘是同样的混乱。我需要备份唯一文件并删除重复项。所以,我需要做以下事情:

  • 查找所有非零大小的文件
  • 计算所有文件的MD5摘要
  • 查找具有重复文件名的文件
  • 从主副本和其他副本中分离唯一的文件。

根据这个脚本的输出,我将:

  • 备份唯一文件和主文件
  • 删除其他副本

唯一文件 =没有其他副本

主副本 =第一个实例,其中存在其他副本,可能与优先路径匹配

其他副本 =非主副本

我已经创建了附加脚本,这似乎对我有意义,但是:

总文件数!=唯一文件+主副本+其他副本

我有两个问题:

  1. 我逻辑中的错误在哪里?
  2. 有更有效的方法吗?
  3. 我选择了磁盘哈希,因此在处理大量文件列表时我不会耗尽内存。

    #!/usr/bin/perl
    
    use strict;
    use warnings;
    use DB_File;
    use File::Spec;
    use Digest::MD5;
    
    my $path_pref = '/usr/local/bin';
    my $base = '/var/backup/test';
    
    my $find = "$base/find.txt";
    my $files = "$base/files.txt";
    
    my $db_duplicate_file = "$base/duplicate.db";
    my $db_duplicate_count_file = "$base/duplicate_count.db";
    my $db_unique_file = "$base/unique.db";
    my $db_master_copy_file = "$base/master_copy.db";
    my $db_other_copy_file = "$base/other_copy.db";
    
    open (FIND, "< $find");
    open (FILES, "> $files");
    
    print "Extracting non-zero files from:\n\t$find\n";
    my $total_files = 0;
    while (my $path = <FIND>) {
      chomp($path);
      next if ($path =~ /^\s*$/);
      if (-f $path && -s $path) {
        print FILES "$path\n";
        $total_files++;
        printf "\r$total_files";
      }
    }
    
    close(FIND);
    close(FILES);
    open (FILES, "< $files");
    
    sub compare {
      my ($key1, $key2) = @_;
      $key1 cmp $key2;
    }
    
    $DB_BTREE->{'compare'} = \&compare;
    
    my %duplicate_count = ();
    
    tie %duplicate_count, "DB_File", $db_duplicate_count_file, O_RDWR|O_CREAT, 0666, $DB_BTREE
         or die "Cannot open $db_duplicate_count_file: $!\n";
    
    my %unique = ();
    
    tie %unique, "DB_File", $db_unique_file, O_RDWR|O_CREAT, 0666, $DB_BTREE
         or die "Cannot open $db_unique_file: $!\n";
    
    my %master_copy = ();
    
    tie %master_copy, "DB_File", $db_master_copy_file, O_RDWR|O_CREAT, 0666, $DB_BTREE
         or die "Cannot open $db_master_copy_file: $!\n";
    
    my %other_copy = ();
    
    tie %other_copy, "DB_File", $db_other_copy_file, O_RDWR|O_CREAT, 0666, $DB_BTREE
         or die "Cannot open $db_other_copy_file: $!\n";
    
    print "\nFinding duplicate filenames and calculating their MD5 digests\n";
    
    my $file_counter = 0;
    my $percent_complete = 0;
    
    while (my $path = <FILES>) {
    
      $file_counter++;
    
      # remove trailing whitespace
      chomp($path);
    
      # extract filename from path
      my ($vol,$dir,$filename) = File::Spec->splitpath($path);
    
      # calculate the file's MD5 digest
      open(FILE, $path) or die "Can't open $path: $!";
      binmode(FILE);
      my $md5digest = Digest::MD5->new->addfile(*FILE)->hexdigest;
      close(FILE);
    
      # filename not stored as duplicate
      if (!exists($duplicate_count{$filename})) {
        # assume unique
        $unique{$md5digest} = $path;
        # which implies 0 duplicates
        $duplicate_count{$filename} = 0;
      }
      # filename already found
      else {
        # delete unique record
        delete($unique{$md5digest});
        # second duplicate
        if ($duplicate_count{$filename}) {
          $duplicate_count{$filename}++;
        }
        # first duplicate
        else {
          $duplicate_count{$filename} = 1;
        }
        # the master copy is already assigned
        if (exists($master_copy{$md5digest})) {
          # the current path matches $path_pref, so becomes our new master copy
          if ($path =~ qq|^$path_pref|) {
            $master_copy{$md5digest} = $path;
          }
          else {
            # this one is a secondary copy
            $other_copy{$path} = $md5digest;
            # store with path as key, as there are duplicate digests
          }
        }
        # assume this is the master copy
        else {
          $master_copy{$md5digest} = $path;
        }
      }
      $percent_complete = int(($file_counter/$total_files)*100);
      printf("\rProgress: $percent_complete %%");
    }
    
    close(FILES);    
    
    # Write out data to text files for debugging
    
    open (UNIQUE, "> $base/unique.txt");
    open (UNIQUE_MD5, "> $base/unique_md5.txt");
    
    print "\n\nUnique files: ",scalar keys %unique,"\n";
    
    foreach my $key (keys %unique) {
      print UNIQUE "$key\t", $unique{$key}, "\n";
      print UNIQUE_MD5 "$key\n";
    }
    
    close UNIQUE;
    close UNIQUE_MD5;
    
    open (MASTER, "> $base/master_copy.txt");
    open (MASTER_MD5, "> $base/master_copy_md5.txt");
    
    print "Master copies: ",scalar keys %master_copy,"\n";
    
    foreach my $key (keys %master_copy) {
      print MASTER "$key\t", $master_copy{$key}, "\n";
      print MASTER_MD5 "$key\n";
    }
    
    close MASTER;
    close MASTER_MD5;
    
    open (OTHER, "> $base/other_copy.txt");
    open (OTHER_MD5, "> $base/other_copy_md5.txt");
    
    print "Other copies: ",scalar keys %other_copy,"\n";
    
    foreach my $key (keys %other_copy) {
      print OTHER $other_copy{$key}, "\t$key\n";
      print OTHER_MD5 "$other_copy{$key}\n";
    }
    
    close OTHER;
    close OTHER_MD5;
    
    print "\n";
    
    untie %duplicate_count;
    untie %unique;
    untie %master_copy;
    untie %other_copy;
    
    print "\n";
    

4 个答案:

答案 0 :(得分:2)

看看算法,我想我明白你为什么要泄漏文件。第一次遇到文件副本时,将其标记为“唯一”:

if (!exists($duplicate_count{$filename})) {
   # assume unique
   $unique{$md5digest} = $path;
   # which implies 0 duplicates
   $duplicate_count{$filename} = 0;
}

下次删除该唯一记录,而不存储路径:

 # delete unique record
delete($unique{$md5digest});

因此无论文件路径是$ unique {$ md5digest},你都丢失了它,并且不会包含在unique + other + master中。

你需要这样的东西:

if(my $original_path = delete $unique{$md5digest}) {
    // Where should this one go?
}

另外,正如我在上面的评论中提到的那样,IO::File会真正清理这段代码。

答案 1 :(得分:1)

这不是对程序中较大逻辑的回应,但你应该每次都检查open中的错误(当我们在它时,为什么不使用更现代的形式带有词法文件句柄和三个参数的open

open my $unique, '>', "$base/unique.txt"
  or die "Can't open $base/unique.txt for writing: $!";

如果您不想每次都明确询问,您还可以查看autodie模块。

答案 2 :(得分:0)

一个明显的优化是使用文件大小作为初始比较基础,并且只有计算机MD5用于低于特定大小的文件,或者如果您有两个具有相同大小的文件的冲突。给定文件在盘上越大,MD5计算的成本越高,但其确切大小与系统上的另一个文件冲突的可能性也越小。你可以用这种方式节省很多运行时间。

您还可以考虑更改某些类型的文件,这些文件包含可能在不更改基础数据的情况下更改的嵌入式元数据,因此即使MD5不匹配,您也可以找到其他欺骗。我说的当然是MP3或其他音乐文件,其中包含可能由分类器或播放器程序更新的元数据标签,但是否则包含相同的音频位。

答案 3 :(得分:0)

请参阅此处,了解有关抽象性解决方案的相关数据。

  
    

https://stackoverflow.com/questions/405628/what-is-the-best-method-to-remove-duplicate-image-files-from-your-computer

  

重要提示,尽管我们想要相信具有相同MD5的2个文件是同一个文件,但这不一定是真的。如果您的数据对您来说意味着什么,那么一旦您将其分解为MD5告诉您的同一文件的候选列表,您需要线性地遍历这些文件的每个位以检查它们实际上是一样的。

这样,给定大小为1位的散列函数(MD5),只有2种可能的组合。

0 1

如果您的哈希函数告诉您2个文件都返回“1”,您不会认为它们是同一个文件。

给定2位散列,只有4种可能的组合,

 00  01 10 11 

2返回相同值的文件,您不会认为它们是同一个文件。

给定3位散列,只有8种可能的组合

 000 001 010 011 
 100 101 110 111

2个文件返回相同的值,您不会认为它们是同一个文件。

这种模式的数量不断增加,人们出于某种奇怪的原因开始将“机会”放入等式中。即使在128位(MD5),共享相同散列的2个文件并不意味着它们 实际上是同一个文件。 知道的唯一方法是比较每一位。

如果您从头开始读取它们会发生次要优化,因为您可以在找到不同位时立即停止读取,但要确认相同,您需要读取每一位