Encontre duplicatas de um arquivo por conteúdo

5

No momento, estou tentando pegar um arquivo (um arquivo de imagem como test1.jpg) e preciso ter uma lista de todas as duplicatas desse arquivo (por conteúdo). Eu tentei fdupes , mas isso não permite que um arquivo de entrada baseie suas verificações.

TLDR: preciso de uma maneira de listar todos os duplicados de um arquivo específico pelo conteúdo deles.

É mais fácil procurar uma solução por meio da linha de comando, mas os aplicativos completos também serão bons.

    
por GamrCorps 11.10.2016 / 00:01

6 respostas

8

Primeiro, encontre o hash md5 do seu arquivo:

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

(a primeira linha é o comando que você precisa executar, a segunda linha é o hash md5 desse arquivo)

Copie o hash (seria diferente no seu caso) e cole-o no próximo comando:

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

Se você quiser se interessar, combine os 2 em um único comando:

$ 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
....

Você pode usar o sha1 ou qualquer outro hash sofisticado, se quiser.

Editar

Se o caso de uso for pesquisar "vários MP4s ou iso-arquivos de vários gigabytes" para encontrar um "jpg de 4 KB" (conforme a resposta @Tijn), especificar o tamanho do arquivo aceleraria bastante as coisas.

Se o tamanho do arquivo que você está procurando for exatamente 3952 bytes (você pode ver que usando ls -l path/to/file , então este comando executaria muito mais rápido:

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

Observe o c extra após o tamanho, indicando caracteres / bytes.

    
por sмurf 11.10.2016 / 00:36
4

Use o comando diff com operadores booleanos && e ||

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

Se você quiser passar por vários arquivos no diretório específico, cd lá e usar um loop for assim:

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

Para casos recursivos, use o comando find para percorrer o diretório e todos os seus subdiretórios (lembre-se das aspas e de todas as barras apropriadas):

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
    
por Sergiy Kolodyazhnyy 11.10.2016 / 00:31
4

Você pode usar filecmp em Python

Por exemplo:

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

Imprimirá True se for igual, caso contrário, False

    
por Benny 11.10.2016 / 00:25
2

Obtenha o md5sum do arquivo em questão e salve em uma variável, por exemplo, md5 :

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

Use find para percorrer a árvore de diretórios desejada e verifique se algum arquivo tem o mesmo valor de hash, se for o caso, imprima o nome do arquivo:

find . -type f -exec sh -c '[ "$(md5sum "$1" | awk "{print \}")" = "$2" ] \
                             && echo "$1"' _ {} "$md5" \;
  • find . -type f encontra todos os arquivos no diretório atual, altere o diretório para atender sua necessidade

  • o predicado -exec executa o comando sh -c ... em todos os arquivos encontrados

  • Em sh -c , _ é um marcador para $0 , $1 é o arquivo encontrado, $2 is $md5

  • [ $(md5sum "$1"|awk "{print \}") = "$2" ] && echo "$1" imprime o nome do arquivo se o valor de hash do arquivo for o mesmo que o que estamos verificando duplicatas para

Exemplo:

% 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 \}")" = "$2" ] && echo "$1"' _ {} "$md5" \;
bar.txt
    
por heemayl 11.10.2016 / 06:25
1

@smurf e @heemayl certamente estão corretos, mas descobri que, no meu caso, era mais lento do que eu queria que fosse; Eu simplesmente tinha muitos arquivos para processar. Por isso, escrevi uma pequena ferramenta de linha de comando que, acredito, pode ajudá-lo também. ( link ; ruby; sem dependências externas)

Basicamente, meu script adia o cálculo de hash: ele só executará o cálculo quando os tamanhos dos arquivos estiverem correspondentes. Desde porque eu iria querer transmitir o conteúdo de vários MP4s multi-gigabyte ou iso-arquivos através de um algoritmo de hash quando eu sei que estou procurando por um jpg de 4 KB !? O restante do script é principalmente formatação de saída.

Edit: (obrigado @Serg) Aqui está o código-fonte de todo o script. Você deve salvá-lo em ~/bin/find-dups ou talvez /usr/local/bin/find-dups e, em seguida, usar chmod +x para torná-lo executável. Ele precisa ter o Ruby instalado, mas caso contrário não haverá outras dependências.

#!/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 = "3[1m"
NORMAL = "3[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
    
por Tijn 11.10.2016 / 11:23
1

É possível usar a opção -c de md5sum na linha de comando, se você fizer uma pequena manipulação de seu fluxo de entrada. O comando a seguir não é recursivo, ele funcionará apenas no diretório de trabalho atual. Substitua original_file pelo nome do arquivo com o qual você deseja verificar as duplicatas.

(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)

Você pode substituir a parte for f in ./* por for f in /directory/path/* para pesquisar em um diretório diferente.

Se você gostaria que a pesquisa reconsiderasse os diretórios, você pode definir a opção de shell 'globstar' e usar duas estrelas no padrão dado ao loop 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)

A versão do comando só emitirá o nome dos arquivos duplicados com a instrução ./file is a duplicate . Ambos são encapsulados em colchetes para evitar a configuração da variável hash ou a opção de shell globstar fora do próprio comando. O comando pode usar outros algoritmos de hash como sha256sum , apenas substitua as duas ocorrências de md5sum para conseguir isso.

    
por Arronical 11.10.2016 / 13:53