Dapatkan MD5 hash file besar dengan Python

188

Saya telah menggunakan hashlib (yang menggantikan md5 dengan Python 2.6 / 3.0) dan berfungsi dengan baik jika saya membuka file dan meletakkan kontennya dalam hashlib.md5()fungsi.

Masalahnya dengan file yang sangat besar ukurannya bisa melebihi ukuran RAM.

Bagaimana cara mendapatkan hash MD5 file tanpa memuat seluruh file ke memori?

JustRegisterMe
sumber
20
Saya akan ulangi: "Bagaimana mendapatkan file MD5 tanpa memuat seluruh file ke memori?"
XTL

Jawaban:

147

Pecah file menjadi potongan 8192 byte (atau kelipatan 128 byte lainnya) dan berikan mereka ke MD5 secara berturut-turut menggunakan update().

Ini mengambil keuntungan dari fakta bahwa MD5 memiliki blok digest 128-byte (8192 adalah 128 × 64). Karena Anda tidak membaca seluruh file ke dalam memori, ini tidak akan menggunakan lebih dari 8192 byte memori.

Dengan Python 3.8+ Anda bisa melakukannya

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
sumber
81
Anda dapat secara efektif menggunakan ukuran blok kelipatan 128 (katakanlah 8192, 32768, dll.) Dan itu akan jauh lebih cepat daripada membaca 128 byte pada suatu waktu.
jmanning2k
40
Terima kasih jmanning2k untuk catatan penting ini, tes pada file 184MB memakan waktu (0m9.230s, 0m2.547s, 0m2.429s) menggunakan (128, 8192, 32768), saya akan menggunakan 8192 karena nilai yang lebih tinggi memberikan pengaruh yang tidak terlihat.
JustRegisterMe
Jika Anda bisa, Anda harus menggunakan hashlib.blake2bbukan md5. Tidak seperti MD5, BLAKE2 aman, dan bahkan lebih cepat.
Boris
2
@ Boris, Anda tidak bisa mengatakan bahwa BLAKE2 aman. Yang bisa Anda katakan adalah bahwa itu belum rusak.
vy32
@ vy32 Anda tidak bisa mengatakan itu pasti akan rusak juga. Kita akan melihat dalam 100 tahun, tapi setidaknya lebih baik daripada MD5 yang pasti tidak aman.
Boris
220

Anda perlu membaca file dalam ukuran yang sesuai:

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

CATATAN: Pastikan Anda membuka file Anda dengan 'rb' di tempat terbuka - jika tidak, Anda akan mendapatkan hasil yang salah.

Jadi untuk melakukan semuanya dalam satu metode - gunakan sesuatu seperti:

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

Pembaruan di atas didasarkan pada komentar yang diberikan oleh Frerich Raabe - dan saya mengujinya dan ternyata benar pada instalasi windows Python 2.7.2 saya

Saya memeriksa ulang hasilnya menggunakan alat 'jacksum'.

jacksum -a md5 <filename>

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

Dokter
sumber
29
Yang penting diperhatikan adalah bahwa file yang diteruskan ke fungsi ini harus dibuka dalam mode biner, yaitu dengan meneruskan rbke openfungsi tersebut.
Frerich Raabe
11
Ini adalah tambahan sederhana, tetapi menggunakan hexdigestbukannya digestakan menghasilkan hash heksadesimal yang "terlihat" seperti kebanyakan contoh hash.
tchaymore
Bukankah seharusnya begitu if len(data) < block_size: break?
Erik Kaplun
2
Erik, tidak, mengapa itu terjadi? Tujuannya adalah untuk memberi makan semua byte ke MD5, hingga akhir file. Mendapatkan blok parsial tidak berarti semua byte tidak boleh diumpankan ke checksum.
2
@ user2084795 open selalu membuka pegangan file baru dengan posisi diatur ke awal file, (kecuali jika Anda membuka file untuk ditambahkan).
Steve Barnes
110

Di bawah ini saya memasukkan saran dari komentar. Terima kasih, 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 dan di atas

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

pos asli

jika Anda peduli dengan cara pythonic (no 'while True') yang lebih banyak untuk membaca file, periksa kode ini:

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

Perhatikan bahwa fungsi iter () membutuhkan string byte kosong agar iterator yang dikembalikan berhenti di EOF, karena read () mengembalikan b '' (bukan hanya '').

Piotr Czapla
sumber
17
Lebih baik lagi, gunakan sesuatu seperti 128*md5.block_sizebukan 8192.
mrkj
1
mrkj: Saya pikir lebih penting untuk memilih ukuran blok baca Anda berdasarkan disk Anda dan kemudian untuk memastikan bahwa itu adalah kelipatan md5.block_size.
Harvey
6
yang b''sintaks baru bagiku. Dijelaskan di sini .
cod3monk3y
1
@ Tumormoner: Tidak juga, tetapi dari pekerjaan saya menemukan ukuran blok optimal untuk memori flash, saya sarankan hanya memilih angka seperti 32k atau sesuatu yang mudah dibagi 4, 8, atau 16k. Misalnya, jika ukuran blok Anda 8k, bacaan 32k akan menjadi 4 bacaan pada ukuran blok yang benar. Jika 16, maka 2. Tetapi dalam setiap kasus, kami baik karena kami kebetulan membaca bilangan bulat beberapa blok.
Harvey
1
"while True" cukup pythonic.
Jürgen A. Erhard
49

Inilah versi metode @Piotr Czapla saya:

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
sumber
30

Menggunakan beberapa komentar / jawaban di utas ini, berikut adalah solusi saya:

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()
  • Ini adalah "pythonic"
  • Ini sebuah fungsi
  • Ini menghindari nilai implisit: selalu lebih suka yang eksplisit.
  • Ini memungkinkan (sangat penting) optimalisasi kinerja

Dan akhirnya,

- Ini dibangun oleh komunitas, terima kasih atas saran / ide Anda.

Bastien Semene
sumber
3
Satu saran: jadikan objek md5 Anda sebagai parameter opsional fungsi untuk memungkinkan fungsi hashing alternatif, seperti sha256 untuk dengan mudah mengganti MD5. Saya akan mengusulkan ini sebagai edit juga.
Hawkwing
1
juga: intisari tidak dapat dibaca manusia. hexdigest () memungkinkan keluaran yang lebih dapat dimengerti, biasanya dapat dikenali lagi serta pertukaran hash yang lebih mudah
Hawkwing
Format hash lainnya berada di luar cakupan pertanyaan, tetapi saran tersebut relevan untuk fungsi yang lebih umum. Saya menambahkan opsi "dapat dibaca manusia" sesuai dengan saran ke-2 Anda.
Bastien Semene
Bisakah Anda menguraikan bagaimana 'jam' berfungsi di sini?
EnemyBagJones
@EnemyBagJones 'hr' adalah singkatan yang dapat dibaca manusia. Ini mengembalikan string 32 digit panjang heksadesimal char: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene
8

Solusi portabel Python 2/3

Untuk menghitung checksum (md5, sha1, dll.), Anda harus membuka file dalam mode biner, karena Anda akan menjumlahkan nilai byte:

Untuk menjadi py27 / py3 portable, Anda harus menggunakan iopaket, seperti ini:

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

Jika file Anda besar, Anda mungkin lebih suka membaca file dengan potongan untuk menghindari menyimpan seluruh konten file dalam memori:

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

Kuncinya di sini adalah menggunakan iter()fungsi dengan sentinel (string kosong).

Iterator yang dibuat dalam kasus ini akan memanggil o [fungsi lambda] tanpa argumen untuk setiap panggilan ke next()metodenya; jika nilai yang dikembalikan sama dengan sentinel, StopIterationakan dinaikkan, jika tidak nilai akan dikembalikan.

Jika file Anda sangat besar, Anda mungkin juga perlu menampilkan informasi kemajuan. Anda dapat melakukannya dengan memanggil fungsi panggilan balik yang mencetak atau mencatat jumlah byte yang dihitung:

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

Sebuah remix dari kode Bastien Semene yang mempertimbangkan komentar Hawkwing tentang fungsi hashing generik menjadi pertimbangan ...

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

Anda tidak bisa mendapatkannya md5 tanpa membaca konten lengkap. tetapi Anda dapat menggunakan fungsi pembaruan untuk membaca konten file blok demi blok.
m.update (a); m.update (b) setara dengan m.update (a + b)

sunqiang
sumber
0

Saya pikir kode berikut ini lebih 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()
Waket Zheng
sumber
-1

Implementasi jawaban yang diterima untuk 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()
lamplave
sumber
-1

Saya tidak suka loop. Berdasarkan @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
sumber
Apa alasan yang mungkin ada untuk mengganti loop sederhana dan jelas dengan functools.reduce abberasi yang mengandung banyak lambda? Saya tidak yakin apakah ada konvensi tentang pemrograman ini tidak rusak.
Naltharial
Masalah utama saya adalah bahwa hashlibs API tidak benar-benar cocok dengan sisa Python. Sebagai contoh mari kita ambil shutil.copyfileobjyang gagal berfungsi. Ide saya berikutnya adalah fold(alias reduce) yang melipat iterables bersama menjadi objek tunggal. Seperti misalnya hash. hashlibtidak menyediakan operator yang membuat ini sedikit rumit. Namun demikian melipat iterables di sini.
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
sumber
1
tolong, format kode dalam jawaban, dan baca bagian ini sebelum memberikan jawaban: stackoverflow.com/help/how-to-answer
Farside
1
Ini tidak akan berfungsi dengan benar karena membaca file dalam mode teks baris demi baris kemudian mengacaukannya dan mencetak md5 dari setiap strip, disandikan, baris!
Steve Barnes
-4

Saya tidak yakin bahwa tidak ada terlalu banyak keributan di sekitar sini. Saya baru-baru ini punya masalah dengan md5 dan file yang disimpan sebagai gumpalan di MySQL jadi saya bereksperimen dengan berbagai ukuran file dan pendekatan Python langsung, yaitu:

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

Saya dapat mendeteksi tidak ada perbedaan kinerja yang nyata dengan kisaran ukuran file 2Kb hingga 20Mb dan karenanya tidak perlu 'memotong' hashing. Bagaimanapun, jika Linux harus pergi ke disk, itu mungkin akan melakukannya setidaknya serta kemampuan rata-rata programmer untuk mencegahnya melakukannya. Ketika itu terjadi, masalahnya tidak ada hubungannya dengan MD5. Jika Anda menggunakan MySQL, jangan lupa fungsi md5 () dan sha1 () sudah ada di sana.

pengguna2099484
sumber
2
Ini tidak menjawab pertanyaan dan 20 MB hampir tidak dianggap sebagai file yang sangat besar yang mungkin tidak cocok dengan RAM seperti yang dibahas di sini.
Chris