Поиск повторяющихся файлов и их удаление

Я пишу программу Python для поиска и удаления повторяющихся файлов из папки.

У меня есть несколько копий файлов mp3 и некоторых других файлов. Я использую алгоритм sh1.

Как мне найти эти повторяющиеся файлы и удалить их?


person sanorita    schedule 14.04.2009    source источник
comment
Для всех, кто интересуется, я написал скромную программу с графическим интерфейсом пользователя, которая сравнивает на основе размера файла и двоичных фрагментов: github. ru / nav9 / duplicateFileFinder   -  person Nav    schedule 15.03.2021


Ответы (8)


Самый быстрый алгоритм - 100-кратное увеличение производительности по сравнению с принятым ответом (правда :))

Подходы в других решениях очень крутые, но они забывают о важном свойстве дубликатов файлов - они имеют одинаковый размер файла. Расчет дорогостоящего хэша только для файлов с одинаковым размером сэкономит огромное количество ресурсов процессора; сравнение производительности в конце, вот объяснение.

Итерируя твердые ответы, данные @nosklo, и заимствуя идею @Raffi, чтобы иметь быстрый хеш только для начала каждого файла и вычислять полный только при коллизиях в быстром хеше, вот шаги:

  1. Создайте хеш-таблицу файлов, в которой размер файла является ключевым.
  2. Для файлов одинакового размера создайте хеш-таблицу с хеш-кодом их первых 1024 байтов; не конфликтующие элементы уникальны
  3. Для файлов с одинаковым хешем в первых 1 Кбайтах вычислите хеш для всего содержимого - файлы с совпадающими значениями НЕ уникальны.

Код:

#!/usr/bin/env python
# if running in py3, change the shebang, drop the next import for readability (it does no harm in py3)
from __future__ import print_function   # py2 compatibility
from collections import defaultdict
import hashlib
import os
import sys


def chunk_reader(fobj, chunk_size=1024):
    """Generator that reads a file in chunks of bytes"""
    while True:
        chunk = fobj.read(chunk_size)
        if not chunk:
            return
        yield chunk


def get_hash(filename, first_chunk_only=False, hash=hashlib.sha1):
    hashobj = hash()
    file_object = open(filename, 'rb')

    if first_chunk_only:
        hashobj.update(file_object.read(1024))
    else:
        for chunk in chunk_reader(file_object):
            hashobj.update(chunk)
    hashed = hashobj.digest()

    file_object.close()
    return hashed


def check_for_duplicates(paths, hash=hashlib.sha1):
    hashes_by_size = defaultdict(list)  # dict of size_in_bytes: [full_path_to_file1, full_path_to_file2, ]
    hashes_on_1k = defaultdict(list)  # dict of (hash1k, size_in_bytes): [full_path_to_file1, full_path_to_file2, ]
    hashes_full = {}   # dict of full_file_hash: full_path_to_file_string

    for path in paths:
        for dirpath, dirnames, filenames in os.walk(path):
            # get all files that have the same size - they are the collision candidates
            for filename in filenames:
                full_path = os.path.join(dirpath, filename)
                try:
                    # if the target is a symlink (soft one), this will 
                    # dereference it - change the value to the actual target file
                    full_path = os.path.realpath(full_path)
                    file_size = os.path.getsize(full_path)
                    hashes_by_size[file_size].append(full_path)
                except (OSError,):
                    # not accessible (permissions, etc) - pass on
                    continue


    # For all files with the same file size, get their hash on the 1st 1024 bytes only
    for size_in_bytes, files in hashes_by_size.items():
        if len(files) < 2:
            continue    # this file size is unique, no need to spend CPU cycles on it

        for filename in files:
            try:
                small_hash = get_hash(filename, first_chunk_only=True)
                # the key is the hash on the first 1024 bytes plus the size - to
                # avoid collisions on equal hashes in the first part of the file
                # credits to @Futal for the optimization
                hashes_on_1k[(small_hash, size_in_bytes)].append(filename)
            except (OSError,):
                # the file access might've changed till the exec point got here 
                continue

    # For all files with the hash on the 1st 1024 bytes, get their hash on the full file - collisions will be duplicates
    for __, files_list in hashes_on_1k.items():
        if len(files_list) < 2:
            continue    # this hash of fist 1k file bytes is unique, no need to spend cpy cycles on it

        for filename in files_list:
            try: 
                full_hash = get_hash(filename, first_chunk_only=False)
                duplicate = hashes_full.get(full_hash)
                if duplicate:
                    print("Duplicate found: {} and {}".format(filename, duplicate))
                else:
                    hashes_full[full_hash] = filename
            except (OSError,):
                # the file access might've changed till the exec point got here 
                continue


if __name__ == "__main__":
    if sys.argv[1:]:
        check_for_duplicates(sys.argv[1:])
    else:
        print("Please pass the paths to check as parameters to the script")

И вот самое интересное - сравнение производительности.

Исходный уровень -

  • каталог с 1047 файлами, 32 mp4, 1015 - jpg, общий размер - 5445.998 MiB - т.е. каталог автозагрузки камеры моего телефона :)
  • небольшой (но полнофункциональный) процессор - 1600 BogoMIPS, 1,2 ГГц 32L1 + 256L2 Кбайт кеша, / proc / cpuinfo:

Процессор: Feroceon 88FR131 rev 1 (v5l) BogoMIPS: 1599.07

(то есть мой бюджетный NAS :), работающий на Python 2.7.11.

Итак, вывод очень удобного решения @nosklo:

root@NAS:InstantUpload# time ~/scripts/checkDuplicates.py 
Duplicate found: ./IMG_20151231_143053 (2).jpg and ./IMG_20151231_143053.jpg
Duplicate found: ./IMG_20151125_233019 (2).jpg and ./IMG_20151125_233019.jpg
Duplicate found: ./IMG_20160204_150311.jpg and ./IMG_20160204_150311 (2).jpg
Duplicate found: ./IMG_20160216_074620 (2).jpg and ./IMG_20160216_074620.jpg

real    5m44.198s
user    4m44.550s
sys     0m33.530s

И вот версия с фильтром при проверке размера, затем небольшими хешами и, наконец, полным хешем, если обнаружены коллизии:

root@NAS:InstantUpload# time ~/scripts/checkDuplicatesSmallHash.py . "/i-data/51608399/photo/Todor phone"
Duplicate found: ./IMG_20160216_074620 (2).jpg and ./IMG_20160216_074620.jpg
Duplicate found: ./IMG_20160204_150311.jpg and ./IMG_20160204_150311 (2).jpg
Duplicate found: ./IMG_20151231_143053 (2).jpg and ./IMG_20151231_143053.jpg
Duplicate found: ./IMG_20151125_233019 (2).jpg and ./IMG_20151125_233019.jpg

real    0m1.398s
user    0m1.200s
sys     0m0.080s

Обе версии запускались по 3 раза каждая, чтобы получить среднее необходимое время.

Итак, v1 - это (user + sys) 284s, другой - 2s; довольно большая разница, да :) При таком увеличении можно было бы перейти на SHA512, или даже лучше - штраф за производительность будет смягчен меньшим количеством необходимых вычислений.

Минус:

  • Больше доступа к диску, чем в других версиях - к каждому файлу обращаются один раз для статистики размера (это дешево, но по-прежнему является дисковым вводом-выводом), и каждый дубликат открывается дважды (для небольшого первого хэша размером 1 Кбайт и для хэша полного содержимого)
  • Потребляет больше памяти из-за хранения среды выполнения хэш-таблиц
person Todor Minakov    schedule 20.03.2016
comment
@TodorMinakov - Добро пожаловать. Я использую os.readlink(), только если это символическая ссылка. Есть разные варианты использования этого кода, но в моем случае я не хочу, чтобы на файл указывала символическая ссылка на «count». Насколько я помню, обработка OSError может потребоваться, когда у пользователя есть права на чтение для каталога (и, следовательно, размера файла), но не для самого файла. Но мы вышли за рамки того, где можно было бы использовать DVCS. :-) - person bitinerant; 10.01.2019
comment
@TodorMinakov Мне просто любопытно, сколько ложных срабатываний от проверки размера файла до короткой проверки хеша и сколько от короткого хеша до полного хеша? тиа - person gboffi; 16.08.2019
comment
Вы можете написать hashes_by_size.setdefault(file_size, []).append(filename) вместо проверки вспомогательной переменной (то же самое относится и к другой hashes_by_x) и (но это действительно придирки ...) вы напишите filename (без подчеркивания) и file_size (с подчеркиванием). Это прекрасно, тем не менее я понимаю ... Чао - person gboffi; 16.08.2019
comment
Я обновил этот скрипт для python3, добавив небольшие улучшения кода: gist.github.com/tfeldmann/fc8752e6630d > - person tfeldmann; 27.11.2019
comment
Это неправда и плохой подход, особенно если вы создаете наборы данных изображений. Иногда у вас есть одно и то же изображение, но оно обрезано. Итак, хотя изображение такое же, размер - нет. Я понимаю, что другой метод дорогостоящий, но, поскольку вы, скорее всего, сделаете это только один раз, это не проблема. - person oo92; 19.03.2020
comment
Я тебя не понимаю @OnukOzbek? Этот сценарий ищет идентичные побайтовые файлы. Наличие второй обрезанной версии изображения не идентично - оно может выглядеть когнитивно, имея тот же предмет и структуру для человеческого глаза, но даже в этом случае - это другое изображение, с меньшим количеством информации (обрезанные данные) и другой и не идентичный файл. - person Todor Minakov; 19.03.2020
comment
Вы увеличиваете риск ложных срабатываний, потому что словари hashes_on_1k и hashes_full сравнивают хэши для файлов, которые могут иметь разные размеры. В качестве ключей следует использовать кортежи (размер, хэш), а не только хеш. Если файлов миллионы, риск немалый. - person Futal; 27.03.2020
comment
@Futal, это действительно хорошая оптимизация, на огромном наборе файлов она действительно уменьшит количество вычислений для hashes_on_1k элементов. - person Todor Minakov; 27.03.2020
comment
не работает, при обнаружении дубликатов файлы не удаляются - person prismspecs; 14.01.2021
comment
Напротив, работает :); но - он не удаляет файлы, это никогда не было намерением - проверьте код, он просто печатает их имена. Я не добавлял его специально - в моем случае я хочу посмотреть, какой из дубликатов на самом деле оставить. Если вы хотите сделать это автоматически - просто добавьте os.remove(duplicate) сразу после печати, на свой страх и риск. @prismpecs - person Todor Minakov; 14.01.2021
comment
заголовок сообщения Поиск дубликатов файлов и их удаление спасибо за обновленный код - person prismspecs; 14.01.2021

Версия рекурсивных папок:

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

import sys
import os
import hashlib

def chunk_reader(fobj, chunk_size=1024):
    """Generator that reads a file in chunks of bytes"""
    while True:
        chunk = fobj.read(chunk_size)
        if not chunk:
            return
        yield chunk

def check_for_duplicates(paths, hash=hashlib.sha1):
    hashes = {}
    for path in paths:
        for dirpath, dirnames, filenames in os.walk(path):
            for filename in filenames:
                full_path = os.path.join(dirpath, filename)
                hashobj = hash()
                for chunk in chunk_reader(open(full_path, 'rb')):
                    hashobj.update(chunk)
                file_id = (hashobj.digest(), os.path.getsize(full_path))
                duplicate = hashes.get(file_id, None)
                if duplicate:
                    print "Duplicate found: %s and %s" % (full_path, duplicate)
                else:
                    hashes[file_id] = full_path

if sys.argv[1:]:
    check_for_duplicates(sys.argv[1:])
else:
    print "Please pass the paths to check as parameters to the script"
person nosklo    schedule 14.04.2009
comment
@Jakob Bowyer: Конечно, реализация итеративна. Под рекурсивными папками я подразумеваю рекурсию всего дерева папок. - person nosklo; 04.08.2011
comment
Пожалуйста, я новичок в Python, как мне пройти по моим путям ...? - person X-Black...; 06.06.2018
comment
@ X-Black ... передать его как параметры командной строки. Пример: откройте командную строку, перейдите в папку и введите: python myscript.py c:\path1 c:\path2 - person nosklo; 06.06.2018
comment
Воскрешая этот старый пост. Это отличный сценарий, но он терпит неудачу, если сталкивается с файлом, к которому у него нет разрешения (например, pagefile.sys). Как можно изменить строку for chunk in ... для обработки этой ошибки? - person Michael; 09.07.2018
comment
@Michael Используйте try / except; try: for chunk..... except OSError: print('Skipping file') - person nosklo; 09.07.2018
comment
Пожалуйста, не могли бы вы объяснить назначение функции chunk_reader? Вы не хотите загружать в память большие файлы? - person Alex; 04.01.2019
comment
@Alex: да, у меня большие файлы, и загрузка только одного из них полностью превысила бы доступную мне память. - person nosklo; 04.01.2019
comment
Блестяще. Кроме того, проверка типа файла не уменьшит время. - person user96265; 19.01.2020

def remove_duplicates(dir):
    unique = []
    for filename in os.listdir(dir):
        if os.path.isfile(filename):
            filehash = md5.md5(file(filename).read()).hexdigest()
            if filehash not in unique: 
                unique.append(filehash)
            else: 
                os.remove(filename)

//редактировать:

Для MP3 вас также может заинтересовать эта тема Обнаружить повторяющиеся файлы MP3 с разным битрейтом и / или разными тегами ID3?

person zalew    schedule 14.04.2009
comment
Для повышения производительности вам, вероятно, следует изменить unique на набор (хотя, вероятно, это не будет большим фактором, если не будет много маленьких файлов). Кроме того, ваш код не сработает, если в каталоге dir есть каталог. Перед их обработкой проверьте os.path.isfile (). - person Brian; 14.04.2009
comment
да, этот код больше похож на основу. Я добавил isfile, как вы предложили. - person zalew; 15.04.2009
comment
Предупреждение: я не знаю почему, но ваш код MD5 сгенерировал один и тот же хеш для многих файлов , которые не были дублированы ... Когда я заменил на hashlib.md5(open(filename, 'rb').read()).hexdigest(), он работал правильно. - person Basj; 15.11.2016

Некоторое время назад я написал его на Python - вы можете его использовать.

import sys
import os
import hashlib

check_path = (lambda filepath, hashes, p = sys.stdout.write:
        (lambda hash = hashlib.sha1 (file (filepath).read ()).hexdigest ():
                ((hash in hashes) and (p ('DUPLICATE FILE\n'
                                          '   %s\n'
                                          'of %s\n' % (filepath, hashes[hash])))
                 or hashes.setdefault (hash, filepath)))())

scan = (lambda dirpath, hashes = {}: 
                map (lambda (root, dirs, files):
                        map (lambda filename: check_path (os.path.join (root, filename), hashes), files), os.walk (dirpath)))

((len (sys.argv) > 1) and scan (sys.argv[1]))
person Community    schedule 14.04.2009
comment
Я не могу следить за тем, что там происходит. Если у вас будет возможность, не могли бы вы немного объяснить, что происходит? - person tgray; 14.04.2009

Более быстрый алгоритм

В случае, если необходимо проанализировать много файлов «большого размера» (изображения, файлы в формате mp3, pdf), было бы интересно / быстрее иметь следующий алгоритм сравнения:

  1. первое быстрое хеширование выполняется для первых N байтов файла (скажем, 1 КБ). Этот хеш без сомнения скажет, если файлы разные, но не скажет, если два файла точно такие же (точность хэша, ограниченные данные, считанные с диска)

  2. второй, более медленный, хэш, который более точен и выполняется для всего содержимого файла, если на первом этапе возникает коллизия

Вот реализация этого алгоритма:

import hashlib
def Checksum(current_file_name, check_type = 'sha512', first_block = False):
  """Computes the hash for the given file. If first_block is True,
  only the first block of size size_block is hashed."""
  size_block = 1024 * 1024 # The first N bytes (1KB)

  d = {'sha1' : hashlib.sha1, 'md5': hashlib.md5, 'sha512': hashlib.sha512}

  if(not d.has_key(check_type)):
    raise Exception("Unknown checksum method")

  file_size = os.stat(current_file_name)[stat.ST_SIZE]
  with file(current_file_name, 'rb') as f:
    key = d[check_type].__call__()
    while True:
      s = f.read(size_block)
      key.update(s)
      file_size -= size_block
      if(len(s) < size_block or first_block):
        break
  return key.hexdigest().upper()

def find_duplicates(files):
  """Find duplicates among a set of files.
  The implementation uses two types of hashes:
  - A small and fast one one the first block of the file (first 1KB), 
  - and in case of collision a complete hash on the file. The complete hash 
  is not computed twice.
  It flushes the files that seems to have the same content 
  (according to the hash method) at the end.
  """

  print 'Analyzing', len(files), 'files'

  # this dictionary will receive small hashes
  d = {}
  # this dictionary will receive full hashes. It is filled
  # only in case of collision on the small hash (contains at least two 
  # elements)
  duplicates = {}

  for f in files:

    # small hash to be fast
    check = Checksum(f, first_block = True, check_type = 'sha1')

    if(not d.has_key(check)):
      # d[check] is a list of files that have the same small hash
      d[check] = [(f, None)]
    else:
      l = d[check]
      l.append((f, None))

      for index, (ff, checkfull) in enumerate(l):

        if(checkfull is None):
          # computes the full hash in case of collision
          checkfull = Checksum(ff, first_block = False)
          l[index] = (ff, checkfull)

          # for each new full hash computed, check if their is 
          # a collision in the duplicate dictionary. 
          if(not duplicates.has_key(checkfull)):
            duplicates[checkfull] = [ff]
          else:
            duplicates[checkfull].append(ff)

  # prints the detected duplicates
  if(len(duplicates) != 0):
    print
    print "The following files have the same sha512 hash"

    for h, lf in duplicates.items():
      if(len(lf)==1):
        continue
      print 'Hash value', h
      for f in lf:
        print '\t', f.encode('unicode_escape') if \
          type(f) is types.UnicodeType else f
  return duplicates

Функция find_duplicates принимает список файлов. Таким образом, также можно сравнить два каталога (например, для лучшей синхронизации их содержимого). Ниже приведен пример функции, создающей список файлов с указанным расширением и избегающей входа в некоторые каталоги:

def getFiles(_path, extensions = ['.png'], 
             subdirs = False, avoid_directories = None):
  """Returns the list of files in the path :'_path', 
     of extension in 'extensions'. 'subdir' indicates if 
     the search should also be performed in the subdirectories. 
     If extensions = [] or None, all files are returned.
     avoid_directories: if set, do not parse subdirectories that 
     match any element of avoid_directories."""

  l = []
  extensions = [p.lower() for p in extensions] if not extensions is None \
    else None
  for root, dirs, files in os.walk(_path, topdown=True):

    for name in files:
      if(extensions is None or len(extensions) == 0 or \
         os.path.splitext(name)[1].lower() in extensions):
        l.append(os.path.join(root, name))

    if(not subdirs):
      while(len(dirs) > 0):
        dirs.pop()
    elif(not avoid_directories is None):
      for d in avoid_directories:
        if(d in dirs): dirs.remove(d)

  return l    

Этот метод удобен для того, чтобы, например, не анализировать .svn пути, что наверняка вызовет конфликт файлов в find_duplicates.

Отзывы приветствуются.

person Raffi    schedule 24.10.2012

У @ IanLee1521 есть хорошее решение здесь. Это очень эффективно, потому что сначала проверяет дубликат на основе размера файла.

#! /usr/bin/env python

# Originally taken from:
# http://www.pythoncentral.io/finding-duplicate-files-with-python/
# Original Auther: Andres Torres

# Adapted to only compute the md5sum of files with the same size

import argparse
import os
import sys
import hashlib


def find_duplicates(folders):
    """
    Takes in an iterable of folders and prints & returns the duplicate files
    """
    dup_size = {}
    for i in folders:
        # Iterate the folders given
        if os.path.exists(i):
            # Find the duplicated files and append them to dup_size
            join_dicts(dup_size, find_duplicate_size(i))
        else:
            print('%s is not a valid path, please verify' % i)
            return {}

    print('Comparing files with the same size...')
    dups = {}
    for dup_list in dup_size.values():
        if len(dup_list) > 1:
            join_dicts(dups, find_duplicate_hash(dup_list))
    print_results(dups)
    return dups


def find_duplicate_size(parent_dir):
    # Dups in format {hash:[names]}
    dups = {}
    for dirName, subdirs, fileList in os.walk(parent_dir):
        print('Scanning %s...' % dirName)
        for filename in fileList:
            # Get the path to the file
            path = os.path.join(dirName, filename)
            # Check to make sure the path is valid.
            if not os.path.exists(path):
                continue
            # Calculate sizes
            file_size = os.path.getsize(path)
            # Add or append the file path
            if file_size in dups:
                dups[file_size].append(path)
            else:
                dups[file_size] = [path]
    return dups


def find_duplicate_hash(file_list):
    print('Comparing: ')
    for filename in file_list:
        print('    {}'.format(filename))
    dups = {}
    for path in file_list:
        file_hash = hashfile(path)
        if file_hash in dups:
            dups[file_hash].append(path)
        else:
            dups[file_hash] = [path]
    return dups


# Joins two dictionaries
def join_dicts(dict1, dict2):
    for key in dict2.keys():
        if key in dict1:
            dict1[key] = dict1[key] + dict2[key]
        else:
            dict1[key] = dict2[key]


def hashfile(path, blocksize=65536):
    afile = open(path, 'rb')
    hasher = hashlib.md5()
    buf = afile.read(blocksize)
    while len(buf) > 0:
        hasher.update(buf)
        buf = afile.read(blocksize)
    afile.close()
    return hasher.hexdigest()


def print_results(dict1):
    results = list(filter(lambda x: len(x) > 1, dict1.values()))
    if len(results) > 0:
        print('Duplicates Found:')
        print(
            'The following files are identical. The name could differ, but the'
            ' content is identical'
            )
        print('___________________')
        for result in results:
            for subresult in result:
                print('\t\t%s' % subresult)
            print('___________________')

    else:
        print('No duplicate files found.')


def main():
    parser = argparse.ArgumentParser(description='Find duplicate files')
    parser.add_argument(
        'folders', metavar='dir', type=str, nargs='+',
        help='A directory to parse for duplicates',
        )
    args = parser.parse_args()

    find_duplicates(args.folders)


if __name__ == '__main__':
    sys.exit(main())
person qun    schedule 27.04.2016

В целях безопасности (их автоматическое удаление может быть опасным, если что-то пойдет не так!), Вот что я использую, основываясь на ответе @zalew.

Также обратите внимание, что код суммы md5 немного отличается от кода @zalew, потому что его код сгенерировал слишком много неправильных повторяющихся файлов (поэтому я сказал, что их автоматическое удаление опасно!).

import hashlib, os
unique = dict()
for filename in os.listdir('.'):
    if os.path.isfile(filename):
        filehash = hashlib.md5(open(filename, 'rb').read()).hexdigest()

        if filehash not in unique: 
            unique[filehash] = filename
        else:
            print filename + ' is a duplicate of ' + unique[filehash]
person Basj    schedule 15.11.2016

person    schedule
comment
как мне установить директорию в свой собственный директорию ...? - person X-Black...; 05.06.2018