Получить MD5 хеш больших файлов в Python

188

Я использовал hashlib (который заменяет md5 в Python 2.6 / 3.0), и он работал нормально, если я открыл файл и поместил его содержимое в hashlib.md5()функцию.

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

Как получить MD5-хеш файла без загрузки всего файла в память?

JustRegisterMe
источник
20
Я бы перефразировал: «Как получить файл MD5 без загрузки всего файла в память?»
XTL

Ответы:

147

Разбейте файл на 8192-байтовые куски (или другие кратные 128 байтам) и последовательно подайте их в MD5 update().

Это использует тот факт, что MD5 имеет 128-байтовые блоки дайджеста (8192 - 128 × 64). Поскольку вы не читаете весь файл в память, он не будет использовать намного больше, чем 8192 байта памяти.

В 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
Ювал Адам
источник
81
Вы можете также эффективно использовать размер блока, кратный 128 (скажем, 8192, 32768 и т. Д.), И это будет намного быстрее, чем чтение 128 байтов за раз.
jmanning2k
40
Спасибо jmanning2k за это важное примечание, тест на файл размером 184 МБ (0m9.230s, 0m2.547s, 0m2.429s) с использованием (128, 8192, 32768), я буду использовать 8192, так как более высокое значение дает незаметный эффект.
JustRegisterMe
Если вы можете, вы должны использовать hashlib.blake2bвместо md5. В отличие от MD5, BLAKE2 безопасен, и он еще быстрее.
Борис
2
@ Борис, ты не можешь сказать, что BLAKE2 безопасен. Все, что вы можете сказать, это то, что он еще не сломан.
vy32
@ vy32 ты не можешь сказать, что он тоже будет сломан. Мы посмотрим через 100 лет, но это по крайней мере лучше, чем MD5, который определенно небезопасен.
Борис
220

Вам нужно прочитать файл кусками подходящего размера:

def md5_for_file(f, block_size=2**20):
    md5 = hashlib.md5()
    while True:
        data = f.read(block_size)
        if not data:
            break
        md5.update(data)
    return md5.digest()

ПРИМЕЧАНИЕ. Убедитесь, что вы открыли свой файл с помощью 'rb', иначе вы получите неправильный результат.

Таким образом, чтобы сделать все в одном методе - используйте что-то вроде:

def generate_file_md5(rootdir, filename, blocksize=2**20):
    m = hashlib.md5()
    with open( os.path.join(rootdir, filename) , "rb" ) as f:
        while True:
            buf = f.read(blocksize)
            if not buf:
                break
            m.update( buf )
    return m.hexdigest()

Вышеупомянутое обновление было основано на комментариях, предоставленных Frerich Raabe - и я проверил это и нашел, что это правильно на моей установке Windows Python 2.7.2

Я перепроверил результаты, используя инструмент «jacksum».

jacksum -a md5 <filename>

http://www.jonelo.de/java/jacksum/

Доктор
источник
29
Важно отметить, что файл, который передается в эту функцию, должен быть открыт в двоичном режиме, то есть путем передачи rbв openфункцию.
Фрерих Раабе
11
Это простое дополнение, но использование hexdigestвместо него digestдаст шестнадцатеричный хеш, который «выглядит» как большинство примеров хешей.
Чайморе
Не должно ли это быть if len(data) < block_size: break?
Эрик Каплун
2
Эрик, нет, с чего бы это? Цель состоит в том, чтобы передать все байты в MD5 до конца файла. Получение частичного блока не означает, что все байты не должны подаваться в контрольную сумму.
2
@ user2084795 open всегда открывает новый дескриптор файла с позицией, установленной на начало файла (если вы не открываете файл для добавления).
Стив Барнс
110

Ниже я включил предложение из комментариев. Спасибо всем!

питон <3,7

import hashlib

def checksum(filename, hash_factory=hashlib.md5, chunk_num_blocks=128):
    h = hash_factory()
    with open(filename,'rb') as f: 
        for chunk in iter(lambda: f.read(chunk_num_blocks*h.block_size), b''): 
            h.update(chunk)
    return h.digest()

Python 3.8 и выше

import hashlib

def checksum(filename, hash_factory=hashlib.md5, chunk_num_blocks=128):
    h = hash_factory()
    with open(filename,'rb') as f: 
        while chunk := f.read(chunk_num_blocks*h.block_size): 
            h.update(chunk)
    return h.digest()

оригинальный пост

если вам нужен более питонический (без 'True') способ чтения файла, проверьте этот код:

import hashlib

def checksum_md5(filename):
    md5 = hashlib.md5()
    with open(filename,'rb') as f: 
        for chunk in iter(lambda: f.read(8192), b''): 
            md5.update(chunk)
    return md5.digest()

Обратите внимание, что для функции iter () требуется пустая строка байтов, чтобы возвращаемый итератор остановился на EOF, поскольку read () возвращает b '' (а не просто '').

Петр Чапла
источник
17
Еще лучше использовать что-то вроде 128*md5.block_sizeвместо 8192.
mrkj
1
mrkj: я думаю, что более важно выбрать размер блока чтения на основе вашего диска, а затем убедиться, что он кратен md5.block_size.
Харви
6
b''синтаксис был для меня новым. Объяснил здесь .
cod3monk3y
1
@ThorSummoner: Не совсем, но из моей работы по поиску оптимальных размеров блоков для флэш-памяти я бы предложил просто выбрать число, например 32 КБ, или что-нибудь, легко делимое на 4, 8 или 16 КБ. Например, если ваш размер блока 8 КБ, чтение 32 КБ будет 4 чтения при правильном размере блока. Если это 16, то 2. Но в каждом случае мы хороши, потому что мы читаем целое число блоков.
Харви
1
«В то время как Истина» довольно питонна.
Юрген А. Эрхард
49

Вот моя версия метода @Piotr Czapla:

def md5sum(filename):
    md5 = hashlib.md5()
    with open(filename, 'rb') as f:
        for chunk in iter(lambda: f.read(128 * md5.block_size), b''):
            md5.update(chunk)
    return md5.hexdigest()
Натан Фегер
источник
30

Используя несколько комментариев / ответов в этой теме, вот мое решение:

import hashlib
def md5_for_file(path, block_size=256*128, hr=False):
    '''
    Block size directly depends on the block size of your filesystem
    to avoid performances issues
    Here I have blocks of 4096 octets (Default NTFS)
    '''
    md5 = hashlib.md5()
    with open(path,'rb') as f: 
        for chunk in iter(lambda: f.read(block_size), b''): 
             md5.update(chunk)
    if hr:
        return md5.hexdigest()
    return md5.digest()
  • Это "питон"
  • Это функция
  • Избегает неявных значений: всегда предпочитайте явные.
  • Это позволяет (очень важно) оптимизации производительности

И наконец,

- Это было создано сообществом, спасибо всем за ваши советы / идеи.

Бастьен Семене
источник
3
Одно из предложений: сделайте ваш объект md5 необязательным параметром функции, чтобы позволить альтернативным функциям хеширования, таким как sha256, легко заменить MD5. Я предложу это как редактирование, а также.
Hawkwing
1
также: дайджест не читается человеком. hexdigest () обеспечивает более понятный, общеизвестный вывод, а также более легкий обмен хешем
Hawkwing
Другие форматы хеша выходят за рамки вопроса, но это предложение относится к более общей функции. Я добавил «читабельный» вариант по вашему второму предложению.
Бастьен Семене
Можете ли вы рассказать о том, как здесь работает «час»?
EnemyBagJones
@EnemyBagJones 'hr' означает удобочитаемый для человека. Возвращает строку из шестнадцатеричных цифр длиной 32 символа: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Бастьен Семене,
8

Портативное решение Python 2/3

Чтобы вычислить контрольную сумму (md5, sha1 и т. Д.), Вы должны открыть файл в двоичном режиме, потому что вы суммируете байтовые значения:

Чтобы быть переносимым py27 / py3, вы должны использовать ioпакеты, например так:

import hashlib
import io


def md5sum(src):
    md5 = hashlib.md5()
    with io.open(src, mode="rb") as fd:
        content = fd.read()
        md5.update(content)
    return md5

Если ваши файлы большие, вы можете предпочесть чтение файла по частям, чтобы избежать хранения всего содержимого файла в памяти:

def md5sum(src, length=io.DEFAULT_BUFFER_SIZE):
    md5 = hashlib.md5()
    with io.open(src, mode="rb") as fd:
        for chunk in iter(lambda: fd.read(length), b''):
            md5.update(chunk)
    return md5

Хитрость здесь в том, чтобы использовать iter()функцию с часовым (пустая строка).

Созданный в этом случае итератор будет вызывать o [лямбда-функцию] без аргументов для каждого вызова своего next()метода; если возвращаемое значение равно часовому, StopIterationбудет повышено, в противном случае будет возвращено значение.

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

def md5sum(src, callback, length=io.DEFAULT_BUFFER_SIZE):
    calculated = 0
    md5 = hashlib.md5()
    with io.open(src, mode="rb") as fd:
        for chunk in iter(lambda: fd.read(length), b''):
            md5.update(chunk)
            calculated += len(chunk)
            callback(calculated)
    return md5
Лоран Лапорт
источник
3

Ремикс кода Bastien Semene, который учитывает комментарии Hawkwing о универсальной хеширующей функции ...

def hash_for_file(path, algorithm=hashlib.algorithms[0], block_size=256*128, human_readable=True):
    """
    Block size directly depends on the block size of your filesystem
    to avoid performances issues
    Here I have blocks of 4096 octets (Default NTFS)

    Linux Ext4 block size
    sudo tune2fs -l /dev/sda5 | grep -i 'block size'
    > Block size:               4096

    Input:
        path: a path
        algorithm: an algorithm in hashlib.algorithms
                   ATM: ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512')
        block_size: a multiple of 128 corresponding to the block size of your filesystem
        human_readable: switch between digest() or hexdigest() output, default hexdigest()
    Output:
        hash
    """
    if algorithm not in hashlib.algorithms:
        raise NameError('The algorithm "{algorithm}" you specified is '
                        'not a member of "hashlib.algorithms"'.format(algorithm=algorithm))

    hash_algo = hashlib.new(algorithm)  # According to hashlib documentation using new()
                                        # will be slower then calling using named
                                        # constructors, ex.: hashlib.md5()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(block_size), b''):
             hash_algo.update(chunk)
    if human_readable:
        file_hash = hash_algo.hexdigest()
    else:
        file_hash = hash_algo.digest()
    return file_hash
Ричард
источник
0

Вы не можете получить его MD5 без чтения полного содержания. но вы можете использовать функцию обновления, чтобы прочитать содержимое файла блок за блоком.
m.update (а); m.update (b) эквивалентно m.update (a + b)

sunqiang
источник
0

Я думаю, что следующий код более питонический:

from hashlib import md5

def get_md5(fname):
    m = md5()
    with open(fname, 'rb') as fp:
        for chunk in fp:
            m.update(chunk)
    return m.hexdigest()
Вакет Чжэн
источник
-1

Реализация принятого ответа для Django:

import hashlib
from django.db import models


class MyModel(models.Model):
    file = models.FileField()  # any field based on django.core.files.File

    def get_hash(self):
        hash = hashlib.md5()
        for chunk in self.file.chunks(chunk_size=8192):
            hash.update(chunk)
        return hash.hexdigest()
lampslave
источник
-1

Я не люблю петли. Основано на @Nathan Feger:

md5 = hashlib.md5()
with open(filename, 'rb') as f:
    functools.reduce(lambda _, c: md5.update(c), iter(lambda: f.read(md5.block_size * 128), b''), None)
md5.hexdigest()
Себастьян Вагнер
источник
Какова возможная причина для замены простого и понятного цикла на аббревиатуру functools.recece, содержащую несколько лямбд? Я не уверен, есть ли какое-то соглашение по программированию, которое не нарушено.
Naltharial
Моя главная проблема заключалась в том, что hashlibAPI не очень хорошо работает с остальной частью Python. Например, давайте возьмем то, shutil.copyfileobjчто близко не работает. Моей следующей идеей было fold(иначе reduce) объединение итерируемых элементов в единые объекты. Например, хэш. hashlibне предоставляет операторов, что делает это немного громоздким. Тем не менее складывали здесь иероглифы.
Себастьян Вагнер,
-3
import hashlib,re
opened = open('/home/parrot/pass.txt','r')
opened = open.readlines()
for i in opened:
    strip1 = i.strip('\n')
    hash_object = hashlib.md5(strip1.encode())
    hash2 = hash_object.hexdigest()
    print hash2
Мхмад Мсарве
источник
1
Пожалуйста, отформатируйте код в ответе и прочитайте этот раздел, прежде чем давать ответы: stackoverflow.com/help/how-to-answer
Farside
1
Это не будет работать правильно, так как он читает строку в текстовом режиме построчно, а затем связывается с ним и печатает md5 каждой зачищенной, закодированной строки!
Стив Барнс
-4

Я не уверен, что здесь не так много суеты. Недавно у меня были проблемы с md5 и файлами, хранящимися как двоичные объекты на MySQL, поэтому я экспериментировал с файлами разных размеров и простым подходом Python, а именно:

FileHash=hashlib.md5(FileData).hexdigest()

Я не обнаружил заметной разницы в производительности с диапазоном размеров файлов от 2 КБ до 20 МБ и, следовательно, нет необходимости «разбивать» хэширование. В любом случае, если Linux должен перейти на диск, он, вероятно, сделает это по крайней мере так же, как способность обычного программиста удержать его от этого. Как оказалось, проблема не была связана с md5. Если вы используете MySQL, не забудьте, что функции md5 () и sha1 () уже есть.

user2099484
источник
2
Это не отвечает на вопрос, и 20 МБ вряд ли считаются очень большими файлами, которые могут не помещаться в ОЗУ, как обсуждалось здесь.
Крис