web-dev-qa-db-fra.com

Rechercher des doublons d'un fichier par contenu

J'essaie actuellement de prendre un fichier (un fichier image tel que test1.jpg) et j'ai besoin d'une liste de tous les doublons de ce fichier (par contenu). J'ai essayé fdupes mais cela ne permet pas à un fichier d'entrée de se baser sur ses vérifications.

TLDR: J'ai besoin d'un moyen de lister tous les doublons d'un fichier spécifique par leur contenu.

Il est préférable de rechercher une solution via la ligne de commande, mais des applications complètes conviendront également.

6
GamrCorps

Recherchez d’abord le hachage md5 de votre fichier:

$ md5sum path/to/file
e740926ec3fce151a68abfbdac3787aa  path/to/file

(la première ligne est la commande que vous devez exécuter, la deuxième ligne est le hash md5 de ce fichier)

Copiez ensuite le hachage (ce serait différent dans votre cas) et collez-le dans la commande suivante:

$ find . -type f -print0 | xargs -0 md5sum | grep e740926ec3fce151a68abfbdac3787aa
e740926ec3fce151a68abfbdac3787aa  ./path/to/file
e740926ec3fce151a68abfbdac3787aa  ./path/to/other/file/with/same/content
....

Si vous voulez avoir envie, vous pouvez combiner les 2 en une seule commande:

$ find . -type f -print0 | xargs -0 md5sum | grep `md5sum path/to/file | cut -d " " -f 1`
e740926ec3fce151a68abfbdac3787aa  ./path/to/file
e740926ec3fce151a68abfbdac3787aa  ./path/to/other/file/with/same/content
....

Vous pouvez utiliser sha1 ou n'importe lequel des autres hachages fantaisie si vous le souhaitez.

Éditer

Si le cas d'utilisation consiste à rechercher dans "plusieurs MP4 ou fichiers ISO de plusieurs gigaoctets" pour trouver un "4 KB jpg" (comme indiqué dans la réponse @Tijn), la spécification de la taille du fichier accélèrerait considérablement.

Si la taille du fichier que vous recherchez est exactement de 3952 octets (comme vous pouvez le constater avec ls -l path/to/file alors cette commande s'exécute beaucoup plus rapidement:

$ find . -type f -size 3952c -print0 | xargs -0 md5sum | grep e740926ec3fce151a68abfbdac3787aa
e740926ec3fce151a68abfbdac3787aa  ./path/to/file
e740926ec3fce151a68abfbdac3787aa  ./path/to/other/file/with/same/content

Notez le supplément c après la taille, indiquant les caractères/octets.

Si vous le souhaitez, vous pouvez combiner cela en une seule commande:

FILE=./path/to/file && find . -type f -size $(du -b $FILE | cut -f1)c -print0 | xargs -0 md5sum | grep $(md5sum $FILE | cut -f1 -d " ")
8
sмurf

Vous pouvez utiliser filecmp dans Python

Par exemple:

import filecmp 
print filecmp.cmp('filename.png', 'filename.png') 

Imprimera Vrai si est égal à, sinon Faux

4
Benny

Utilisez la commande diff avec les opérateurs booléens && et ||

bash-4.3$ diff /etc/passwd passwd_duplicate.txt > /dev/null && echo "SAME CONTENT" || echo "CONTENT DIFFERS"
SAME CONTENT

bash-4.3$ diff /etc/passwd TESTFILE.txt > /dev/null && echo "SAME CONTENT" || echo "CONTENT DIFFERS"
CONTENT DIFFERS

Si vous voulez parcourir plusieurs fichiers dans un répertoire spécifique, cd ici et utilisez une boucle for comme ceci:

bash-4.3$ for file in * ; do  diff /etc/passwd "$file" > /dev/null && echo "$file has same contents" || echo "$file has different contents"; done
also-waste.txt has different contents
directory_cleaner.py has different contents
dontdeletethisfile.txt has different contents
dont-delete.txt has different contents
important.txt has different contents
list.txt has different contents
neverdeletethis.txt has different contents
never-used-it.txt has different contents
passwd_dulicate.txt has same contents

Pour les cas récursifs, utilisez la commande find pour parcourir le répertoire et tous ses sous-répertoires (sans oublier les guillemets et toutes les barres obliques appropriées):

bash-4.3$ find . -type f -exec sh -c 'diff /etc/passwd "{}" > /dev/null &&  echo "{} same" || echo "{} differs"' \;
./reallyimportantfile.txt differs
./dont-delete.txt differs
./directory_cleaner.py differs
./TESTFILE.txt differs
./dontdeletethisfile.txt differs
./neverdeletethis.txt differs
./important.txt differs
./passwd_dulicate.txt same
./this-can-be-deleted.txt differs
./also-waste.txt differs
./never-used-it.txt differs
./list.txt differs
4

Obtenez le md5sum du fichier en question et enregistrez-le dans une variable, par exemple. md5:

md5=$(md5sum file.txt | awk '{print $1}')

Utilisez find pour parcourir l’arborescence de répertoires souhaitée et vérifiez si un fichier a la même valeur de hachage. Le cas échéant, indiquez le nom du fichier:

find . -type f -exec sh -c '[ "$(md5sum "$1" | awk "{print \$1}")" = "$2" ] \
                             && echo "$1"' _ {} "$md5" \;
  • find . -type f trouve tous les fichiers dans le répertoire actuel, changez le répertoire pour répondre à vos besoins

  • le prédicat -exec exécute la commande sh -c ... sur tous les fichiers trouvés

  • Dans sh -c, _ est un espace réservé pour $0, $1 est le fichier trouvé, $2 est $md5.

  • [ $(md5sum "$1"|awk "{print \$1}") = "$2" ] && echo "$1" affiche le nom du fichier si la valeur de hachage du fichier est identique à celle pour laquelle nous vérifions les doublons.

Exemple:

% md5sum ../foo.txt bar.txt 
d41d8cd98f00b204e9800998ecf8427e  ../foo.txt
d41d8cd98f00b204e9800998ecf8427e  bar.txt

% md5=$(md5sum ../foo.txt | awk '{print $1}')

% find . -type f -exec sh -c '[ "$(md5sum "$1" | awk "{print \$1}")" = "$2" ] && echo "$1"' _ {} "$md5" \;
bar.txt
2
heemayl

@smurf et @heemayl ont certes raison, mais j’ai découvert que dans mon cas, c’était plus lent que je ne le voulais; J'ai tout simplement trop de fichiers à traiter. Par conséquent, j’ai écrit un petit outil en ligne de commande qui, je pense, pourrait vous aider aussi. ( https://github.com/tijn/dupfinder ; Ruby; pas de dépendances externes)

Fondamentalement, mon script reporte le calcul du hachage: il effectuera le calcul uniquement lorsque la taille des fichiers correspond. Depuis pourquoi voudrais-je diffuser en streaming le contenu de plusieurs MP4 ou fichiers iso de plusieurs gigaoctets à l'aide d'un algorithme de hachage, alors que je sais que je recherche un fichier jpg de 4 Ko!? Le reste du script consiste principalement en un format de sortie.

Edit: (merci @Serg) Voici le code source de tout le script. Vous devriez le sauvegarder dans ~/bin/find-dups ou peut-être même /usr/local/bin/find-dups, puis utilisez chmod +x pour le rendre exécutable. Ruby doit être installé, mais sinon, il n'y a pas d'autres dépendances.

#!/usr/bin/env Ruby

require 'digest/md5'
require 'fileutils'
require 'optparse'

def glob_from_argument(arg)
  if File.directory?(arg)
    arg + '/**/*'
  elsif File.file?(arg)
    arg
  else # it's already a glob
    arg
  end
end

# Wrap text at 80 chars. (configurable)
def wrap_text(*args)
  width = args.last.is_a?(Integer) ? args.pop : 80
  words = args.flatten.join(' ').split(' ')
  if words.any? { |Word| Word.size > width }
    raise NotImplementedError, 'cannot deal with long words'
  end

  lines = []
  line = []
  until words.empty?
    Word = words.first
    if line.size + line.map(&:size).inject(0, :+) + Word.size > width
      lines << line.join(' ')
      line = []
    else
      line << words.shift
    end
  end
  lines << line.join(' ') unless line.empty?
  lines.join("\n")
end

ALLOWED_PRINT_OPTIONS = %w(hay needle separator)

def parse_options(args)
  options = {}
  options[:print] = %w(hay needle)

  opt_parser = OptionParser.new do |opts|
    opts.banner = "Usage: #{$0} [options] HAYSTACK NEEDLES"
    opts.separator ''
    opts.separator 'Search for duplicate files (needles) in a directory (the haystack).'
    opts.separator ''
    opts.separator 'HAYSTACK should be the directory (or one file) that you want to search in.'
    opts.separator ''
    opts.separator wrap_text(
      'NEEDLES are the files you want to search for.',
      'A NEEDLE can be a file or a directory,',
      'in which case it will be recursively scanned.',
      'Note that NEEDLES may overlap the HAYSTACK.')
    opts.separator ''

    opts.on("-p", "--print PROPERTIES", Array,
      "When a match is found, print needle, or",
      "hay, or both. PROPERTIES is a comma-",
      "separated list with one or more of the",
      "words 'needle', 'hay', or 'separator'.",
      "'separator' prints an empty line.",
      "Default: 'needle,hay'") do |list|
      options[:print] = list
    end

    opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
      options[:verbose] = v
    end

    opts.on_tail("-h", "--help", "Show this message") do
      puts opts
      exit
    end
  end
  opt_parser.parse!(args)

  options[:haystack] = ARGV.shift
  options[:needles] = ARGV.shift(ARGV.size)

  raise ArgumentError, "Missing HAYSTACK" if options[:haystack].nil?
  raise ArgumentError, "Missing NEEDLES" if options[:needles].empty?
  unless options[:print].all? { |option| ALLOWED_PRINT_OPTIONS.include? option }
    raise ArgumentError, "Allowed print options are  'needle', 'hay', 'separator'"
  end

  options
rescue OptionParser::InvalidOption, ArgumentError => error
  puts error, nil, opt_parser.banner
  exit 1
end

options = parse_options(ARGV)

VERBOSE = options[:verbose]
PRINT_HAY = options[:print].include? 'hay'
PRINT_NEEDLE = options[:print].include? 'needle'
PRINT_SEPARATOR = options[:print].include? 'separator'

HAYSTACK_GLOB = glob_from_argument options[:haystack]
NEEDLES_GLOB = options[:needles].map { |arg| glob_from_argument(arg) }

def info(*strings)
  return unless VERBOSE
  STDERR.puts strings
end

def info_with_ellips(string)
  return unless VERBOSE
  STDERR.print string + '... '
end

def all_files(*globs)
  globs
    .map { |glob| Dir.glob(glob) }
    .flatten
    .map { |filename| File.expand_path(filename) } # normalize filenames
    .uniq
    .sort
    .select { |filename| File.file?(filename) }
end

def index_haystack(glob)
  all_files(glob).group_by { |filename| File.size(filename) }
end

@checksums = {}
def checksum(filename)
  @checksums[filename] ||= calculate_checksum(filename)
end

def calculate_checksum(filename)
  Digest::MD5.hexdigest(File.read(filename))
end

def find_needle(needle, haystack)
  straws = haystack[File.size(needle)] || return

  checksum_needle = calculate_checksum(needle)
  straws.detect do |straw|
    straw != needle &&
      checksum(straw) == checksum_needle &&
      FileUtils.identical?(needle, straw)
  end
end

BOLD = "\033[1m"
NORMAL = "\033[22m"

def print_found(needle, found)
  if PRINT_NEEDLE
    print BOLD if $stdout.tty?
    puts needle
    print NORMAL if $stdout.tty?
  end
  puts found if PRINT_HAY
  puts if PRINT_SEPARATOR
end

info "Searching #{HAYSTACK_GLOB} for files equal to #{NEEDLES_GLOB}."

info_with_ellips 'Indexing haystack by file size'
haystack = index_haystack(HAYSTACK_GLOB)
haystack[0] = nil # ignore empty files
info "#{haystack.size} files"

info 'Comparing...'
all_files(*NEEDLES_GLOB).each do |needle|
  info "  examining #{needle}"
  found = find_needle(needle, haystack)
  print_found(needle, found) unless found.nil?
end
1
Tijn

Il est possible d'utiliser l'option -c de md5sum sur la ligne de commande, si vous manipulez un peu son flux d'entrée. La commande suivante n'est pas récursive, elle ne fonctionnera que dans le répertoire de travail actuel. Remplacez original_file par le nom de fichier avec lequel vous souhaitez vérifier les doublons.

(hash=$(md5sum original_file) ; for f in ./* ; do echo "${hash%% *} ${f}" | if md5sum -c --status 2>/dev/null ; then echo "$f is a duplicate" ; fi ; done)

Vous pouvez remplacer la partie for f in ./* par for f in /directory/path/* pour effectuer une recherche dans un autre répertoire.

Si vous souhaitez que la recherche passe en revue dans les annuaires, vous pouvez définir l'option Shell "globstar" et utiliser deux étoiles dans le modèle donné à la boucle for:

(shopt -s globstar; hash=$(md5sum original_file); for f in ./** ; do echo "${hash%% *} ${f}" | if md5sum -c --status 2>/dev/null; then echo "$f is a duplicate"; fi; done)

Les deux versions de la commande ne généreront que le nom des fichiers en double avec l'instruction ./file is a duplicate. Ils sont tous deux encapsulés entre parenthèses afin d'éviter de définir la variable hash ou l'option globstar Shell en dehors de la commande elle-même. La commande peut utiliser d'autres algorithmes de hachage tels que sha256sum, il vous suffit de remplacer les deux occurrences de md5sum pour y parvenir.

1
Arronical