Генерация контрольной суммы MD5 файла

350

Есть ли простой способ генерирования (и проверки) контрольных сумм MD5 списка файлов в Python? (У меня есть небольшая программа, над которой я работаю, и я хотел бы подтвердить контрольные суммы файлов).

Александр
источник
3
Почему бы просто не использовать md5sum?
Kennytm
100
Хранение в Python облегчает управление кроссплатформенной совместимостью.
Александр
Если вам нужно решение с «индикатором выполнения» * или аналогичным (для очень больших файлов), рассмотрите это решение: stackoverflow.com/questions/1131220/…
Laurent LAPORTE
1
@kennytm Ссылка, которую вы предоставили, говорит об этом во втором абзаце: «Базовый алгоритм MD5 больше не считается безопасным» при описании md5sum. Вот почему программисты, которые заботятся о безопасности, не должны использовать это, по моему мнению.
Debug255
1
@ Debug255 Хороший и верный момент. Как md5sumи метод , описанный в этом вопросе SO следует избегать - это лучше использовать SHA-2 или SHA-3, если это возможно: en.wikipedia.org/wiki/Secure_Hash_Algorithms
Per Lundberg

Ответы:

466

Вы можете использовать hashlib.md5 ()

Обратите внимание, что иногда вы не сможете разместить весь файл в памяти. В этом случае вам нужно будет последовательно прочитать куски по 4096 байт и передать их md5методу:

import hashlib
def md5(fname):
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

Примечание: hash_md5.hexdigest() вернет представление шестнадцатеричной строки для дайджеста, если вам просто нужно использовать упакованные байты return hash_md5.digest(), поэтому вам не нужно конвертировать обратно.

quantumSoup
источник
297

Есть способ, который довольно неэффективно с памятью .

отдельный файл:

import hashlib
def file_as_bytes(file):
    with file:
        return file.read()

print hashlib.md5(file_as_bytes(open(full_path, 'rb'))).hexdigest()

список файлов:

[(fname, hashlib.md5(file_as_bytes(open(fname, 'rb'))).digest()) for fname in fnamelst]

Напомним, однако, что MD5, как известно, поврежден и не должен использоваться для каких-либо целей, поскольку анализ уязвимостей может быть очень сложным, и анализ любого возможного будущего использования вашего кода может быть применен для решения проблем безопасности, невозможно. ИМХО, он должен быть полностью удален из библиотеки, чтобы все, кто его использует, были вынуждены обновляться. Итак, вот что вы должны сделать вместо этого:

[(fname, hashlib.sha256(file_as_bytes(open(fname, 'rb'))).digest()) for fname in fnamelst]

Если вам нужен всего лишь 128 бит, вы можете это сделать .digest()[:16].

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

Опять я сильно сомневаюсь в вашем использовании MD5. Вы должны по крайней мере использовать SHA1, и учитывая недавние недостатки, обнаруженные в SHA1 , вероятно, даже не это. Некоторые люди думают, что пока вы не используете MD5 для «криптографических» целей, все в порядке. Но вещи имеют тенденцию к тому, чтобы в конечном итоге оказаться шире, чем вы изначально ожидали, и ваш случайный анализ уязвимостей может оказаться совершенно ошибочным. Лучше всего просто привыкнуть использовать правильный алгоритм из ворот. Это просто набор другой связки букв и все. Это не так сложно.

Вот способ более сложный, но эффективный с точки зрения памяти :

import hashlib

def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
    for block in bytesiter:
        hasher.update(block)
    return hasher.hexdigest() if ashexstr else hasher.digest()

def file_as_blockiter(afile, blocksize=65536):
    with afile:
        block = afile.read(blocksize)
        while len(block) > 0:
            yield block
            block = afile.read(blocksize)


[(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.md5()))
    for fname in fnamelst]

И, опять же, поскольку MD5 сломан и больше не должен использоваться:

[(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.sha256()))
    for fname in fnamelst]

Опять же, вы можете поставить [:16]после вызова, hash_bytestr_iter(...)если вы хотите только 128 битов дайджеста.

всевозможный
источник
66
Я только использую MD5, чтобы подтвердить, что файл не поврежден. Я не так обеспокоен тем, что его сломают.
Александр
87
@TheLifelessOne: И несмотря на страшные предупреждения, это очень хорошее применение MD5.
Президент Джеймс К. Полк
22
@GregS, @TheLifelessOne - Да, и следующее, что вы знаете, кто-то находит способ использовать этот факт в вашем приложении для того, чтобы файл был принят как не поврежденный, если это совсем не тот файл, который вы ожидаете. Нет, я поддерживаю свои страшные предупреждения. Я думаю, что MD5 должен быть удален или идти с предупреждениями об устаревании.
Всезнающий
10
Я бы, вероятно, использовал .hexdigest () вместо .digest () - людям легче читать - что является целью OP.
zbstof
21
Я использовал это решение, но оно неверно дало один и тот же хеш для двух разных файлов PDF. Решением было открыть файлы, указав двоичный режим, а именно: [(fname, hashlib.md5 (open (fname, 'rb' ) .read ()). Hexdigest ()) для fname в fnamelst] Это более связано в функцию open, чем в md5, но я подумал, что было бы полезно сообщить об этом, учитывая указанное выше требование кросс-платформенной совместимости (см. также: docs.python.org/2/tutorial/… ).
BlueCoder
34

Я явно не добавляю ничего принципиально нового, но добавил этот ответ, прежде чем приступил к комментированию статуса, плюс регионы кода проясняют ситуацию - во всяком случае, специально для ответа на вопрос @ Nemo из ответа Omnifarious:

Я немного подумал о контрольных суммах (пришел сюда в поисках предложений по размерам блоков, в частности), и обнаружил, что этот метод может быть быстрее, чем вы ожидаете. Взятие самого быстрого (но довольно типичного) timeit.timeitили /usr/bin/timeрезультата каждого из нескольких методов проверки контрольной суммы файла ок. 11MB:

$ ./sum_methods.py
crc32_mmap(filename) 0.0241742134094
crc32_read(filename) 0.0219960212708
subprocess.check_output(['cksum', filename]) 0.0553209781647
md5sum_mmap(filename) 0.0286180973053
md5sum_read(filename) 0.0311000347137
subprocess.check_output(['md5sum', filename]) 0.0332629680634
$ time md5sum /tmp/test.data.300k
d3fe3d5d4c2460b5daacc30c6efbc77f  /tmp/test.data.300k

real    0m0.043s
user    0m0.032s
sys     0m0.010s
$ stat -c '%s' /tmp/test.data.300k
11890400

Итак, похоже, что для Python и / usr / bin / md5sum требуется около 30 мс для файла размером 11 МБ. Соответствующая md5sumфункция ( md5sum_readв приведенном выше списке) очень похожа на функцию Omnifarious:

import hashlib
def md5sum(filename, blocksize=65536):
    hash = hashlib.md5()
    with open(filename, "rb") as f:
        for block in iter(lambda: f.read(blocksize), b""):
            hash.update(block)
    return hash.hexdigest()

Конечно, они из одиночных прогонов ( mmapте, которые выполняются на несколько шагов быстрее, когда выполняется, по крайней мере, несколько десятков прогонов), и мой обычно получает дополнительный f.read(blocksize)после того, как буфер исчерпан, но это достаточно повторяется и показывает, что md5sumв командной строке не обязательно быстрее, чем реализация Python ...

РЕДАКТИРОВАТЬ: Извините за долгую задержку, не смотрел на это некоторое время, но чтобы ответить на вопрос @ EdRandall, я запишу реализацию Adler32. Тем не менее, я не проводил тесты для этого. По сути, это то же самое, что и CRC32: вместо вызовов init, update и digest все является zlib.adler32()вызовом:

import zlib
def adler32sum(filename, blocksize=65536):
    checksum = zlib.adler32("")
    with open(filename, "rb") as f:
        for block in iter(lambda: f.read(blocksize), b""):
            checksum = zlib.adler32(block, checksum)
    return checksum & 0xffffffff

Обратите внимание, что это должно начинаться с пустой строки, так как суммы Адлера действительно различаются, когда начинаются с нуля, в сравнении с их суммой "", то есть 1- 0вместо этого можно начать с CRC . AND-Ную требуется , чтобы сделать это 32-разрядное целое число без знака, который гарантирует , что он возвращает то же значение между версиями Python.

rsandwick3
источник
Не могли бы вы добавить пару строк, сравнивающих SHA1, а также zlib.adler32?
Эд Рэндалл
1
Приведенная выше функция md5sum () предполагает, что у вас есть права на запись в файл. Если вы замените «r + b» в вызове open () на «rb», он будет работать нормально.
Кевин Лида
1
@EdRandall: adler32 действительно не стоит беспокоиться, например. leviathansecurity.com/blog/analysis-of-adler32
MikeW
6

В Python 3.8+ вы можете сделать

import hashlib
with open("your_filename.txt", "rb") as f:
    file_hash = hashlib.md5()
    while chunk := f.read(8192):
        file_hash.update(chunk)

print(file_hash.digest())
print(file_hash.hexdigest())  # to get a printable str instead of bytes

Попробуйте использовать hashlib.blake2bвместо md5(просто заменить md5на blake2bв приведенном выше фрагменте). Это криптографически безопасно и быстрее, чем MD5.

Борис
источник
:=Оператор является «оператор присваивания» (новый Python 3.8+); это позволяет назначать значения внутри большего выражения; больше информации здесь: docs.python.org/3/whatsnew/3.8.html#assignment-expressions
Бенджамин
0
hashlib.md5(pathlib.Path('path/to/file').read_bytes()).hexdigest()
Джонсон
источник
3
Здравствуй! Пожалуйста, добавьте некоторые объяснения в ваш код, почему это решение проблемы. Кроме того, этот пост довольно старый, поэтому вы также должны добавить некоторую информацию о том, почему ваше решение добавляет то, что другие еще не рассмотрели.
d_kennetz
1
Это еще один неэффективный способ памяти
Erik Aronesty
-2

Я думаю, что полагаться на пакет invoke и двоичный файл md5sum немного удобнее, чем на подпроцесс или пакет md5.

import invoke

def get_file_hash(path):

    return invoke.Context().run("md5sum {}".format(path), hide=True).stdout.split(" ")[0]

Это, конечно, предполагает, что у вас есть invoke и md5sum установлены.

Puchatek
источник
3
Если pathэто пользовательский путь, это позволит любому пользователю выполнять произвольные команды bash в вашей системе.
Борис