1

我有两个磁盘,一个是临时备份磁盘,到处都是重复的,而我的笔记本电脑中的另一个磁盘也是一团糟。我需要备份唯一文件并删除重复文件。因此,我需要执行以下操作:

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

使用此脚本的输出,我将:

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

唯一文件= 没有其他副本

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

其他副本= 非主副本

我创建了附加脚本,这对我来说似乎很有意义,但是:

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

我有两个问题:

  1. 我的逻辑错误在哪里?
  2. 有没有更有效的方法来做到这一点?

我选择了磁盘哈希,以便在处理大量文件列表时不会耗尽内存。

#!/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

4 回答 4

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真的会清理这段代码。

于 2009-06-08T20:19:26.787 回答
1

这并不是对程序更大逻辑的真正响应,但您应该open每次都检查错误(当我们这样做时,为什么不使用更现代的形式open词法文件句柄和三个参数):

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

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

于 2009-06-08T19:57:05.273 回答
0

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

您可能还需要考虑更改某些包含嵌入元数据的文件的方法,这些文件可能会在不更改基础数据的情况下发生变化,因此即使 MD5 不匹配,您也可以找到额外的欺骗。我说的当然是 MP3 或其他具有元数据标签的音乐文件,这些标签可能由分类器或播放器程序更新,但在其他方面包含相同的音频位。

于 2009-06-08T19:42:58.827 回答
0

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

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

重要说明,尽管我们愿意相信 2 个具有相同 MD5 的文件是同一个文件,但这并不一定是正确的。如果您的数据对您有任何意义,一旦您将其分解为 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 个文件并不意味着它们实际上是同一个文件。唯一知道的方法是比较每一位。

如果您从头到尾阅读它们会发生一个小的优化,因为您可以在找到不同的位后立即停止阅读,但要确认相同,您需要阅读每个位。

于 2009-06-09T02:31:20.593 回答