Поиск дубликатов файлов через hashlib?

Я знаю, что этот вопрос задавался раньше, и я видел некоторые ответы, но этот вопрос больше касается моего кода и лучшего способа выполнения этой задачи.

Я хочу просканировать каталог и посмотреть, есть ли в нем дубликаты (путем проверки хэшей MD5). Ниже приведен мой код:

import sys
import os
import hashlib

fileSliceLimitation = 5000000 #bytes

# if the file is big, slice trick to avoid to load the whole file into RAM
def getFileHashMD5(filename):
     retval = 0;
     filesize = os.path.getsize(filename)

     if filesize > fileSliceLimitation:
        with open(filename, 'rb') as fh:
          m = hashlib.md5()
          while True:
            data = fh.read(8192)
            if not data:
                break
            m.update(data)
          retval = m.hexdigest()

     else:
        retval = hashlib.md5(open(filename, 'rb').read()).hexdigest()

     return retval

searchdirpath = raw_input("Type directory you wish to search: ")
print ""
print ""    
text_file = open('outPut.txt', 'w')

for dirname, dirnames, filenames in os.walk(searchdirpath):
    # print path to all filenames.
    for filename in filenames:
        fullname = os.path.join(dirname, filename)
        h_md5 = getFileHashMD5 (fullname)
        print h_md5 + " " + fullname
        text_file.write("\n" + h_md5 + " " + fullname)   

# close txt file
text_file.close()


print "\n\n\nReading outPut:"
text_file = open('outPut.txt', 'r')

myListOfHashes = text_file.read()

if h_md5 in myListOfHashes:
    print 'Match: ' + " " + fullname

Это дает мне следующий результат:

Please type in directory you wish to search using above syntax: /Users/bubble/Desktop/aF

033808bb457f622b05096c2f7699857v /Users/bubble/Desktop/aF/.DS_Store
409d8c1727960fddb7c8b915a76ebd35 /Users/bubble/Desktop/aF/script copy.py
409d8c1727960fddb7c8b915a76ebd25 /Users/bubble/Desktop/aF/script.py
e9289295caefef66eaf3a4dffc4fe11c /Users/bubble/Desktop/aF/simpsons.mov

Reading outPut:
Match:  /Users/bubble/Desktop/aF/simpsons.mov

Моя идея была:

1) Сканировать каталог 2) Записать хэши MD5 + имя файла в текстовый файл 3) Открыть текстовый файл только для чтения 4) Сканировать каталог СНОВА и проверить текстовый файл...

Я вижу, что это не очень хороший способ сделать это И это не работает. «Сопоставление» просто распечатывает самый последний файл, который был обработан.

Как я могу заставить этот скрипт находить дубликаты? Может ли кто-нибудь сказать мне лучший/более простой способ выполнения этой задачи.

Большое спасибо за любую помощь. Извините, это длинный пост.


person BubbleMonster    schedule 10.09.2013    source источник
comment
На самом деле сравнение MD5 на самом деле медленнее, чем сравнение файлов побайтно (1 цикл на каждый байт файлов). Я почти уверен, что MD5 занимает более одного цикла на байт. Я предлагаю вам просто прочитать фиксированное количество байтов из файла и продолжить сравнение, так будет проще и быстрее.   -  person Ramchandra Apte    schedule 10.09.2013
comment
@RamchandraApte, но сравнение каждого файла с каждым другим файлом происходит медленно. Использование MD5 может помочь избежать этого.   -  person senderle    schedule 10.09.2013
comment
@senderle Вы правы. Но может быть быстрее просто сравнить первые несколько и последние несколько байтов каждого файла, а затем, если они совпадают, сравнить весь файл.   -  person Ramchandra Apte    schedule 10.09.2013
comment
Чтобы действительно увеличить скорость, сначала сгруппируйте файлы по размеру, а затем сравнивайте только внутри группы. Если нет какой-либо странной причины, по которой файлы имеют одинаковый размер, большинство файлов будут иметь уникальные размеры файлов и никогда не будут нуждаться в сканировании.   -  person tdelaney    schedule 10.09.2013


Ответы (3)


Очевидным инструментом для выявления дубликатов является хэш-таблица. Если вы не работаете с очень большим количеством файлов, вы можете сделать что-то вроде этого:

from collections import defaultdict

file_dict = defaultdict(list)
for filename in files:
    file_dict[get_file_hash(filename)].append(filename)

В конце этого процесса file_dict будет содержать список для каждого уникального хэша; когда два файла имеют одинаковый хеш, они оба появятся в списке для этого хэша. Затем отфильтруйте dict в поисках списков значений длиннее 1 и сравните файлы, чтобы убедиться, что они одинаковы - что-то вроде этого:

for duplicates in file_dict.values():   # file_dict.itervalues() in Python 2
    if len(duplicates) > 1:
        # double-check reported duplicates and generate output

Или это:

duplicates = [files for files in file_dict.values() if len(files) > 1]

get_file_hash может использовать MD5; или он может просто получить первый и последний байты файла, как предложил Рамчандра Апте в комментариях выше; или он может просто использовать размеры файлов, как tdelaney предложил в комментариях выше. Однако каждая из двух последних стратегий чаще приводит к ложным срабатываниям. Вы можете комбинировать их, чтобы уменьшить количество ложных срабатываний.

Если вы работаете с очень большим количеством файлов, вы можете использовать более сложную структуру данных, такую ​​как Фильтр Блума.

person senderle    schedule 10.09.2013
comment
Нужно ли мне добавлять это в мой код? Или переписать мой код, чтобы он заработал? - person BubbleMonster; 10.09.2013
comment
@BubbleMonster, вышеприведенное было бы близко к замене блока, начинающегося с for filename in filenames: в вашем коде. (Конечно, вам придется изменить files на filenames и поместить оператор from... import... в начало вашего модуля.) Затем вам придется написать немного больше кода, чтобы отфильтровать списки из одного элемента и протестировать оставшиеся. списки, чтобы убедиться, что файлы действительно являются дубликатами. Хотя эта часть должна быть довольно простой. - person senderle; 10.09.2013
comment
Почему другие стратегии дают ложные срабатывания? Я проверил код, и вы правы. Он помечает множество ложных срабатываний, но почему именно? Спасибо. - person BubbleMonster; 12.09.2013
comment
@BubbleMonster хорошо, это зависит от характера файлов, но в целом есть большая вероятность, что два файла будут иметь одинаковый размер, начальное и конечное содержимое, особенно если файлы меньше, поскольку в них меньше возможных файлов. тот случай; или если они все одного типа, то в этом случае все они могут начинаться одинаково. С другой стороны, MD5 почти никогда (но очень редко) генерирует ложные срабатывания, потому что это криптографический хеш — он специально разработан для получения результатов, равномерно распределенных по всем возможным значениям хэша. - person senderle; 12.09.2013
comment
Но даже MD5 генерирует ложные срабатывания, потому что хэш-значений ограничено — ровно столько, сколько может быть представлено 128 битами. Таким образом, если вы перепробуете все возможные 129-битные файлы, по крайней мере половина из них будет хеширована с тем же значением, что и другой, потому что возможных файлов больше, чем хэшей. - person senderle; 12.09.2013
comment
Спасибо, что прояснили это. Мне удалось реализовать ваше предложение, поэтому теперь, когда я сканирую папку, я получаю словарь с MD5 в качестве ключа и именем файла в качестве значения. Как я могу заставить скрипт распечатать, что он нашел 2 (или более) одинаковых хэша? Кроме того, в моей книге по Python говорится: [quote] «В словаре ключ должен быть хешируемым объектом. Словарь не может содержать повторяющиеся ключи.'[конец цитаты] — Так что плохо, что я на самом деле пытаюсь увидеть, есть ли у ключа повторяющиеся хэши? - person BubbleMonster; 12.09.2013
comment
Я не совсем понимаю ваш последний вопрос. Ключ является хешем. Вы не пытаетесь увидеть, есть ли у ключа повторяющиеся хэши, вы пытаетесь увидеть, есть ли у файлов повторяющиеся хэши. Все файлы с одинаковым хэшем хранятся в списке, который является значением, сопоставленным с ключом — общим хэшем. Смотрите мои правки для получения подробной информации о том, как фильтровать словарь. - person senderle; 12.09.2013

У @senderle есть отличный ответ, но, поскольку он упомянул, что мое решение будет давать ложные срабатывания, я решил, что перчатка закрыта, и мне лучше показать код. Я уменьшил вашу функцию md5 (она всегда должна использовать регистр «fileSliceLimitation» и должна быть менее скупа на свой входной буфер), а затем предварительно отфильтровала по размеру перед выполнением md5s.

import sys
import os
import hashlib
from collections import defaultdict

searchdirpath = sys.argv[1]

size_map = defaultdict(list)

def getFileHashMD5(filename):
    m = hashlib.md5()
    with open(filename, 'rb', 1024*1024) as fh:
          while True:
            data = fh.read(1024*1024)
            if not data:
                break
            m.update(data)
    return m.hexdigest()

# group files by size
for dirname, dirnames, filenames in os.walk(searchdirpath):
    for filename in filenames:
        fullname = os.path.join(dirname, filename)
        size_map[os.stat(fullname).st_size].append(fullname)

# scan files of same size
for fullnames in size_map.itervalues():
    if len(fullnames) > 0:
        hash_map = defaultdict(list)
        for fullname in fullnames:
            hash_map[getFileHashMD5(fullname)].append(fullname)
        for fullnames in hash_map.itervalues():
            if len(fullnames) > 1:
                print "duplicates:"
                for fullname in fullnames:
                    print "   ", fullname

(РЕДАКТИРОВАТЬ)

Было несколько вопросов об этой реализации, на которые я постараюсь ответить здесь:

1) почему размер (1024*1024) не "5000000"

Ваш исходный код читается с шагом 8192 (8 КиБ), что очень мало для современных систем. Вы, вероятно, получите лучшую производительность, захватив больше сразу. 1024*1024 — это 1048576 (1 МБ) байт, и это всего лишь предположение о разумном числе. Что касается того, почему я написал это таким странным образом, 1000 (десятичный килобайт) любят люди, а 1024 (двоичный кибибайт) любят компьютеры и файловые системы. У меня есть привычка писать some_number*1024, поэтому легко увидеть, что я имею в виду приращение в 1 КиБ. 5000000 — это тоже разумное число, но вы должны учитывать 5*1024*1024 (то есть 5 МБ), чтобы получить что-то, хорошо выровненное для файловой системы.

2) что именно делает этот бит: size_map = defaultdict(list)

Он создает «defaultdict», который добавляет функциональность к обычному объекту dict. Обычный dict вызывает исключение KeyError, когда он индексируется по несуществующему ключу. defaultdict создает значение по умолчанию и вместо этого добавляет эту пару ключ/значение в словарь. В нашем случае size_map[some_size] говорит: «Дайте мне список файлов некоторого_размера и создайте новый пустой список, если у вас его нет».

size_map[os.stat(fullname).st_size].append(fullname). Это разбивается на:

stat = os.stat(fullname)
size = stat.st_size
filelist = size_map[size]    # this is the same as:
                             #    if size not in size_map:
                             #        size_map[size] = list()
                             #    filelist = size_map[size]
filelist.append(fullname)

3) sys.argv[1] Я предполагаю, что sys.argv[1] просто заставляет работать аргумент python py.py 'filepath' (где filepath - это argv[1]?

Да, когда вы вызываете скрипт Python, sys.argv[0] — это имя скрипта, а sys.argv[1:] (arg 1 и последующие) — любые дополнительные аргументы, заданные в командной строке. Я использовал sys.argv[1] как быстрый способ протестировать сценарий, когда писал его, и вы должны изменить его в соответствии со своими потребностями.

person tdelaney    schedule 10.09.2013
comment
Спасибо за код. Я только что попытался запустить его, и он сказал: Файл py.py, строка 6, в ‹module› searchdirpath = sys.argv[1] IndexError: индекс списка вне допустимого диапазона. Вы знаете, как это исправить? Должен ли я оставить в моем searchdirpath raw_input? - person BubbleMonster; 10.09.2013
comment
@BubbleMonster - я написал это, предполагая, что оно будет называться как py.py path\to\directory (Windows) или python py.py path/to/directory (linux). Вы можете изменить его в соответствии с вашими потребностями. - person tdelaney; 10.09.2013

Первое, что вам нужно сделать, это сохранить h_md5 в список, когда вы будете перебирать свои файлы. Что-то вроде:

h_md5=[]

прежде чем вы перебираете свой каталог. И

h_md5.append(getFileHashMD5(fullname))

внутри вашего цикла. Теперь у вас есть список хэшей для сравнения с вашим выходным файлом, а не просто с последним, который вы сделали в своем цикле.

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

редактировать: ответ выше @senderle - гораздо лучший способ сделать это, если вы хотите изменить свой код.

person derricw    schedule 10.09.2013
comment
Спасибо за ответ. Я думал, что это будет что-то вроде этого. Я получаю следующую ошибку, когда добавляю ваш код: AttributeError: объект 'str' не имеет атрибута 'append' - person BubbleMonster; 10.09.2013
comment
@BubbleMonster, да, это, вероятно, потому, что вы перезаписываете свой список строкой. По моему предложению, переименуйте h_md5 в h_md5List или что-то в этом роде, и это должно решить проблему. - person derricw; 10.09.2013