Uzyskaj skrót MD5 dużych plików w Pythonie


188

Użyłem hashliba (który zastępuje md5 w Pythonie 2.6 / 3.0) i działało dobrze, jeśli otworzyłem plik i włączyłem jego zawartość do hashlib.md5()funkcji.

Problem dotyczy bardzo dużych plików, których rozmiar może przekraczać rozmiar pamięci RAM.

Jak uzyskać skrót MD5 pliku bez ładowania całego pliku do pamięci?


20
Chciałbym przeformułować: „Jak zdobyć plik MD5 pliku bez ładowania całego pliku do pamięci?”
XTL

Odpowiedzi:


147

Podziel plik na 8192-bajtowe fragmenty (lub inną wielokrotność 128 bajtów) i podaj je kolejno do MD5 update().

Wykorzystuje to fakt, że MD5 ma 128-bajtowe bloki podsumowania (8192 to 128 × 64). Ponieważ nie odczytujesz całego pliku do pamięci, nie zajmie to więcej niż 8192 bajtów pamięci.

W Python 3.8+ możesz to zrobić

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
Równie skutecznie możesz użyć rozmiaru bloku dowolnej wielokrotności 128 (powiedzmy 8192, 32768 itd.), A to będzie znacznie szybsze niż czytanie 128 bajtów naraz.
jmanning2k

40
Dzięki jmanning2k za tę ważną uwagę, test na pliku 184 MB zajmuje (0m9.230s, 0m2.547s, 0m2.429s) używając (128, 8192, 32768), użyję 8192, ponieważ wyższa wartość daje niezauważalny wpływ.
JustRegisterMe 17.07.2009

Jeśli możesz, powinieneś użyć hashlib.blake2bzamiast md5. W przeciwieństwie do MD5 BLAKE2 jest bezpieczny i jeszcze szybszy.
Boris

2
@ Boris, tak naprawdę nie można powiedzieć, że BLAKE2 jest bezpieczny. Wszystko, co możesz powiedzieć, to że jeszcze nie zostało zepsute.
vy32

@ vy32 nie możesz powiedzieć, że to na pewno się zepsuje. Zobaczymy za 100 lat, ale jest co najmniej lepszy niż MD5, co jest zdecydowanie niepewne.
Boris

220

Musisz przeczytać plik w kawałkach o odpowiednim rozmiarze:

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

UWAGA: Upewnij się, że otworzyłeś plik za pomocą „rb” do otwarcia - w przeciwnym razie otrzymasz niewłaściwy wynik.

Aby wykonać cały proces za pomocą jednej metody - użyj czegoś takiego:

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

Powyższa aktualizacja była oparta na komentarzach Frericha Raabe - przetestowałem to i stwierdziłem, że jest poprawna podczas instalacji systemu Windows w Python 2.7.2

Sprawdziłem wyniki za pomocą narzędzia „jacksum”.

jacksum -a md5 <filename>

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


29
Ważne jest, aby zauważyć, że plik przekazywany do tej funkcji musi być otwarty w trybie binarnym, tzn. Poprzez przekazanie rbdo openfunkcji.
Frerich Raabe

11
Jest to prosty dodatek, ale użycie hexdigestzamiast digestspowoduje utworzenie szesnastkowego skrótu, który „wygląda” jak większość przykładów skrótów.
tchaymore

Nie powinno tak być if len(data) < block_size: break?
Erik Kaplun,

2
Erik, nie, dlaczego miałby być? Celem jest przesłanie wszystkich bajtów do MD5, aż do końca pliku. Uzyskanie częściowego bloku nie oznacza, że ​​wszystkie bajty nie powinny być podawane do sumy kontrolnej.

2
@ user2084795 open zawsze otwiera nowy uchwyt pliku z pozycją ustawioną na początek pliku, (chyba że otworzysz plik do dołączenia).
Steve Barnes,

110

Poniżej zamieściłem sugestie z komentarzy. Dziękuję al!

python <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 i wyżej

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

oryginalny post

jeśli zależy Ci na bardziej pythonicznym (nie „while True”) sposobie odczytu pliku, sprawdź ten kod:

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

Zauważ, że iter () func potrzebuje pustego ciągu bajtów, aby zwrócony iterator zatrzymał się na EOF, ponieważ read () zwraca b '' (nie tylko '').


17
Jeszcze lepiej, użyj czegoś takiego jak 128*md5.block_sizezamiast 8192.
mrkj

1
mrkj: Myślę, że ważniejsze jest wybranie rozmiaru bloku odczytu na podstawie dysku, a następnie upewnienie się, że jest to wielokrotność md5.block_size.
Harvey

6
b''składnia było dla mnie nowe. Wyjaśniono tutaj .
cod3monk3y

1
@ThorSummoner: Nie bardzo, ale z mojego pracy polegającej na znalezieniu optymalnych rozmiarów bloków dla pamięci flash, sugerowałbym wybranie liczby takiej jak 32k lub czegoś łatwo podzielnego przez 4, 8 lub 16k. Na przykład, jeśli twój rozmiar bloku to 8k, odczyt 32k będzie 4 odczytami przy prawidłowym rozmiarze bloku. Jeśli jest to 16, to 2. Ale w każdym przypadku jesteśmy dobrzy, ponieważ akurat czytamy liczbę całkowitą wielu bloków.
Harvey

1
„while True” jest dość pytoniczne.
Jürgen A. Erhard

49

Oto moja wersja metody @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

Korzystając z wielu komentarzy / odpowiedzi w tym wątku, oto moje rozwiązanie:

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()
  • To jest „python”
  • To jest funkcja
  • Unika wartości niejawnych: zawsze preferuj wartości jawne.
  • Umożliwia (bardzo ważne) optymalizacje wydajności

I w końcu,

- Zostało to zbudowane przez społeczność, dziękuję wszystkim za porady / pomysły.


3
Jedna sugestia: ustaw obiekt md5 jako opcjonalny parametr funkcji, aby umożliwić alternatywne funkcje mieszające, takie jak sha256, aby łatwo zastąpić MD5. Zaproponuję to również jako edycję.
Hawkwing,

1
także: digest nie jest czytelny dla człowieka. hexdigest () pozwala na bardziej zrozumiałe, powszechnie rozpoznawalne wyjście, a także łatwiejszą wymianę skrótu
Hawkwing

Inne formaty skrótów nie wchodzą w zakres pytania, ale sugestia dotyczy bardziej ogólnej funkcji. Dodałem opcję „czytelną dla człowieka” zgodnie z Twoją drugą sugestią.
Bastien Semene

Czy możesz wyjaśnić, w jaki sposób funkcjonuje tutaj „hr”?
EnemyBagJones

@EnemyBagJones „hr” oznacza czytelny dla człowieka. Zwraca ciąg znaków szesnastkowych o długości 32 znaków: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene

8

Przenośne rozwiązanie Python 2/3

Aby obliczyć sumę kontrolną (md5, sha1 itp.), Musisz otworzyć plik w trybie binarnym, ponieważ sumujesz wartości bajtów:

Aby być przenośnym py27 / py3, powinieneś użyć takich iopakietów:

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

Jeśli twoje pliki są duże, możesz chcieć przeczytać plik po kawałkach, aby uniknąć przechowywania całej zawartości pliku w pamięci:

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

Sztuczka polega na tym, aby użyć iter()funkcji z wartownikiem (pusty ciąg znaków).

Iterator utworzony w tym przypadku wywoła o [funkcję lambda] bez argumentów dla każdego wywołania swojej next()metody; jeśli zwrócona wartość jest równa wartownikowi, StopIterationzostanie podniesiona, w przeciwnym razie wartość zostanie zwrócona.

Jeśli Twoje pliki są naprawdę duże, może być konieczne wyświetlenie informacji o postępach. Możesz to zrobić, wywołując funkcję zwrotną, która drukuje lub rejestruje obliczoną liczbę bajtów:

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

Remiks kodu Bastien Semene, który bierze pod uwagę komentarz Hawkwinga na temat ogólnej funkcji skrótu ...

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

nie można dostać to md5 bez przeczytania pełnej treści. ale możesz użyć funkcji aktualizacji, aby odczytać zawartość pliku blok po bloku.
m. aktualizacja (a); m.update (b) jest równoważne m.update (a + b)


0

Myślę, że następujący kod jest bardziej pythonowy:

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

Realizacja zaakceptowanej odpowiedzi dla 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()

-1

Nie lubię pętli. Na podstawie @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()

Jaki jest możliwy powód zastąpienia prostej i przejrzystej pętli funkcją funools.reduce abberacja zawierająca wiele lambd? Nie jestem pewien, czy istnieje jakaś konwencja dotycząca programowania, która się nie złamała.
Naltharial

Moim głównym problemem było to, że hashlibAPI naprawdę nie gra dobrze z resztą Pythona. Na przykład weźmy, shutil.copyfileobjktóry ściśle nie działa. Mój następny pomysł to fold(aka reduce) składanie iterowalnych razem w pojedyncze obiekty. Jak np. Hash. hashlibnie zapewnia operatorów, co sprawia, że ​​jest to trochę uciążliwe. Niemniej jednak składali tutaj iteracje.
Sebastian Wagner

-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
sformatuj kod w odpowiedzi i przeczytaj tę sekcję przed udzieleniem odpowiedzi: stackoverflow.com/help/how-to-answer
Farside

1
To nie będzie działać poprawnie, ponieważ odczytuje plik w trybie tekstowym linia po linii, a następnie zadziera z nim i drukuje md5 każdej rozebranej, zakodowanej linii!
Steve Barnes,

-4

Nie jestem pewien, czy nie ma tu zbyt wiele problemów. Ostatnio miałem problemy z md5 i plikami przechowywanymi jako obiekty BLOB na MySQL, więc eksperymentowałem z różnymi rozmiarami plików i prostym podejściem do Pythona, a mianowicie:

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

Nie mogłem wykryć zauważalnej różnicy wydajności w zakresie rozmiarów plików od 2Kb do 20Mb, a zatem nie ma potrzeby „dzielenia” haszowania. W każdym razie, jeśli Linux musi przejść na dysk, prawdopodobnie zrobi to co najmniej tak samo, jak zdolność przeciętnego programisty do powstrzymania tego. Tak się złożyło, że problem nie miał nic wspólnego z md5. Jeśli używasz MySQL, nie zapomnij już o funkcjach md5 () i sha1 ().


2
To nie odpowiada na pytanie, a 20 MB nie jest uważane za bardzo duży plik, który może nie pasować do pamięci RAM, jak omówiono tutaj.
Chris
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.