Obtenha o hash MD5 de grandes arquivos em Python

188

Eu usei o hashlib (que substitui o md5 no Python 2.6 / 3.0) e funcionou bem se eu abrisse um arquivo e colocasse seu conteúdo em hashlib.md5() função.

O problema é com arquivos muito grandes que seus tamanhos podem exceder o tamanho da RAM.

Como obter o hash MD5 de um arquivo sem carregar o arquivo inteiro na memória?

JustRegisterMe
fonte
20
Eu reformularia: "Como obter o MD5 de um arquivo sem carregar o arquivo inteiro na memória?"
XTL

Respostas:

147

Divida o arquivo em pedaços de 8192 bytes (ou algum outro múltiplo de 128 bytes) e alimente-os no MD5 consecutivamente usando update().

Isso tira proveito do fato de o MD5 ter blocos de resumo de 128 bytes (8192 é 128 × 64). Como você não está lendo o arquivo inteiro na memória, isso não usará muito mais do que 8192 bytes de memória.

No Python 3.8 ou superior, você pode fazer

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
fonte
81
Você pode usar com a mesma eficácia um tamanho de bloco de qualquer múltiplo de 128 (por exemplo, 8192, 32768 etc.) e isso será muito mais rápido do que ler 128 bytes de cada vez.
jmanning2k
40
Obrigado jmanning2k por essa observação importante, um teste em arquivo de 184 MB leva (0m9.230s, 0m2.547s, 0m2.429s) usando (128, 8192, 32768), usarei 8192, pois o valor mais alto causa um efeito não perceptível.
JustRegisterMe 17/07/2009
Se puder, você deve usar em hashlib.blake2bvez de md5. Ao contrário do MD5, o BLAKE2 é seguro e é ainda mais rápido.
Boris
2
@ Boris, você não pode realmente dizer que o BLAKE2 é seguro. Tudo o que você pode dizer é que ainda não foi quebrado.
vy32 8/04
@ vy32, você não pode dizer que definitivamente vai ser quebrado também. Veremos em 100 anos, mas é pelo menos melhor que o MD5, que é definitivamente inseguro.
Boris
220

Você precisa ler o arquivo em pedaços de tamanho adequado:

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: Certifique-se de abrir seu arquivo com o 'rb' para abrir; caso contrário, você obterá o resultado errado.

Então, para fazer o lote inteiro em um 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()

A atualização acima foi baseada nos comentários fornecidos por Frerich Raabe - e eu testei isso e achei que estava correto na minha instalação do Windows Python 2.7.2

Eu verifiquei os resultados usando a ferramenta 'jacksum'.

jacksum -a md5 <filename>

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

O médico
fonte
29
O que é importante notar é que o arquivo que é passado para esta função deve ser aberto no modo binário, ou seja, passando rbpara a openfunção.
Frerich Raabe
11
Esta é uma adição simples, mas usar em hexdigestvez de digestproduzirá um hash hexadecimal que "se parece" com a maioria dos exemplos de hashes.
Tchaymore 16/10
Não deveria ser if len(data) < block_size: break?
precisa
2
Erik, não, por que seria? O objetivo é alimentar todos os bytes no MD5, até o final do arquivo. Obter um bloco parcial não significa que todos os bytes não devem ser alimentados na soma de verificação.
2
@ user2084795 open sempre abre um novo identificador de arquivo com a posição definida para o início do arquivo (a menos que você abra um arquivo para anexar).
Steve Barnes
110

Abaixo incorporei a sugestão dos comentários. Obrigado a todos!

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 acima

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

postagem original

se você se preocupa com a maneira mais pitônica (sem 'enquanto True') de ler o arquivo, 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()

Observe que a função iter () precisa de uma cadeia de bytes vazia para o iterador retornado parar no EOF, pois read () retorna b '' (não apenas '').

Piotr Czapla
fonte
17
Melhor ainda, use algo como em 128*md5.block_sizevez de 8192.
mrkj
1
mrkj: Eu acho que é mais importante escolher o tamanho do seu bloco de leitura com base no seu disco e depois garantir que ele seja múltiplo md5.block_size.
Harvey
6
a b''sintaxe era nova para mim. Explicado aqui .
Cod3monk3y
1
@ThorSummoner: Na verdade não, mas do meu trabalho para encontrar tamanhos de bloco ideais para memória flash, sugiro que escolha um número como 32k ou algo facilmente divisível por 4, 8 ou 16k. Por exemplo, se o tamanho do seu bloco for 8k, a leitura de 32k terá 4 leituras no tamanho correto do bloco. Se é 16, então 2. Mas em cada caso, somos bons porque estamos lendo um número inteiro múltiplo de blocos.
Harvey
1
"enquanto True" é bastante pitônico.
Jürgen A. Erhard
49

Aqui está minha versão do método @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
fonte
30

Usando vários comentários / respostas neste tópico, aqui está minha solução:

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()
  • Isso é "pitônico"
  • Esta é uma função
  • Evita valores implícitos: sempre prefira valores explícitos.
  • Permite otimizações de desempenho (muito importantes)

E finalmente,

- Isso foi construído por uma comunidade, obrigado a todos por seus conselhos / idéias.

Bastien Semene
fonte
3
Uma sugestão: torne o objeto md5 um parâmetro opcional da função para permitir funções alternativas de hash, como sha256, para substituir facilmente o MD5. Vou propor isso como uma edição também.
precisa saber é o seguinte
1
também: digerir não é legível por humanos. hexdigest () permite uma saída mais compreensível, comumente recogonizable bem como o intercâmbio mais fácil do hash
Hawkwing
Outros formatos de hash estão fora do escopo da pergunta, mas a sugestão é relevante para uma função mais genérica. Eu adicionei uma opção "legível por humanos" de acordo com a sua segunda sugestão.
Bastien Semene
Você pode explicar como 'hr' está funcionando aqui?
EnemyBagJones 23/03
@EnemyBagJones 'hr' significa legível para humanos. Ele retorna uma seqüência de 32 de char dígitos hexadecimais comprimento: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene
8

Uma solução portátil Python 2/3

Para calcular uma soma de verificação (md5, sha1 etc.), você deve abrir o arquivo no modo binário, porque você somará valores de bytes:

Para ser portátil py27 / py3, você deve usar os iopacotes, assim:

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 seus arquivos forem grandes, convém ler o arquivo em pedaços para evitar armazenar todo o conteúdo do arquivo na memória:

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

O truque aqui é usar a iter()função com uma sentinela (a sequência vazia).

O iterador criado neste caso chamará o [a função lambda] sem argumentos para cada chamada ao seu next()método; se o valor retornado for igual a sentinel, StopIterationserá aumentado; caso contrário, o valor será retornado.

Se seus arquivos forem realmente grandes, talvez você também precise exibir informações de progresso. Você pode fazer isso chamando uma função de retorno de chamada que imprime ou registra a quantidade 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
fonte
3

Um remix do código Bastien Semene que leva em consideração o comentário de Hawkwing sobre a função 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
Richard
fonte
0

você não pode obtê-lo MD5 sem ler o conteúdo completo. mas você pode usar a função de atualização para ler o conteúdo dos arquivos bloco por bloco.
m.update (a); m.update (b) é equivalente a m.update (a + b)

sunqiang
fonte
0

Eu acho que o código a seguir é mais 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
fonte
-1

Implementação da resposta aceita para o 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()
escravo
fonte
-1

Eu não gosto de loops. Baseado em @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
fonte
Que possível motivo existe para substituir um loop simples e claro por um functools.reduce abberation contendo várias lambdas? Não tenho certeza se existe alguma convenção sobre programação que não tenha sido quebrada.
Naltharial 14/05/19
Meu principal problema era que hashliba API realmente não funciona bem com o restante do Python. Por exemplo, vamos dar uma shutil.copyfileobjolhada que não funciona muito bem. Minha próxima idéia foi fold(aka reduce), que dobra iterables em objetos únicos. Como, por exemplo, um hash. hashlibnão fornece operadores, o que torna isso um pouco complicado. No entanto, estavam dobrando um iterables aqui.
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
fonte
1
por favor, formate o código na resposta, e ler esta seção antes de dar respostas: stackoverflow.com/help/how-to-answer
Farside
1
Isso não funcionará corretamente, pois está lendo o arquivo no modo de texto linha por linha e depois mexendo com ele e imprimindo o md5 de cada linha despojada, codificada!
9788 Steve Barnes
-4

Não tenho certeza de que não haja muita confusão por aqui. Recentemente, tive problemas com md5 e arquivos armazenados como blobs no MySQL, então experimentei vários tamanhos de arquivo e a abordagem direta do Python, a saber:

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

Não pude detectar nenhuma diferença perceptível de desempenho com uma variedade de tamanhos de arquivo de 2 KB a 20 Mb e, portanto, não há necessidade de "dividir" o hash. De qualquer forma, se o Linux tiver que ir para o disco, provavelmente o fará pelo menos assim como a capacidade do programador médio de impedir que isso aconteça. Por acaso, o problema não estava relacionado ao MD5. Se você estiver usando o MySQL, não esqueça as funções md5 () e sha1 () já existentes.

user2099484
fonte
2
Isso não está respondendo à pergunta e 20 MB dificilmente é considerado um arquivo muito grande que pode não caber na RAM, conforme discutido aqui.
Chris