Obtenga MD5 hash de archivos grandes en Python

188

He usado hashlib (que reemplaza md5 en Python 2.6 / 3.0) y funcionó bien si abrí un archivo y puse su contenido en hashlib.md5()función.

El problema es con archivos muy grandes que sus tamaños podrían exceder el tamaño de RAM.

¿Cómo obtener el hash MD5 de un archivo sin cargar todo el archivo en la memoria?

JustRegisterMe
fuente
20
Reformularía: "¿Cómo obtener el MD5 de un archivo sin cargar todo el archivo en la memoria?"
XTL

Respuestas:

147

Divida el archivo en fragmentos de 8192 bytes (o algún otro múltiplo de 128 bytes) y aliméntelos a MD5 consecutivamente con update().

Esto aprovecha el hecho de que MD5 tiene bloques de resumen de 128 bytes (8192 es 128 × 64). Como no está leyendo todo el archivo en la memoria, esto no utilizará mucho más de 8192 bytes de memoria.

En Python 3.8+ puedes hacer

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
Yuval Adam
fuente
81
Puede usar con igual eficacia un tamaño de bloque de cualquier múltiplo de 128 (digamos 8192, 32768, etc.) y eso será mucho más rápido que leer 128 bytes a la vez.
jmanning2k
40
Gracias jmanning2k por esta importante nota, una prueba en un archivo de 184MB toma (0m9.230s, 0m2.547s, 0m2.429s) usando (128, 8192, 32768), usaré 8192 ya que el valor más alto da un efecto no notable.
JustRegisterMe
Si puede, debe usar en hashlib.blake2blugar de md5. A diferencia de MD5, BLAKE2 es seguro y es aún más rápido.
Boris
2
@ Boris, no puedes decir que BLAKE2 es seguro. Todo lo que puedes decir es que aún no se ha roto.
vy32
@ vy32 tampoco puedes decir que definitivamente se va a romper. Lo veremos en 100 años, pero es al menos mejor que MD5, que definitivamente es inseguro.
Boris
220

Debe leer el archivo en fragmentos de tamaño adecuado:

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: Asegúrese de abrir su archivo con el 'rb' al abierto; de lo contrario, obtendrá el resultado incorrecto.

Entonces, para hacer todo en un método, use algo como:

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

La actualización anterior se basó en los comentarios proporcionados por Frerich Raabe, y probé esto y descubrí que era correcto en mi instalación de Windows Python 2.7.2

Verifiqué los resultados con la herramienta 'jacksum'.

jacksum -a md5 <filename>

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

TheDoctor
fuente
29
Lo importante es notar que el archivo que se pasa a esta función debe abrirse en modo binario, es decir, pasando rba la openfunción.
Frerich Raabe
11
Esta es una adición simple, pero el uso en hexdigestlugar de digestproducirá un hash hexadecimal que "se ve" como la mayoría de los ejemplos de hashes.
tchaymore
¿No debería ser if len(data) < block_size: break?
Erik Kaplun
2
Erik, no, ¿por qué sería? El objetivo es alimentar todos los bytes a MD5, hasta el final del archivo. Obtener un bloque parcial no significa que todos los bytes no se deben alimentar a la suma de verificación.
2
@ user2084795 open siempre abre un nuevo identificador de archivo con la posición establecida al inicio del archivo (a menos que abra un archivo para agregarlo).
Steve Barnes
110

A continuación he incorporado sugerencias de comentarios. Gracias al!

pitón <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 y superior

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

publicación original

si le interesa más la forma pitónica (no 'while True') de leer el archivo, verifique este código:

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

Tenga en cuenta que la función iter () necesita una cadena de bytes vacía para que el iterador devuelto se detenga en EOF, ya que read () devuelve b '' (no solo '').

Piotr Czapla
fuente
17
Mejor aún, use algo como en 128*md5.block_sizelugar de 8192.
mrkj
1
mrkj: Creo que es más importante elegir el tamaño del bloque de lectura en función de su disco y luego asegurarse de que sea un múltiplo de md5.block_size.
Harvey
66
La b''sintaxis era nueva para mí. Explicado aquí .
cod3monk3y
1
@ThorSummoner: No realmente, pero por mi trabajo en encontrar tamaños de bloque óptimos para la memoria flash, sugeriría simplemente elegir un número como 32k o algo fácilmente divisible por 4, 8 o 16k. Por ejemplo, si su tamaño de bloque es 8k, la lectura de 32k será de 4 lecturas con el tamaño de bloque correcto. Si es 16, entonces 2. Pero en cada caso, estamos bien porque estamos leyendo un número entero de múltiples bloques.
Harvey
1
"while True" es bastante pitónico.
Jürgen A. Erhard
49

Aquí está mi versión del método de @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()
Nathan Feger
fuente
30

Usando múltiples comentarios / respuestas en este hilo, aquí está mi solución:

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()
  • Esto es "pitónico"
  • Esta es una funcion
  • Evita los valores implícitos: siempre prefiere los explícitos.
  • Permite optimizaciones de rendimiento (muy importantes)

Y finalmente,

- Esto ha sido construido por una comunidad, gracias a todos por sus consejos / ideas.

Bastien Semene
fuente
3
Una sugerencia: haga que su objeto md5 sea un parámetro opcional de la función para permitir funciones de hashing alternativas, como sha256 para reemplazar fácilmente MD5. Propondré esto como una edición, también.
Hawkwing
1
también: digest no es legible para humanos. hexdigest () permite una salida más comprensible, comúnmente reconocible, así como un intercambio más fácil del hash
Hawkwing
Otros formatos hash están fuera del alcance de la pregunta, pero la sugerencia es relevante para una función más genérica. Agregué una opción "legible para humanos" de acuerdo con su segunda sugerencia.
Bastien Semene
¿Puedes explicar cómo funciona 'hr' aquí?
EnemyBagJones
@EnemyBagJones 'hr' significa legible por humanos. Devuelve una cadena de 32 dígitos hexadecimales de longitud de caracteres: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene
8

Una solución portátil Python 2/3

Para calcular una suma de comprobación (md5, sha1, etc.), debe abrir el archivo en modo binario, ya que sumará los valores de bytes:

Para ser portátil py27 / py3, debe usar los iopaquetes, de esta manera:

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

Si sus archivos son grandes, puede preferir leer el archivo por partes para evitar almacenar todo el contenido del archivo en la 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

El truco aquí es usar la iter()función con un centinela (la cadena vacía).

El iterador creado en este caso llamará a o [la función lambda] sin argumentos para cada llamada a su next()método; si el valor devuelto es igual a centinela, se StopIterationelevará, de lo contrario, se devolverá el valor.

Si sus archivos son realmente grandes, es posible que también necesite mostrar información de progreso. Puede hacerlo llamando a una función de devolución de llamada que imprime o registra la cantidad de bytes calculados:

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
Laurent LAPORTE
fuente
3

Una remezcla del código Bastien Semene que tiene en cuenta el comentario de Hawkwing sobre la función de hash genérica ...

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
Ricardo
fuente
0

no puedes obtener su md5 sin leer el contenido completo. pero puede usar la función de actualización para leer el contenido de los archivos bloque por bloque.
m.actualización (a); m.update (b) es equivalente a m.update (a + b)

sunqiang
fuente
0

Creo que el siguiente código es más pitónico:

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()
Waket Zheng
fuente
-1

Implementación de la respuesta aceptada para 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()
lampara
fuente
-1

No me gustan los loops. Basado en @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()
Sebastian Wagner
fuente
¿Qué posible razón hay para reemplazar un bucle simple y claro con un functools.reduce la abberación que contiene múltiples lambdas? No estoy seguro de si hay alguna convención sobre programación que no se haya roto.
Naltharial
Mi principal problema fue que hashlibla API realmente no funciona bien con el resto de Python. Por ejemplo, tomemos shutil.copyfileobjque estrechamente no funciona. Mi siguiente idea fue fold(aka reduce) que pliega iterables en objetos individuales. Como, por ejemplo, un hash. hashlibno proporciona operadores, lo que hace que esto sea un poco engorroso. Sin embargo estaban doblando un iterables aquí.
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
mhmad msarwe
fuente
1
por favor, formatee el código en la respuesta y lea esta sección antes de responder: stackoverflow.com/help/how-to-answer
Farside
1
¡Esto no funcionará correctamente ya que está leyendo el archivo en modo de texto línea por línea y luego jugando con él e imprimiendo el md5 de cada línea pelada y codificada!
Steve Barnes
-4

No estoy seguro de que no haya demasiado alboroto por aquí. Recientemente tuve problemas con md5 y archivos almacenados como blobs en MySQL, así que experimenté con varios tamaños de archivos y el enfoque directo de Python, a saber:

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

No pude detectar ninguna diferencia de rendimiento notable con un rango de tamaños de archivo de 2Kb a 20Mb y, por lo tanto, no es necesario 'fragmentar' el hash. De todos modos, si Linux tiene que ir al disco, probablemente lo hará al menos tan bien como la capacidad promedio del programador para evitar que lo haga. Como sucedió, el problema no tenía nada que ver con md5. Si está utilizando MySQL, no olvide las funciones md5 () y sha1 () que ya están allí.

usuario2099484
fuente
2
Esto no responde a la pregunta y 20 MB difícilmente se consideran un archivo muy grande que puede no encajar en la RAM como se explica aquí.
Chris