Holen Sie sich MD5-Hash von großen Dateien in Python

188

Ich habe hashlib verwendet (das md5 in Python 2.6 / 3.0 ersetzt) ​​und es hat gut funktioniert, wenn ich eine Datei geöffnet und ihren Inhalt in hashlib.md5()Funktion gesetzt habe.

Das Problem ist bei sehr großen Dateien, dass ihre Größe die RAM-Größe überschreiten kann.

Wie erhalte ich den MD5-Hash einer Datei, ohne die gesamte Datei in den Speicher zu laden?

JustRegisterMe
quelle
20
Ich würde umformulieren: "Wie bekomme ich das MD5 von einer Datei, ohne die gesamte Datei in den Speicher zu laden?"
XTL

Antworten:

147

Teilen Sie die Datei in 8192-Byte-Blöcke (oder ein anderes Vielfaches von 128 Byte) auf und führen Sie sie nacheinander mit MD5 ein update().

Dies nutzt die Tatsache aus, dass MD5 128-Byte-Digest-Blöcke hat (8192 ist 128 × 64). Da Sie nicht die gesamte Datei in den Speicher einlesen, werden nicht viel mehr als 8192 Byte Speicher benötigt.

In Python 3.8+ können Sie dies tun

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
quelle
81
Sie können eine Blockgröße mit einem beliebigen Vielfachen von 128 (z. B. 8192, 32768 usw.) genauso effektiv verwenden, und dies ist viel schneller als das Lesen von jeweils 128 Byte.
jmanning2k
40
Vielen Dank an jmanning2k für diesen wichtigen Hinweis. Ein Test für 184 MB-Dateien dauert (0m9.230s, 0m2.547s, 0m2.429s) mit (128, 8192, 32768). Ich werde 8192 verwenden, da der höhere Wert einen nicht wahrnehmbaren Effekt ergibt.
JustRegisterMe
Wenn Sie können, sollten Sie hashlib.blake2banstelle von verwenden md5. Im Gegensatz zu MD5 ist BLAKE2 sicher und noch schneller.
Boris
2
@ Boris, man kann eigentlich nicht sagen, dass BLAKE2 sicher ist. Sie können nur sagen, dass es noch nicht kaputt ist.
vy32
@ vy32 man kann nicht sagen, dass es definitiv auch kaputt gehen wird. Wir werden in 100 Jahren sehen, aber es ist zumindest besser als MD5, was definitiv unsicher ist.
Boris
220

Sie müssen die Datei in Blöcken geeigneter Größe lesen:

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

HINWEIS: Stellen Sie sicher, dass Sie Ihre Datei mit dem 'rb' zum Öffnen öffnen - andernfalls erhalten Sie das falsche Ergebnis.

Um das Ganze in einer Methode zu erledigen, verwenden Sie Folgendes:

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

Das obige Update basiert auf den Kommentaren von Frerich Raabe - und ich habe dies getestet und festgestellt, dass es in meiner Python 2.7.2-Windows-Installation korrekt ist

Ich habe die Ergebnisse mit dem Tool 'jacksum' überprüft.

jacksum -a md5 <filename>

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

Der Doktor
quelle
29
Es ist wichtig zu beachten, dass die Datei, die an diese Funktion übergeben wird, im Binärmodus geöffnet werden muss, dh durch Übergabe rban die openFunktion.
Frerich Raabe
11
Dies ist eine einfache Ergänzung, aber die Verwendung von hexdigestanstelle von digesterzeugt einen hexadezimalen Hash, der wie die meisten Beispiele für Hashes "aussieht".
Tchaymore
Sollte es nicht sein if len(data) < block_size: break?
Erik Kaplun
2
Erik, nein, warum sollte es so sein? Ziel ist es, alle Bytes bis zum Ende der Datei an MD5 weiterzuleiten. Das Erhalten eines Teilblocks bedeutet nicht, dass nicht alle Bytes der Prüfsumme zugeführt werden sollten.
2
@ user2084795 öffnet open immer ein neues Dateihandle mit der Position am Anfang der Datei (es sei denn, Sie öffnen eine Datei zum Anhängen).
Steve Barnes
110

Unten habe ich Vorschläge aus Kommentaren aufgenommen. Vielen Dank 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 und höher

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

ursprünglicher Beitrag

Wenn Sie mehr pythonische (kein 'while True') Methoden zum Lesen der Datei bevorzugen, überprüfen Sie diesen 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()

Beachten Sie, dass die Funktion iter () eine leere Bytezeichenfolge benötigt, damit der zurückgegebene Iterator bei EOF anhält, da read () b '' (nicht nur '') zurückgibt.

Piotr Czapla
quelle
17
Besser noch, verwenden Sie etwas wie 128*md5.block_sizeanstelle von 8192.
Mrkj
1
mrkj: Ich denke, es ist wichtiger, die Größe Ihres Leseblocks basierend auf Ihrer Festplatte auszuwählen und dann sicherzustellen, dass es ein Vielfaches von ist md5.block_size.
Harvey
6
Die b''Syntax war neu für mich. Erklärt hier .
cod3monk3y
1
@ThorSummoner: Nicht wirklich, aber aufgrund meiner Arbeit bei der Suche nach optimalen Blockgrößen für den Flash-Speicher würde ich vorschlagen, nur eine Zahl wie 32k oder etwas zu wählen, das leicht durch 4, 8 oder 16k teilbar ist. Wenn Ihre Blockgröße beispielsweise 8 KB beträgt, entspricht das Lesen von 32 KB 4 Lesevorgängen bei der richtigen Blockgröße. Wenn es 16 ist, dann 2. Aber in jedem Fall sind wir gut, weil wir zufällig eine ganzzahlige Anzahl von Blöcken lesen.
Harvey
1
"while True" ist ziemlich pythonisch.
Jürgen A. Erhard
49

Hier ist meine Version der Methode von @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
quelle
30

Mit mehreren Kommentaren / Antworten in diesem Thread ist hier meine Lösung:

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()
  • Das ist "pythonisch"
  • Dies ist eine Funktion
  • Implizite Werte werden vermieden: Bevorzugen Sie immer explizite.
  • Es ermöglicht (sehr wichtige) Leistungsoptimierungen

Und schlussendlich,

- Dies wurde von einer Community erstellt. Vielen Dank für Ihre Ratschläge / Ideen.

Bastien Semene
quelle
3
Ein Vorschlag: Machen Sie Ihr md5-Objekt zu einem optionalen Parameter der Funktion, damit alternative Hashing-Funktionen wie sha256 MD5 problemlos ersetzen können. Ich werde dies auch als Bearbeitung vorschlagen.
Hawkwing
1
auch: verdauung ist nicht lesbar. hexdigest () ermöglicht eine verständlichere, allgemein erkennbare Ausgabe sowie einen einfacheren Austausch des Hash
Hawkwing
Andere Hash-Formate sind nicht Gegenstand der Frage, aber der Vorschlag ist für eine allgemeinere Funktion relevant. Ich habe eine "für Menschen lesbare" Option gemäß Ihrem zweiten Vorschlag hinzugefügt.
Bastien Semene
Können Sie näher erläutern, wie 'hr' hier funktioniert?
EnemyBagJones
@EnemyBagJones 'hr' steht für menschlich lesbar. Es gibt eine Zeichenfolge mit 32 hexadezimalen Ziffern zurück: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene
8

Eine tragbare Python 2/3-Lösung

Um eine Prüfsumme (md5, sha1 usw.) zu berechnen, müssen Sie die Datei im Binärmodus öffnen, da Sie Bytewerte summieren:

Um py27 / py3 portabel zu sein, sollten Sie die ioPakete wie folgt verwenden :

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

Wenn Ihre Dateien groß sind, ziehen Sie es möglicherweise vor, die Datei in Blöcken zu lesen, um zu vermeiden, dass der gesamte Dateiinhalt im Speicher gespeichert wird:

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

Der Trick hier besteht darin, die iter()Funktion mit einem Sentinel (der leeren Zeichenfolge) zu verwenden.

Der in diesem Fall erstellte Iterator ruft o [die Lambda-Funktion] ohne Argumente für jeden Aufruf seiner next()Methode auf. Wenn der zurückgegebene Wert gleich Sentinel ist, StopIterationwird er erhöht, andernfalls wird der Wert zurückgegeben.

Wenn Ihre Dateien wirklich groß sind, müssen Sie möglicherweise auch Fortschrittsinformationen anzeigen. Sie können dies tun, indem Sie eine Rückruffunktion aufrufen, die die Anzahl der berechneten Bytes druckt oder protokolliert:

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
quelle
3

Ein Remix von Bastien Semene-Code, der Hawkwing-Kommentare zur generischen Hashing-Funktion berücksichtigt ...

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
quelle
0

Sie können es nicht md5 bekommen, ohne den vollständigen Inhalt zu lesen. Sie können jedoch die Aktualisierungsfunktion verwenden, um den Dateiinhalt Block für Block zu lesen.
m.update (a); m.update (b) entspricht m.update (a + b)

Sunqiang
quelle
0

Ich denke, der folgende Code ist pythonischer:

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
quelle
-1

Implementierung der akzeptierten Antwort für 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()
Lampensklave
quelle
-1

Ich mag keine Loops. Basierend auf @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
quelle
Welchen möglichen Grund gibt es, eine einfache und übersichtliche Schleife durch eine functools.reduce-Abberation zu ersetzen, die mehrere Lambdas enthält? Ich bin mir nicht sicher, ob es eine Konvention zur Programmierung gibt, die nicht gebrochen wurde.
Naltharial
Mein Hauptproblem war, dass die hashlibAPI mit dem Rest von Python nicht wirklich gut funktioniert. Nehmen wir zum Beispiel, shutil.copyfileobjwas genau nicht funktioniert. Meine nächste Idee war fold(aka reduce), die iterables zu einzelnen Objekten zusammenfaltet. Wie zB ein Hash. hashlibbietet keine Operatoren, was dies etwas umständlich macht. Trotzdem falteten sich hier iterable.
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
quelle
1
Bitte formatieren Sie den Code in der Antwort und lesen Sie diesen Abschnitt, bevor Sie Antworten geben: stackoverflow.com/help/how-to-answer
Farside
1
Dies funktioniert nicht richtig, da die Datei im Textmodus Zeile für Zeile gelesen wird, dann damit herumgespielt wird und der MD5 jeder abisolierten, codierten Zeile gedruckt wird!
Steve Barnes
-4

Ich bin mir nicht sicher, ob es hier nicht zu viel Aufhebens gibt. Ich hatte kürzlich Probleme mit md5 und Dateien, die als Blobs auf MySQL gespeichert waren, also experimentierte ich mit verschiedenen Dateigrößen und dem einfachen Python-Ansatz, nämlich:

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

Ich konnte keinen merklichen Leistungsunterschied bei einem Bereich von Dateigrößen von 2 KB bis 20 MB feststellen und musste daher das Hashing nicht "aufteilen". Wenn Linux auf die Festplatte gehen muss, wird es dies wahrscheinlich mindestens genauso gut tun wie die Fähigkeit des durchschnittlichen Programmierers, dies zu verhindern. Das Problem hatte übrigens nichts mit md5 zu tun. Wenn Sie MySQL verwenden, vergessen Sie nicht die bereits vorhandenen Funktionen md5 () und sha1 ().

user2099484
quelle
2
Dies beantwortet die Frage nicht und 20 MB werden kaum als sehr große Datei angesehen , die möglicherweise nicht in den RAM passt, wie hier beschrieben.
Chris