Suchen Sie nach Duplikaten einer Datei nach Inhalt


9

Ich versuche derzeit, eine Datei (eine Bilddatei wie test1.jpg) zu erstellen, und ich muss eine Liste aller Duplikate dieser Datei (nach Inhalt) haben. Ich habe es versucht, fdupesaber das erlaubt einer Eingabedatei nicht, ihre Überprüfungen zu begründen.

TLDR: Ich brauche eine Möglichkeit, alle Duplikate einer bestimmten Datei nach ihrem Inhalt aufzulisten.

Suchen Sie vorzugsweise über die Befehlszeile nach einer Lösung, aber auch vollständige Anwendungen sind in Ordnung.


2
Aktualisieren Sie die Tags nach Bedarf auf der Website.
GamrCorps


@ Gilles der Unterschied zwischen diesen beiden Fragen ist, dass diese alles mit einer Referenzdatei vergleicht, während die andere Frage ALLE Duplikate findet
GamrCorps

Antworten:


11

Suchen Sie zuerst den md5-Hash Ihrer Datei:

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

(Die erste Zeile ist der Befehl, den Sie ausführen müssen, die zweite Zeile ist der MD5-Hash dieser Datei.)

Kopieren Sie dann den Hash (in Ihrem Fall wäre er anders) und fügen Sie ihn in den nächsten Befehl ein:

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

Wenn Sie Lust haben, können Sie die 2 in einem einzigen Befehl kombinieren:

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

Sie können sha1 oder einen der anderen ausgefallenen Hashes verwenden, wenn Sie möchten.

Bearbeiten

Wenn der Anwendungsfall darin besteht, "mehrere Multi-Gigabyte-MP4s oder ISO-Dateien" zu durchsuchen, um ein "4-KB-JPG" (gemäß @ Tijn-Antwort) zu finden, würde die Angabe der Dateigröße die Dinge dramatisch beschleunigen.

Wenn die Größe der gesuchten Datei genau 3952 Byte beträgt (Sie können sehen, dass die Verwendung ls -l path/to/filedieses Befehls viel schneller ausgeführt wird:

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

Beachten Sie das Extra cnach der Größe, das Zeichen / Bytes angibt.

Wenn Sie möchten, können Sie dies in einem einzigen Befehl kombinieren:

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

Da dies nicht sicherheitsrelevant ist, können Sie einen noch schnelleren Hash wie den in cksum verwendeten crc32 verwenden. SHA1 wird nur langsamer ohne zusätzlichen Nutzen.
Calimo

crc32 hat wahrscheinlich mehrere Duplikate, wenn Sie viele Dateien haben, und die Verarbeitungskosten sind im Vergleich zum Lesen der Dateien vernachlässigbar.
Simon Richter

4

Verwenden Sie den Befehl diff mit booleschen Operatoren &&und||

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

Wenn Sie mehrere Dateien in einem bestimmten Verzeichnis durchsuchen möchten, verwenden Sie cddort eine forSchleife wie folgt:

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

Verwenden Sie in rekursiven Fällen den findBefehl, um das Verzeichnis und alle seine Unterverzeichnisse zu durchlaufen (beachten Sie die Anführungszeichen und alle entsprechenden Schrägstriche):

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


3

Holen Sie sich die md5sumbetreffende Datei und speichern Sie sie in einer Variablen, z md5.

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

Verwenden Sie finddiese Option, um den gewünschten Verzeichnisbaum zu durchlaufen und zu überprüfen, ob eine Datei denselben Hashwert hat. Wenn ja, drucken Sie den Dateinamen aus:

find . -type f -exec sh -c '[ "$(md5sum "$1" | awk "{print \$1}")" = "$2" ] \
                             && echo "$1"' _ {} "$md5" \;
  • find . -type f Findet alle Dateien im aktuellen Verzeichnis, ändern Sie das Verzeichnis entsprechend Ihren Anforderungen

  • Das -execPrädikat führt den Befehl sh -c ...für alle gefundenen Dateien aus

  • In sh -c, _ist ein Platzhalter für $0, $1ist die Datei gefunden, $2wird$md5

  • [ $(md5sum "$1"|awk "{print \$1}") = "$2" ] && echo "$1" Gibt den Dateinamen aus, wenn der Hashwert der Datei mit dem übereinstimmt, auf den wir Duplikate prüfen

Beispiel:

% 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

Es ist möglich, die -cOption md5sumin der Befehlszeile zu verwenden, wenn Sie den Eingabestream ein wenig manipulieren. Der folgende Befehl ist nicht rekursiv, sondern funktioniert nur im aktuellen Arbeitsverzeichnis. Ersetzen Sie original_filedurch den Dateinamen, mit dem Sie Duplikate vergleichen möchten.

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

Sie können das for f in ./*Teil durch ersetzen for f in /directory/path/*, um ein anderes Verzeichnis zu durchsuchen.

Wenn Sie möchten, dass die Suche durch Verzeichnisse wiederholt wird, können Sie die Shell-Option 'globstar' festlegen und zwei Sterne in dem Muster verwenden, das der for-Schleife zugewiesen wurde:

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

Beide Versionen des Befehls geben nur den Namen doppelter Dateien mit der Anweisung aus ./file is a duplicate. Sie sind beide in Klammern gekapselt, um zu vermeiden, dass die Hash-Variable oder die Globstar-Shell-Option außerhalb des Befehls selbst festgelegt werden. Der Befehl kann andere Hashing-Algorithmen verwenden, z. B. sha256sumdie beiden Vorkommen von ersetzen md5sum, um dies zu erreichen.


1

@smurf und @heemayl sind sicherlich richtig, aber ich fand heraus, dass es in meinem Fall langsamer war, als ich es wollte; Ich hatte einfach zu viele Dateien zum Verarbeiten. Deshalb habe ich ein kleines Kommandozeilen-Tool geschrieben, von dem ich denke, dass es Ihnen auch helfen könnte. ( https://github.com/tijn/dupfinder ; Ruby; keine externen Abhängigkeiten)

Grundsätzlich verschiebt mein Skript die Hash-Berechnung: Es führt die Berechnung nur durch, wenn die Dateigrößen übereinstimmen. Denn warum sollte ich den Inhalt mehrerer MP4s oder ISO-Dateien mit mehreren Gigabyte über einen Hash-Algorithmus streamen wollen, wenn ich weiß, dass ich nach einem 4-KB-JPG suche? Der Rest des Skripts besteht hauptsächlich aus Ausgabeformatierungen.

Bearbeiten: (danke @Serg) Hier ist der Quellcode des gesamten Skripts. Sie sollten es in ~/bin/find-dupsoder vielleicht sogar speichern /usr/local/bin/find-dupsund dann verwenden chmod +x, um es ausführbar zu machen. Ruby muss installiert sein, ansonsten gibt es keine anderen Abhängigkeiten.

#!/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
Während Github eine angesehene und vertrauenswürdige Site ist, wird empfohlen, Quellcode in die Antworten einzufügen, damit die Antwort autark ist. Bitte beziehen Sie sich auf diese Diskussion: meta.askubuntu.com/q/15743/295286
Sergiy Kolodyazhnyy

Scheint etwas komplexer, aber warum sollte man damit aufhören? Sie können sogar das erste X-Zeichen überprüfen, um einen Prozentsatz der Datei zu entfernen (meistens, wenn Sie nach einer großen Datei suchen).
AxelH

@AxelH Ja, und ich hatte Code, der genau das tat, aber es stellte sich heraus, dass er langsamer war als ohne diese Funktionalität. Ich bin damit einverstanden, dass es für große Dateien sehr nützlich sein kann. Vielleicht sollte ich es wieder hinzufügen und optional mit einem Befehlszeilenflag aktivieren. (Wenn jemand anderes es schreiben möchte, wäre eine Pull-Anfrage fantastisch.) Nun, da ich darüber nachdenke, können Sie sogar auswählen, welche Strategien in welcher Reihenfolge verwendet werden sollen. :-)
Tijn
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.