Использование Perl для очистки файловой системы с одним или несколькими дубликатами - PullRequest
1 голос
/ 08 июня 2009

У меня есть два диска, один резервный диск ad-hoc, который запутан повсюду с дубликатами, и другой диск в моем ноутбуке, который представляет собой беспорядок Мне нужно сделать резервную копию уникальных файлов и удалить дубликаты. Итак, мне нужно сделать следующее:

  • Найти все файлы ненулевого размера
  • Рассчитать дайджест 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 ]

2 голосов
/ 09 июня 2009

Глядя на алгоритм, я думаю, я понимаю, почему вы пропускаете файлы. Когда вы впервые сталкиваетесь с копией файла, вы помечаете ее как «уникальную»:

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}, вы потеряли его и не будете включены в уникальный + other + master.

Вам понадобится что-то вроде:

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

Кроме того, как я уже упоминал в комментарии выше, IO :: File действительно очистит этот код.

1 голос
/ 08 июня 2009

Это на самом деле не является ответом на более обширную логику программы, но вы должны каждый раз проверять ошибки в open (и пока мы это делаем, почему бы не использовать более современную форму open * с лексическими файловыми дескрипторами и тремя аргументами):

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

Если вы не хотите явно спрашивать каждый раз, вы также можете проверить модуль autodie.

0 голосов
/ 09 июня 2009

См. Здесь для связанных данных о решениях в абстрактной природе.

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

ВАЖНО! Примечание , как бы нам ни хотелось, чтобы 2 файла с одинаковым MD5 были одним файлом, это не обязательно так. Если ваши данные что-то значат для вас, после того как вы разбили их на список кандидатов, которые MD5 сообщает, что вы - один и тот же файл, вам необходимо линейно просмотреть каждый бит этих файлов, чтобы проверить, на самом деле то же самое.

Скажем так, учитывая хэш-функцию (то есть MD5) размером 1 бит, есть только 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 файла, совместно использующие один и тот же хеш, не означают, что они на самом деле одного и того же файла. единственный способ узнать - это сравнить каждый бит.

Существует небольшая оптимизация , которая происходит, если вы читаете их от начала до конца, потому что вы можете прекратить чтение, как только вы найдете отличающийся бит, но для подтверждения идентичности вам нужно прочитать каждый бит.

0 голосов
/ 08 июня 2009

Одной из очевидных оптимизаций является использование размера файла в качестве исходного начального сравнения, и только компьютер MD5 для файлов ниже определенного размера или если у вас есть столкновение двух файлов с одинаковым размером. Чем больше данный файл находится на диске, тем дороже вычисления MD5, но также менее вероятно, что его точный размер будет конфликтовать с другим файлом в системе. Таким образом, вы можете сэкономить много времени выполнения.

Вы также можете рассмотреть возможность изменения вашего подхода для определенных типов файлов, которые содержат встроенные метаданные, которые могут изменяться без изменения базовых данных, поэтому вы можете найти дополнительные дубликаты, даже если MD5 не совпадают. Я говорю, конечно, о MP3 или других музыкальных файлах, которые имеют теги метаданных, которые могут обновляться классификаторами или программами проигрывателя, но в остальном содержат те же биты аудио.

...