Самый быстрый алгоритм - увеличение производительности в 100 раз по сравнению с принятым ответом (действительно :))
Подходы в других решениях очень крутые, но они забывают о важном свойстве дубликатов файлов - они имеют одинаковый размер файла. Вычисление дорогого хэша только для файлов с одинаковым размером сэкономит огромное количество ресурсов процессора; Сравнение производительности в конце, вот объяснение.
Итерация на твердых ответах, данных @nosklo, и заимствование идеи @Raffi иметь быстрый хэш только в начале каждого файла и вычисление полного хэша только для коллизий в быстром хэше, вот шаги:
- Построить хэш-таблицу файлов, где размер файла является ключом.
- Для файлов одинакового размера создайте хеш-таблицу с хешем их первых 1024 байтов; не сталкивающиеся элементы уникальны
- Для файлов с одинаковым хешем в первых 1 килобайтах вычислите хеш для полного содержимого - файлы с совпадающими НЕ являются уникальными.
код:
#!/usr/bin/env python
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 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 = {}
hashes_on_1k = {}
hashes_full = {}
for path in paths:
for dirpath, dirnames, filenames in os.walk(path):
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)
except (OSError,):
# not accessible (permissions, etc) - pass on
continue
duplicate = hashes_by_size.get(file_size)
if duplicate:
hashes_by_size[file_size].append(full_path)
else:
hashes_by_size[file_size] = [] # create the list for this file size
hashes_by_size[file_size].append(full_path)
# For all files with the same file size, get their hash on the 1st 1024 bytes
for __, files in hashes_by_size.items():
if len(files) < 2:
continue # this file size is unique, no need to spend cpy cycles on it
for filename in files:
try:
small_hash = get_hash(filename, first_chunk_only=True)
except (OSError,):
# the file access might've changed till the exec point got here
continue
duplicate = hashes_on_1k.get(small_hash)
if duplicate:
hashes_on_1k[small_hash].append(filename)
else:
hashes_on_1k[small_hash] = [] # create the list for this 1k hash
hashes_on_1k[small_hash].append(filename)
# For all files with the hash on the 1st 1024 bytes, get their hash on the full file - collisions will be duplicates
for __, files in hashes_on_1k.items():
if len(files) < 2:
continue # this hash of fist 1k file bytes is unique, no need to spend cpy cycles on it
for filename in files:
try:
full_hash = get_hash(filename, first_chunk_only=False)
except (OSError,):
# the file access might've changed till the exec point got here
continue
duplicate = hashes_full.get(full_hash)
if duplicate:
print "Duplicate found: %s and %s" % (filename, duplicate)
else:
hashes_full[full_hash] = filename
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 МиБ, т. Е. Каталог автоматической загрузки камеры моего телефона:)
- небольшой (но полностью функциональный) процессор - 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 (пользователь + sys) 284 с , другой - 2 с ; довольно разный, да :)
С этим увеличением можно было бы перейти к SHA512 или даже больше - штраф за перфорирование будет уменьшен за счет меньшего количества необходимых вычислений.
Отрицательные:
- Больше доступа к диску, чем в других версиях - каждый файл доступен один раз для статистики размера (это дешево, но все еще является дисковым вводом-выводом), и каждый дубликат открывается дважды (для небольшого первого хеша размером 1 Кбайт и для полного содержимого хэш)
- Потребляет больше памяти из-за хранения времени выполнения хеш-таблиц