Obtenez le hachage MD5 de gros fichiers en Python

192

J'ai utilisé hashlib (qui remplace md5 dans Python 2.6 / 3.0) et cela fonctionnait bien si j'ouvrais un fichier et mettais son contenu en hashlib.md5()fonction.

Le problème est que les très gros fichiers peuvent dépasser la taille de la RAM.

Comment obtenir le hachage MD5 d'un fichier sans charger le fichier entier en mémoire?

JustRegisterMe
la source
22
Je reformulerais: "Comment obtenir le MD5 a d'un fichier sans charger le fichier entier en mémoire?"
XTL

Réponses:

152

Divisez le fichier en morceaux de 8192 octets (ou un autre multiple de 128 octets) et transmettez-les à MD5 consécutivement en utilisant update().

Cela tire parti du fait que MD5 a des blocs de résumé de 128 octets (8192 est 128 × 64). Puisque vous ne lisez pas l'intégralité du fichier en mémoire, cela n'utilisera pas beaucoup plus de 8192 octets de mémoire.

Dans Python 3.8+, vous pouvez faire

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
la source
81
Vous pouvez tout aussi efficacement utiliser une taille de bloc de n'importe quel multiple de 128 (disons 8192, 32768, etc.) et ce sera beaucoup plus rapide que la lecture de 128 octets à la fois.
jmanning2k
41
Merci jmanning2k pour cette note importante, un test sur un fichier de 184 Mo prend (0m9.230s, 0m2.547s, 0m2.429s) en utilisant (128, 8192, 32768), j'utiliserai 8192 car la valeur la plus élevée donne un effet non perceptible.
JustRegisterMe
Si vous le pouvez, vous devriez utiliser à la hashlib.blake2bplace de md5. Contrairement à MD5, BLAKE2 est sécurisé et encore plus rapide.
Boris
3
@Boris, vous ne pouvez pas vraiment dire que BLAKE2 est sécurisé. Tout ce que vous pouvez dire, c'est qu'il n'a pas encore été cassé.
vy32
@ vy32 vous ne pouvez pas dire non plus qu'il sera définitivement cassé. Nous verrons dans 100 ans, mais c'est au moins mieux que MD5 qui n'est définitivement pas sûr.
Boris
221

Vous devez lire le fichier en morceaux de taille appropriée:

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

REMARQUE: assurez-vous d'ouvrir votre fichier avec le «rb» à l'ouverture - sinon vous obtiendrez le mauvais résultat.

Donc, pour faire le tout en une seule méthode - utilisez quelque chose comme:

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 mise à jour ci-dessus était basée sur les commentaires fournis par Frerich Raabe - et j'ai testé cela et je l'ai trouvé correct sur mon installation Windows Python 2.7.2

J'ai vérifié les résultats en utilisant l'outil «jacksum».

jacksum -a md5 <filename>

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

Le médecin
la source
29
Il est important de noter que le fichier qui est passé à cette fonction doit être ouvert en mode binaire, c'est-à-dire en passant rbà la openfonction.
Frerich Raabe
11
C'est un simple ajout, mais utiliser à la hexdigestplace de digestproduira un hachage hexadécimal qui "ressemble" à la plupart des exemples de hachages.
tchaymore
Ne devrait-il pas l'être if len(data) < block_size: break?
Erik Kaplun
2
Erik, non, pourquoi serait-ce? Le but est de fournir tous les octets à MD5, jusqu'à la fin du fichier. Obtenir un bloc partiel ne signifie pas que tous les octets ne doivent pas être envoyés à la somme de contrôle.
2
@ user2084795 ouvre open toujours un nouveau descripteur de fichier avec la position définie au début du fichier (sauf si vous ouvrez un fichier à ajouter).
Steve Barnes
111

Ci-dessous, j'ai incorporé la suggestion des commentaires. Merci à tous!

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 et supérieur

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

message original

si vous vous souciez d'une manière plus pythonique (pas de `` while True '') de lire le fichier, vérifiez ce code:

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

Notez que la fonction iter () a besoin d'une chaîne d'octets vide pour que l'itérateur retourné s'arrête à EOF, puisque read () renvoie b '' (pas seulement '').

Piotr Czapla
la source
17
Mieux encore, utilisez quelque chose comme à la 128*md5.block_sizeplace de 8192.
mrkj
1
mrkj: Je pense qu'il est plus important de choisir la taille de votre bloc de lecture en fonction de votre disque, puis de vous assurer qu'il s'agit d'un multiple de md5.block_size.
Harvey
6
la b''syntaxe était nouvelle pour moi. Expliqué ici .
cod3monk3y
1
@ThorSummoner: Pas vraiment, mais d'après mon travail sur la recherche de tailles de bloc optimales pour la mémoire flash, je suggérerais simplement de choisir un nombre comme 32k ou quelque chose de facilement divisible par 4, 8 ou 16k. Par exemple, si la taille de votre bloc est de 8k, la lecture de 32k correspondra à 4 lectures à la taille de bloc correcte. Si c'est 16, alors 2. Mais dans chaque cas, nous sommes bons parce que nous lisons un nombre entier multiple de blocs.
Harvey
1
"while True" est assez pythonique.
Jürgen A. Erhard
49

Voici ma version de la méthode 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
la source
30

En utilisant plusieurs commentaires / réponses dans ce fil, voici ma solution:

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()
  • C'est "pythonique"
  • C'est une fonction
  • Cela évite les valeurs implicites: préférez toujours les valeurs explicites.
  • Il permet des optimisations (très importantes) des performances

Et enfin,

- Ceci a été construit par une communauté, merci à tous pour vos conseils / idées.

Bastien Semene
la source
3
Une suggestion: faites de votre objet md5 un paramètre optionnel de la fonction pour permettre à d'autres fonctions de hachage, comme sha256, de remplacer facilement MD5. Je vais également proposer cela comme une modification.
Hawkwing
1
aussi: digest n'est pas lisible par l'homme. hexdigest () permet une sortie plus compréhensible et généralement reconnaissable ainsi qu'un échange plus facile du hachage
Hawkwing
D'autres formats de hachage sont hors du champ de la question, mais la suggestion est pertinente pour une fonction plus générique. J'ai ajouté une option "lisible par l'homme" selon votre 2ème suggestion.
Bastien Semene
Pouvez-vous expliquer comment fonctionne «hr» ici?
EnemyBagJones
@EnemyBagJones 'hr' signifie lisible par l'homme. Il renvoie une chaîne de 32 caractères hexadécimaux de longueur: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene
8

Une solution portable Python 2/3

Pour calculer une somme de contrôle (md5, sha1, etc.), vous devez ouvrir le fichier en mode binaire, car vous allez additionner les valeurs d'octets:

Pour être portable py27 / py3, vous devez utiliser les iopackages, comme ceci:

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 vos fichiers sont volumineux, vous préférerez peut-être lire le fichier par morceaux pour éviter de stocker tout le contenu du fichier en mémoire:

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

L'astuce ici est d'utiliser la iter()fonction avec une sentinelle (la chaîne vide).

L'itérateur créé dans ce cas appellera o [la fonction lambda] sans argument pour chaque appel à sa next()méthode; si la valeur retournée est égale à sentinel, StopIterationsera levée, sinon la valeur sera retournée.

Si vos fichiers sont vraiment volumineux, vous devrez peut-être également afficher des informations de progression. Vous pouvez le faire en appelant une fonction de rappel qui imprime ou enregistre la quantité d'octets calculés:

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
la source
3

Un remix du code Bastien Semene qui prend en compte le commentaire de Hawkwing sur la fonction de hachage générique

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
la source
0

Vous ne pouvez pas obtenir son md5 sans lire le contenu complet. mais vous pouvez utiliser la fonction de mise à jour pour lire le contenu des fichiers bloc par bloc.
m.update (a); m.update (b) équivaut à m.update (a + b)

sunqiang
la source
0

Je pense que le code suivant est plus pythonique:

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
la source
0

Je n'aime pas les boucles. Basé sur @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()
Sébastien Wagner
la source
Quelle raison possible existe-t-il pour remplacer une boucle simple et claire par une abberation functools.reduce contenant plusieurs lambdas? Je ne sais pas s'il existe une convention sur la programmation qui n'a pas été rompue.
Naltharial
Mon principal problème était que l' hashlibAPI de s ne fonctionne pas vraiment bien avec le reste de Python. Par exemple, prenons shutil.copyfileobjce qui échoue de près. Ma prochaine idée était fold(aka reduce) de plier les itérables ensemble en objets uniques. Comme par exemple un hachage. hashlibne fournit pas d'opérateurs, ce qui rend cela un peu compliqué. Néanmoins, nous plions un iterables ici.
Sebastian Wagner
-1

Implémentation de la réponse acceptée pour 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()
esclave
la source
-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
la source
1
s'il vous plaît, formatez le code dans la réponse et lisez cette section avant de donner des réponses: stackoverflow.com/help/how-to-answer
Farside
1
Cela ne fonctionnera pas correctement car il lit le fichier en mode texte ligne par ligne, puis le manipule et imprime le md5 de chaque ligne dépouillée et encodée!
Steve Barnes
-4

Je ne suis pas sûr qu'il n'y ait pas trop de tracas ici. J'ai récemment eu des problèmes avec md5 et des fichiers stockés sous forme de blobs sur MySQL, alors j'ai expérimenté différentes tailles de fichiers et l'approche simple de Python, à savoir:

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

Je n'ai pu détecter aucune différence de performance notable avec une plage de tailles de fichiers allant de 2 Ko à 20 Mo et donc pas besoin de «fragmenter» le hachage. Quoi qu'il en soit, si Linux doit aller sur disque, il le fera probablement au moins aussi bien que la capacité du programmeur moyen de l'empêcher de le faire. En fait, le problème n'avait rien à voir avec md5. Si vous utilisez MySQL, n'oubliez pas les fonctions md5 () et sha1 () déjà présentes.

user2099484
la source
2
Cela ne répond pas à la question et 20 Mo sont à peine considérés comme un très gros fichier qui peut ne pas rentrer dans la RAM, comme indiqué ici.
Chris