Использование hashlib для вычисления md5-дайджеста файла в Python 3

В python 2.7 следующий код вычисляет шестнадцатеричный файл mD5 содержимого файла.

(РЕДАКТИРОВАТЬ: ну, не совсем так, как показали ответы, я просто так подумал).

import hashlib

def md5sum(filename):
    f = open(filename, mode='rb')
    d = hashlib.md5()
    for buf in f.read(128):
        d.update(buf)
    return d.hexdigest()

Теперь, если я запустил этот код с помощью python3, он вызовет исключение TypeError:

    d.update(buf)
TypeError: object supporting the buffer API required

Я понял, что могу заставить этот код работать с python2 и python3, изменив его на:

def md5sum(filename):
    f = open(filename, mode='r')
    d = hashlib.md5()
    for buf in f.read(128):
        d.update(buf.encode())
    return d.hexdigest()

Теперь мне все еще интересно, почему исходный код перестал работать. Кажется, что при открытии файла с использованием модификатора двоичного режима он возвращает целые числа вместо строк, закодированных как байты (я говорю это, потому что type (buf) возвращает int). Это поведение где-то объясняется?


person kriss    schedule 19.10.2011    source источник
comment
связанные: stackoverflow.com/q/4949162   -  person jfs    schedule 27.10.2011
comment
Было бы быстрее, если бы вы читали больше, ближе к размеру блока файловой системы? (например, 1024 байта в Linux ext3 и 4096 байтов или более в Windows NTFS)   -  person rakslice    schedule 01.08.2013


Ответы (3)


Я думаю, вы хотели, чтобы цикл for выполнял последовательные вызовы f.read(128). Это можно сделать с помощью iter () и functools.partial ():

import hashlib
from functools import partial

def md5sum(filename):
    with open(filename, mode='rb') as f:
        d = hashlib.md5()
        for buf in iter(partial(f.read, 128), b''):
            d.update(buf)
    return d.hexdigest()

print(md5sum('utils.py'))
person Raymond Hettinger    schedule 19.10.2011
comment
Да, именно это я и пытался сделать. В конце концов я добился этого с помощью менее элегантного решения, чем ваше, с использованием генератора. - person kriss; 20.10.2011
comment
Это приводит к утечке дескриптора файла в некоторых реализациях Python. Вы должны хотя бы позвонить close. - person phihag; 20.10.2011
comment
Я добавил оператор with, чтобы правильно закрыть файл. - person jfs; 20.10.2011
comment
@phihag: действительно ли существует реализация Python, в которой автоматическое закрытие фактически обрабатывает файл leaks? Я думал, это просто отложило выпуск этих файловых дескрипторов до сборки мусора? - person kriss; 20.10.2011
comment
но с заявлением действительно хорошо в любом случае - person kriss; 20.10.2011
comment
@kriss Ой, ты прав - в конце концов вызывается close, даже на Jython. Однако это только в том случае, если у вас нет трассировки стека исключений, лежащей в sys.exc_info (например, если read не удалось), поэтому рекомендуется вызвать close или использовать оператор with. - person phihag; 20.10.2011
comment
@ J.F.Sebastian Добавление оператора with улучшило код за счет обфускации ответа на вопрос OP. Многие люди смущаются или отвлекаются семантикой оператора with, поэтому он не входит в состав ответа, касающегося основ итераций. Люди, которые зацикливаются на утечке дескрипторов файлов, тратят свое время на то, что почти никогда не имеет значения в реальном коде. Оператор with хорош, но автоматическое закрытие файлов - это отдельная тема, которую не стоит отвлекать от ясного ответа на основной вопрос о чтении файлов по частям. - person Raymond Hettinger; 05.09.2012
comment
@RaymondHettinger: если вам это не нравится; просто отмените изменение. Я счел это слишком незначительным изменением, чтобы обсуждать его. Хотя я категорически не согласен с вашими рассуждениями. Открытый код должен соответствовать лучшим практикам, особенно, если он предназначен для начинающих. Если передовые методы слишком сложны для выполнения (хотя я не думаю, что это так) для такой общей задачи, тогда язык должен измениться. - person jfs; 05.09.2012

for buf in f.read(128):
  d.update(buf)

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

d.update(97)
d.update(98)
d.update(99)
d.update(100)

что не то, что вы хотите.

Вместо этого вы хотите:

def md5sum(filename):
  with open(filename, mode='rb') as f:
    d = hashlib.md5()
    while True:
      buf = f.read(4096) # 128 is smaller than the typical filesystem block
      if not buf:
        break
      d.update(buf)
    return d.hexdigest()
person phihag    schedule 19.10.2011
comment
Это съедает всю оперативную память, если открыть огромный файл. Вот почему мы буферизуем. - person Umur Kontacı; 20.10.2011
comment
@fastreload Это уже добавил;). Поскольку исходное решение не работало даже для файлов с ›128 байтами, я не думаю, что проблема с памятью, но я все равно добавил буферизованное чтение. - person phihag; 20.10.2011
comment
Тогда все было хорошо, но OP заявил, что может использовать свой код в Python 2.x, и перестал работать на 3.x. И я помню, что я сделал 1-байтовый буфер для вычисления md5 из 3-гигабайтного iso-файла для тестирования, и он не потерпел неудачу. Могу поспорить, что в python 2.7 есть отказоустойчивый механизм, согласно которому, независимо от пользовательского ввода, минимальный размер буфера не опускается ниже определенного уровня. Что ты говоришь? - person Umur Kontacı; 20.10.2011
comment
@fastreload Код в Python 2 не аварийно завершился после того, как итерация str произвела str. Результат по-прежнему был неверным для файлов размером более 128 байт. Конечно, вы можете настроить размер буфера по своему усмотрению (если у вас нет быстрого SSD, ЦП все равно надоест, а хорошие ОС предварительно загружают следующие байты файла). Python 2.7 определенно не имеет такого отказоустойчивого механизма; это нарушит договор read. OP просто не сравнивал результаты сценария с каноническими md5sum или результаты сценария на двух файлах со 128 идентичными первыми байтами. - person phihag; 20.10.2011
comment
да, мой исходный код действительно сломан (но еще не в дикой природе). Я просто не тестировал на больших файлах с таким же началом. Я должен был догадаться, что это была настоящая проблема, так как он работал слишком быстро. - person kriss; 20.10.2011
comment
Этот ответ неверен, если в нем говорится, что итерация байтов дает str objs. список (b'abc ') - ›[97, 98, 99] - person Raymond Hettinger; 21.10.2011
comment
@RaymondHettinger Упс, я дурак. Протестировал в 2.7 и был удивлен, увидев strs - да. Фиксированный. - person phihag; 21.10.2011

Я наконец изменил свой код на версию ниже (которую мне легко понять) после того, как я задал вопрос. Но я, вероятно, заменю его на версию, предложенную Раймондом Хеттингом без использования functools.partial.

import hashlib

def chunks(filename, chunksize):
    f = open(filename, mode='rb')
    buf = "Let's go"
    while len(buf):
        buf = f.read(chunksize)
        yield buf

def md5sum(filename):
    d = hashlib.md5()
    for buf in chunks(filename, 128):
        d.update(buf)
    return d.hexdigest()
person kriss    schedule 20.10.2011
comment
Теперь это будет работать, если длина файла не кратна размеру фрагмента, чтение фактически вернет более короткий буфер в последнем чтении. Завершение задается пустым буфером, поэтому условие not buf в приведенном выше примере кода (которое работает). - person Mapio; 20.10.2011
comment
@Mapio: в моем коде действительно есть какая-то ошибка, но совсем не там, где вы говорите. Длина файла значения не имеет. Приведенный выше код работает при условии, что нет частичного чтения, возвращающего неполные буферы. Если происходит частичное чтение, оно будет остановлено слишком рано (но с учетом частичного буфера). В некоторых случаях может произойти частичное чтение, например, если программа получает сигнал управляемого прерывания во время чтения, а затем продолжить чтение после выхода из прерывания. - person kriss; 20.10.2011
comment
Что ж, в приведенном выше комментарии, говоря о коде выше, я имею в виду старую версию. Этот текущий теперь работает (даже если это не лучшее возможное решение). - person kriss; 20.10.2011