Ottieni l'hash MD5 di file di grandi dimensioni in Python


188

Ho usato l'hashlib (che sostituisce md5 in Python 2.6 / 3.0) e ha funzionato bene se ho aperto un file e ne ho messo in hashlib.md5()funzione il contenuto .

Il problema è con file molto grandi che le loro dimensioni potrebbero superare le dimensioni della RAM.

Come ottenere l'hash MD5 di un file senza caricare l'intero file in memoria?


20
Vorrei riformulare: "Come ottenere l'MD5 di un file senza caricare l'intero file in memoria?"
XTL

Risposte:


147

Suddividere il file in blocchi di 8192 byte (o qualche altro multiplo di 128 byte) e inviarli a MD5 consecutivamente usando update().

Ciò sfrutta il fatto che MD5 ha blocchi digest a 128 byte (8192 è 128 × 64). Poiché non stai leggendo l'intero file in memoria, questo non utilizzerà molto più di 8192 byte di memoria.

In Python 3.8+ puoi farlo

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
Puoi usare altrettanto efficacemente una dimensione di blocco di qualsiasi multiplo di 128 (diciamo 8192, 32768, ecc.) E sarà molto più veloce della lettura di 128 byte alla volta.
jmanning2k,

40
Grazie a jmanning2k per questa nota importante, un test su file da 184 MB richiede (0m9.230s, 0m2.547s, 0m2.429s) utilizzando (128, 8192, 32768), userò 8192 poiché il valore più alto produce effetti non evidenti.
JustRegister,

Se puoi, dovresti usare hashlib.blake2binvece di md5. A differenza di MD5, BLAKE2 è sicuro ed è ancora più veloce.
Boris,

2
@Boris, in realtà non si può dire che BLAKE2 sia sicuro. Tutto quello che puoi dire è che non è stato ancora rotto.
vy32

@ vy32 non puoi dire che sarà sicuramente rotto. Lo vedremo tra 100 anni, ma è almeno meglio di MD5 che è sicuramente insicuro.
Boris

220

Devi leggere il file in blocchi di dimensioni adeguate:

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

NOTA: assicurati di aprire il file con "rb" all'aperto, altrimenti otterrai un risultato errato.

Quindi, per fare tutto in un solo metodo, usa qualcosa come:

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

L'aggiornamento sopra si basava sui commenti forniti da Frerich Raabe - e l'ho provato e l'ho trovato corretto sulla mia installazione di Windows Python 2.7.2

Ho effettuato un controllo incrociato dei risultati usando lo strumento "jacksum".

jacksum -a md5 <filename>

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


29
Ciò che è importante notare è che il file che viene passato a questa funzione deve essere aperto in modalità binaria, cioè passando rballa openfunzione.
Frerich Raabe,

11
Questa è una semplice aggiunta, ma usando hexdigestinvece di digestprodurrà un hash esadecimale che "sembra" come la maggior parte degli esempi di hash.
Per il

Non dovrebbe essere if len(data) < block_size: break?
Erik Kaplun,

2
Erik, no, perché dovrebbe essere? L'obiettivo è fornire tutti i byte a MD5, fino alla fine del file. Ottenere un blocco parziale non significa che tutti i byte non debbano essere inviati al checksum.

2
@ user2084795 apre open sempre un handle di file nuovo con la posizione impostata all'inizio del file (a meno che non si apra un file per aggiungere).
Steve Barnes,

110

Di seguito ho incorporato suggerimenti dai commenti. Grazie a tutti!

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 e versioni successive

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

post originale

se ti interessa un modo più pythonic (no 'while True') di leggere il file, controlla questo codice:

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

Nota che l'iter () func ha bisogno di una stringa di byte vuota affinché l'iteratore restituito si fermi su EOF, poiché read () restituisce b '' (non solo '').


17
Meglio ancora, usa qualcosa di simile 128*md5.block_sizeinvece di 8192.
Mrkj,

1
mrkj: Penso che sia più importante scegliere la dimensione del blocco di lettura in base al disco e quindi assicurarsi che sia un multiplo di md5.block_size.
Harvey,

6
la b''sintassi era nuova per me. Spiegato qui .
cod3monk3y,

1
@ThorSummoner: Non proprio, ma dalla mia ricerca di dimensioni dei blocchi ottimali per la memoria flash, suggerirei di scegliere un numero come 32k o qualcosa di facilmente divisibile per 4, 8 o 16k. Ad esempio, se la dimensione del blocco è 8k, la lettura di 32k sarà di 4 letture con la dimensione del blocco corretta. Se è 16, quindi 2. Ma in ogni caso, siamo a posto perché stiamo leggendo un numero intero multiplo di blocchi.
Harvey,

1
"while True" è piuttosto pitonico.
Jürgen A. Erhard,

49

Ecco la mia versione del metodo di @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

Utilizzando più commenti / risposte in questa discussione, ecco la mia soluzione:

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()
  • Questo è "pythonic"
  • Questa è una funzione
  • Evita i valori impliciti: preferisci sempre quelli espliciti.
  • Permette (molto importanti) ottimizzazioni delle prestazioni

E infine,

- Questo è stato creato da una comunità, grazie a tutti per i tuoi consigli / idee.


3
Un suggerimento: rendere l'oggetto md5 un parametro facoltativo della funzione per consentire funzioni di hashing alternative, come sha256, per sostituire facilmente MD5. Lo proporrò anche come modifica.
Hawkwing,

1
inoltre: digest non è leggibile dall'uomo. hexdigest () consente un output più comprensibile, comunemente riconoscibile e un più facile scambio dell'hash
Hawkwing

Altri formati di hash non rientrano nell'ambito della domanda, ma il suggerimento è rilevante per una funzione più generica. Ho aggiunto un'opzione "leggibile dall'uomo" secondo il tuo secondo suggerimento.
Bastien Semene,

Puoi approfondire come funziona "hr" qui?
EnemyBagJones

@EnemyBagJones 'hr' sta per lettura umana. Restituisce una stringa di 32 cifre esadecimali di lunghezza char: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene

8

Una soluzione portatile Python 2/3

Per calcolare un checksum (md5, sha1, ecc.), È necessario aprire il file in modalità binaria, poiché si sommeranno i valori dei byte:

Per essere portatile py27 / py3, è necessario utilizzare i iopacchetti, in questo modo:

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

Se i tuoi file sono grandi, potresti preferire leggere il file in blocchi per evitare di memorizzare l'intero contenuto del file in memoria:

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

Il trucco qui è usare la iter()funzione con una sentinella (la stringa vuota).

L'iteratore creato in questo caso chiamerà o [la funzione lambda] senza argomenti per ogni chiamata al suo next()metodo; se il valore restituito è uguale a sentinella, StopIterationverrà generato, altrimenti verrà restituito.

Se i tuoi file sono molto grandi, potresti anche dover visualizzare le informazioni sullo stato di avanzamento. Puoi farlo chiamando una funzione di callback che stampa o registra la quantità di byte calcolati:

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

Un remix del codice Bastien Semene che prende in considerazione il commento di Hawkwing sulla funzione di hashing generico ...

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

non puoi ottenere è md5 senza leggere il contenuto completo. ma puoi usare la funzione di aggiornamento per leggere il contenuto del file blocco per blocco.
m.update (a); m.update (b) è equivalente a m.update (a + b)


0

Penso che il seguente codice sia più pythonic:

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

Implementazione della risposta accettata per 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

Non mi piacciono i loop. Basato su @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()

Quale possibile ragione c'è per sostituire un ciclo semplice e chiaro con un functools.Ridurre l'abberazione contenente più lambda? Non sono sicuro che ci sia una convenzione sulla programmazione che non ha funzionato.
Naltharial

Il mio problema principale era che l' hashlibAPI non funzionava davvero bene con il resto di Python. Ad esempio prendiamo shutil.copyfileobjche non riesce a funzionare da vicino. La mia prossima idea fu fold(aka reduce) che piega gli iterabili in singoli oggetti. Ad esempio un hash. hashlibnon fornisce operatori che lo rendono un po 'ingombrante. Tuttavia qui stavano piegando un iterabile.
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
per favore, formatta il codice nella risposta e leggi questa sezione prima di dare le risposte: stackoverflow.com/help/how-to-answer
Farside

1
Questo non funzionerà correttamente in quanto legge il file in modalità testo riga per riga, quindi si scherza con esso e stampa la md5 di ogni riga rimossa, codificata!
Steve Barnes,

-4

Non sono sicuro che non ci sia un po 'di confusione da queste parti. Di recente ho avuto problemi con md5 e file archiviati come BLOB su MySQL, quindi ho sperimentato varie dimensioni di file e il semplice approccio Python, vale a dire:

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

Non sono riuscito a rilevare alcuna differenza di prestazione evidente con una gamma di file di dimensioni comprese tra 2 Kb e 20 Mb e quindi non è necessario "bloccare" l'hash. Comunque, se Linux deve andare su disco, probabilmente lo farà almeno così come la capacità del programmatore medio di impedirlo. Come è accaduto, il problema non aveva nulla a che fare con md5. Se stai usando MySQL, non dimenticare le funzioni md5 () e sha1 () già presenti.


2
Questo non risponde alla domanda e 20 MB non sono quasi considerati un file molto grande che potrebbe non rientrare nella RAM, come discusso qui.
Chris,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.