Nhận băm MD5 của các tệp lớn trong Python


188

Tôi đã sử dụng hashlib (thay thế md5 trong Python 2.6 / 3.0) và nó hoạt động tốt nếu tôi mở một tệp và đưa nội dung của nó vào hashlib.md5()chức năng.

Vấn đề là với các tệp rất lớn mà kích thước của chúng có thể vượt quá kích thước RAM.

Làm cách nào để lấy băm MD5 của một tệp mà không tải toàn bộ tệp vào bộ nhớ?


20
Tôi sẽ viết lại: "Làm thế nào để MD5 có một tệp mà không tải toàn bộ tệp vào bộ nhớ?"
XtL

Câu trả lời:


147

Chia tệp thành các khối 8192 byte (hoặc một số bội số khác của 128 byte) và đưa chúng vào MD5 liên tiếp bằng cách sử dụng update().

Điều này lợi dụng thực tế là MD5 có các khối digest 128 byte (8192 là 128 × 64). Vì bạn không đọc toàn bộ tệp vào bộ nhớ, nên điều này sẽ không sử dụng nhiều hơn 8192 byte bộ nhớ.

Trong Python 3.8+ bạn có thể làm

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

81
Bạn có thể sử dụng hiệu quả kích thước khối của bất kỳ bội số nào của 128 (ví dụ 8192, 32768, v.v.) và điều đó sẽ nhanh hơn nhiều so với việc đọc 128 byte mỗi lần.
jmanning2k

40
Cảm ơn jmanning2k về lưu ý quan trọng này, một thử nghiệm trên tệp 184 MB mất (0m9.230s, 0m2.547s, 0m2.429s) bằng cách sử dụng (128, 8192, 32768), tôi sẽ sử dụng 8192 vì giá trị cao hơn cho thấy ảnh hưởng không đáng kể.
JustRegisterMe

Nếu bạn có thể, bạn nên sử dụng hashlib.blake2bthay vì md5. Không giống như MD5, BLAKE2 an toàn và thậm chí còn nhanh hơn.
Boris

2
@Boris, bạn thực sự không thể nói rằng BLAKE2 an toàn. Tất cả những gì bạn có thể nói là nó chưa bị phá vỡ.
vy32

@ vy32 bạn không thể nói nó chắc chắn sẽ bị phá vỡ. Chúng ta sẽ thấy trong 100 năm, nhưng ít nhất là tốt hơn MD5, điều này chắc chắn không an toàn.
Boris

220

Bạn cần đọc các tập tin trong khối có kích thước phù hợp:

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

LƯU Ý: Đảm bảo bạn mở tệp của mình bằng 'rb' để mở - nếu không bạn sẽ nhận được kết quả sai.

Vì vậy, để thực hiện toàn bộ lô trong một phương thức - sử dụng một cái gì đó như:

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

Bản cập nhật ở trên dựa trên các nhận xét được cung cấp bởi Frerich Raabe - và tôi đã thử nghiệm điều này và thấy nó là chính xác khi cài đặt cửa sổ Python 2.7.2 của tôi

Tôi đã kiểm tra chéo kết quả bằng công cụ 'jacksum'.

jacksum -a md5 <filename>

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


29
Điều quan trọng cần lưu ý là tệp được truyền cho hàm này phải được mở ở chế độ nhị phân, tức là bằng cách chuyển rbđến openhàm.
Frerich Raabe

11
Đây là một bổ sung đơn giản, nhưng sử dụng hexdigestthay vì digestsẽ tạo ra một hàm băm thập lục phân "trông" giống như hầu hết các ví dụ về băm.
tchaymore

Có nên không if len(data) < block_size: break?
Erik Kaplun

2
Erik, không, tại sao nó lại như vậy? Mục tiêu là cung cấp tất cả các byte cho MD5, cho đến khi kết thúc tệp. Bắt một khối một phần không có nghĩa là tất cả các byte không nên được đưa vào tổng kiểm tra.

2
@ user2084795 open luôn mở một tệp xử lý tệp mới với vị trí được đặt thành bắt đầu của tệp, (trừ khi bạn mở tệp để chắp thêm).
Steve Barnes

110

Dưới đây tôi đã kết hợp đề xuất từ ​​ý kiến. Cảm ơn bạn

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

trăn 3,8 trở lên

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

bài gốc

nếu bạn quan tâm đến cách đọc tệp pythonic (không 'trong khi True'), hãy kiểm tra mã này:

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

Lưu ý rằng func iter () cần một chuỗi byte trống để trình lặp được trả về tạm dừng tại EOF, vì read () trả về b '' (không chỉ '').


17
Vẫn tốt hơn, sử dụng một cái gì đó như 128*md5.block_sizethay vì 8192.
mrkj

1
mrkj: Tôi nghĩ điều quan trọng hơn là chọn kích thước khối đọc của bạn dựa trên đĩa của bạn và sau đó để đảm bảo rằng nó là bội số của md5.block_size.
Harvey

6
các b''cú pháp là mới với tôi. Giải thích ở đây .
cod3monk3y

1
@ThorSummoner: Không thực sự, nhưng từ công việc tìm kiếm kích thước khối tối ưu cho bộ nhớ flash, tôi khuyên bạn chỉ nên chọn một số như 32k hoặc thứ gì đó dễ dàng chia hết cho 4, 8 hoặc 16k. Ví dụ: nếu kích thước khối của bạn là 8k, đọc 32k sẽ là 4 lần đọc ở kích thước khối chính xác. Nếu là 16, thì 2. Nhưng trong mỗi trường hợp, chúng tôi vẫn tốt vì chúng tôi đang đọc một số nguyên nhiều số khối.
Harvey

1
"Trong khi True" là khá pythonic.
Jürgen A. Erhard

49

Đây là phiên bản của phương thức @Piotr Czapla của tôi:

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

30

Sử dụng nhiều bình luận / câu trả lời trong chủ đề này, đây là giải pháp của tôi:

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()
  • Đây là "pythonic"
  • Đây là một chức năng
  • Nó tránh các giá trị ngầm định: luôn thích những giá trị rõ ràng.
  • Nó cho phép tối ưu hóa (rất quan trọng) hiệu suất

Và cuối cùng,

- Điều này đã được xây dựng bởi một cộng đồng, cảm ơn tất cả những lời khuyên / ý tưởng của bạn.


3
Một đề xuất: làm cho đối tượng md5 của bạn trở thành một tham số tùy chọn của hàm để cho phép các hàm băm thay thế, chẳng hạn như sha256 để dễ dàng thay thế MD5. Tôi cũng sẽ đề xuất điều này như là một chỉnh sửa.
Hawkwing

1
Ngoài ra: tiêu hóa không phải là con người có thể đọc được. hexdigest () cho phép đầu ra dễ hiểu hơn, thường có thể ghi lại cũng như trao đổi băm dễ dàng hơn
Hawkwing

Các định dạng băm khác nằm ngoài phạm vi của câu hỏi, nhưng gợi ý có liên quan đến một hàm chung hơn. Tôi đã thêm tùy chọn "con người có thể đọc được" theo đề xuất thứ 2 của bạn.
Bastien Semene

Bạn có thể giải thích về cách thức hoạt động của 'hr' ở đây không?
EnemyBagJones

@EnemyBagJones 'hr' là viết tắt của con người có thể đọc được. Nó trả về một chuỗi gồm 32 chữ số char chiều dài hệ thập lục phân: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene

8

Giải pháp di động Python 2/3

Để tính toán tổng kiểm tra (md5, sha1, v.v.), bạn phải mở tệp ở chế độ nhị phân, vì bạn sẽ tính tổng các giá trị byte:

Để có thể mang theo py27 / py3, bạn nên sử dụng các iogói, như thế này:

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

Nếu tệp của bạn lớn, bạn có thể thích đọc tệp theo từng đoạn để tránh lưu trữ toàn bộ nội dung tệp trong bộ nhớ:

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

Mẹo ở đây là sử dụng iter()hàm với sentinel (chuỗi rỗng).

Trình lặp được tạo trong trường hợp này sẽ gọi o [hàm lambda] không có đối số cho mỗi lệnh gọi đến next()phương thức của nó ; nếu giá trị được trả về bằng sentinel, StopIterationsẽ được nâng lên, nếu không giá trị sẽ được trả về.

Nếu các tệp của bạn thực sự lớn, bạn cũng có thể cần hiển thị thông tin tiến trình. Bạn có thể làm điều đó bằng cách gọi một hàm gọi lại in hoặc ghi lại số lượng byte được tính toán:

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

3

Một bản phối lại của mã Bastien Semene đưa nhận xét của Hawkwing về chức năng băm chung ...

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

0

bạn không thể lấy nó md5 mà không đọc nội dung đầy đủ. nhưng bạn có thể sử dụng chức năng cập nhật để đọc khối nội dung tệp theo từng khối.
m.update (a); m.update (b) tương đương với m.update (a + b)


0

Tôi nghĩ đoạn mã sau có tính chất pythonic hơn:

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

-1

Thực hiện câu trả lời được chấp nhận cho 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()

-1

Tôi không thích các vòng lặp. Dựa trên @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()

Lý do nào có thể có ở đó để thay thế một vòng lặp đơn giản và rõ ràng bằng một funcools.reduce abberation có chứa nhiều lambdas? Tôi không chắc chắn nếu có bất kỳ quy ước về lập trình này đã không bị phá vỡ.
Naltharial

Vấn đề chính của tôi là hashlibAPI không thực sự chơi tốt với phần còn lại của Python. Ví dụ, hãy xem shutil.copyfileobjcái nào không hoạt động. Ý tưởng tiếp theo của tôi là fold(aka reduce) có thể gấp các lần lặp lại với nhau thành các đối tượng đơn lẻ. Ví dụ như một hàm băm. hashlibkhông cung cấp các toán tử làm cho điều này hơi cồng kềnh. Tuy nhiên, đã gấp một vòng lặp ở đây.
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

1
xin vui lòng, định dạng mã trong câu trả lời và đọc phần này trước khi đưa ra câu trả lời: stackoverflow.com/help/how-to-answer
Farside

1
Điều này sẽ không hoạt động chính xác vì nó đang đọc tệp trong dòng chế độ văn bản theo dòng sau đó làm rối nó và in md5 của mỗi dòng bị tước, mã hóa, dòng!
Steve Barnes

-4

Tôi không chắc chắn rằng không có quá nhiều sự ồn ào quanh đây. Gần đây tôi gặp vấn đề với md5 và các tệp được lưu trữ dưới dạng blobs trên MySQL vì vậy tôi đã thử nghiệm với nhiều kích cỡ tệp khác nhau và cách tiếp cận Python đơn giản, viz:

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

Tôi có thể phát hiện không có sự khác biệt hiệu suất đáng chú ý với một phạm vi kích thước tệp 2Kb đến 20Mb và do đó không cần phải 'băm' băm. Dù sao, nếu Linux phải vào đĩa, có lẽ nó sẽ làm điều đó ít nhất cũng như khả năng của lập trình viên trung bình để giữ cho nó không làm như vậy. Như đã xảy ra, vấn đề không liên quan gì đến md5. Nếu bạn đang sử dụng MySQL, đừng quên các hàm md5 () và sha1 () đã có.


2
Đây không phải là trả lời câu hỏi và 20 MB hầu như không được coi là một tệp rất lớn có thể không phù hợp với RAM như được thảo luận ở đây.
Chris
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.