Отримайте MD5 хеш великих файлів у Python


188

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

Проблема полягає в дуже великих файлах, що їх розміри можуть перевищувати розмір ОЗУ.

Як отримати хеш файлу MD5 без завантаження всього файлу в пам'ять?


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
@Boris, ти фактично не можеш сказати, що 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()

Оновлення вище ґрунтувалося на коментарях, наданих Фріріхом Раабе, - і я перевірив це, і виявив, що він правильний на моїй установці Windows Python 2.7.2

Я перехресно перевірив результати за допомогою інструмента «jacksum».

jacksum -a md5 <filename>

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


29
Що важливо помітити, це те, що файл, переданий цій функції, повинен бути відкритий у двійковому режимі, тобто шляхом переходу rbдо openфункції.
Фріріх Раабе

11
Це просте доповнення, але використання hexdigestзамість digestцього генерує шістнадцятковий хеш, який "виглядає" як більшість прикладів хешів.
tchaymore

Чи не повинно бути 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()

пітон 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: Насправді, але, працюючи в пошуку оптимальних розмірів блоку для флеш-пам’яті, я б запропонував просто вибрати таке число, як 32k або щось, що легко ділиться на 4, 8 або 16k. Наприклад, якщо розмір блоку становить 8 кб, читання 32 к буде 4 зчитування при правильному розмірі блоку. Якщо це 16, то 2. Але в кожному конкретному випадку ми хороші, тому що ми, мабуть, читаємо ціле ціле число блоків.
Харві

1
"в той час як True" є досить пітонічним.
Юрген А. Ерхард

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. Я також пропоную це як редагування.
Хоукінг

1
також: дайджест не читабельний для людини. hexdigest () дозволяє більш зрозумілою, зазвичай recogonizable вихід, а також більш легкого обміну хеш
Hawkwing

Інші формати хешу виходять за рамки питання, але пропозиція є актуальною для більш загальної функції. Я додав варіант "читабельний для людини" відповідно до вашої 2-ї пропозиції.
Бастієн Семен

Чи можете ви детальніше розповісти про те, як функціонує "hr" тут?
EnemyBagJones

@EnemyBagJones 'hr' означає людську читабельність. Він повертає рядок із шістнадцятковими цифрами довжиною 32 знаків: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene

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

Ремікс коду Бастієна Семена, який враховує коментар Хоукінга про загальну функцію хешування ...

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 (a); m.update (b) еквівалентний m.update (a + b)


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

Впровадження прийнятої відповіді на Джанго:

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()

-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.reduce, що містить кілька лямбда? Я не впевнений, чи є якась конвенція щодо програмування, це не порушилось.
Налтарій

Моя основна проблема полягала в тому, hashlibщо API API не дуже добре грає з рештою 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()

Я не міг виявити помітної різниці в продуктивності з діапазоном розмірів файлів від 2Kb до 20Mb, і тому не потрібно «шматувати» хешування. У будь-якому випадку, якщо Linux доведеться перейти на диск, він, ймовірно, зробить це принаймні так само, як і вміння середнього програміста, щоб уникнути цього. Як це сталося, проблема не мала нічого спільного з md5. Якщо ви використовуєте MySQL, не забудьте функції md5 () та sha1 (), які вже є.


2
Це не дає відповіді на запитання, і 20 Мб навряд чи вважається дуже великим файлом, який може не вміститися в оперативну пам’ять, як обговорювалося тут.
Кріс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.