รับ MD5 แฮชของไฟล์ขนาดใหญ่ใน Python


188

ฉันใช้ hashlib (ซึ่งแทนที่ md5 ใน Python 2.6 / 3.0) และใช้งานได้ดีถ้าฉันเปิดไฟล์และวางเนื้อหาไว้ในhashlib.md5()ฟังก์ชั่น

ปัญหาคือไฟล์ที่มีขนาดใหญ่มากซึ่งขนาดอาจเกินขนาด RAM

วิธีรับ MD5 hash ของไฟล์โดยไม่โหลดทั้งไฟล์ลงในหน่วยความจำ


20
ฉันจะใช้ถ้อยคำใหม่: "วิธีทำให้ MD5 มีไฟล์โดยไม่โหลดไฟล์ทั้งหมดลงในหน่วยความจำ"
XTL

คำตอบ:


147

แบ่งไฟล์เป็นชิ้น 8192 ไบต์ (หรือบางหลายอื่น ๆ ของ 128 bytes) update()และอาหารให้พวกเขาอย่างต่อเนื่องโดยใช้

สิ่งนี้ใช้ประโยชน์จากข้อเท็จจริงที่ว่า MD5 มีบล็อกย่อยย่อย 128- ไบต์ (8192 คือ 128 × 64) เนื่องจากคุณไม่ได้อ่านไฟล์ทั้งหมดในหน่วยความจำนี่จะไม่ใช้หน่วยความจำมากกว่า 8192 ไบต์

ใน Python 3.8+ คุณสามารถทำได้

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
คุณสามารถใช้ขนาดบล็อกได้อย่างมีประสิทธิภาพเท่ากับ 128 (เช่น 8192, 32768, ฯลฯ ) และจะเร็วกว่าการอ่าน 128 ไบต์ในแต่ละครั้ง
jmanning2k

40
ขอบคุณ jmanning2k สำหรับบันทึกย่อที่สำคัญนี้การทดสอบไฟล์ 184MB นั้นใช้เวลา (0m9.230s, 0m2.547s, 0m2.429s) โดยใช้ (128, 8192, 32768) ฉันจะใช้ 8192 เนื่องจากค่าที่สูงกว่าจะส่งผลกระทบที่ไม่สามารถสังเกตเห็นได้
JustRegisterMe

หากคุณสามารถคุณควรใช้แทนhashlib.blake2b BLAKE2นั้นmd5แตกต่างจาก MD5 และปลอดภัยยิ่งขึ้นและเร็วยิ่งขึ้น
บอริส

2
@ บอริสคุณไม่สามารถพูดได้จริง ๆ ว่า BLAKE2 ปลอดภัย ทั้งหมดที่คุณพูดได้ก็คือมันยังไม่พัง
vy32

@ vy32 คุณไม่สามารถพูดได้ว่ามันจะถูกทำลายอย่างแน่นอน เราจะเห็นใน 100 ปี แต่อย่างน้อยก็ดีกว่า MD5 ซึ่งไม่ปลอดภัยอย่างแน่นอน
Boris

220

คุณต้องอ่านไฟล์ในขนาดที่เหมาะสม:

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

หมายเหตุ: ตรวจสอบให้แน่ใจว่าคุณเปิดไฟล์โดยใช้ 'rb' เพื่อเปิดมิฉะนั้นคุณจะได้ผลลัพธ์ที่ผิด

ดังนั้นในการทำล็อตเดียวให้ใช้วิธีดังต่อไปนี้

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

การอัปเดตข้างต้นขึ้นอยู่กับความคิดเห็นที่จัดทำโดย Frerich Raabe - และฉันทดสอบและพบว่าถูกต้องในการติดตั้ง Python 2.7.2 windows ของฉัน

ฉันข้ามการตรวจสอบผลลัพธ์โดยใช้เครื่องมือ 'jacksum'

jacksum -a md5 <filename>

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


29
สิ่งสำคัญที่ควรสังเกตคือไฟล์ที่ถูกส่งผ่านไปยังฟังก์ชั่นนี้จะต้องเปิดในโหมดไบนารีเช่นโดยผ่านrbไปยังopenฟังก์ชั่น
Frerich Raabe

11
นี่คือการเพิ่มอย่างง่าย ๆ แต่การใช้hexdigestแทนที่จะเป็นการdigestสร้างแฮชเลขฐานสิบหกที่ "ดูเหมือน" จะเป็นตัวอย่างของแฮชส่วนใหญ่
tchaymore

มันไม่ควรจะเป็นif len(data) < block_size: break?
Erik Kaplun

2
เอริคไม่ทำไมต้องเป็นเช่นนั้น? เป้าหมายคือการป้อนข้อมูลไบต์ทั้งหมดไปยัง MD5 จนถึงจุดสิ้นสุดของไฟล์ การรับบล็อกบางส่วนไม่ได้หมายความว่าไบต์ทั้งหมดไม่ควรป้อนเข้าสู่การตรวจสอบ

2
@ user2084795 เปิดตัวจัดการไฟล์ใหม่open เสมอโดยกำหนดตำแหน่งไว้ที่จุดเริ่มต้นของไฟล์(เว้นแต่คุณจะเปิดไฟล์เพื่อต่อท้าย)
Steve Barnes

110

ด้านล่างฉันได้รวมข้อเสนอแนะจากความคิดเห็น ขอบคุณมาก!

หลาม <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()

หลาม 3.8 ขึ้นไป

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

โพสต์ต้นฉบับ

หากคุณสนใจวิธีการอ่าน pythonic เพิ่มเติม (ไม่ 'ในขณะที่ True') ให้ตรวจสอบรหัสนี้:

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

โปรดทราบว่า iter () func ต้องการสตริงไบต์ว่างเพื่อให้ตัววนซ้ำกลับมาหยุดที่ EOF เนื่องจาก read () ส่งคืน b '' (ไม่ใช่แค่ '')


17
ยังดีกว่าใช้สิ่งที่ต้องการแทน128*md5.block_size 8192
mrkj

1
mrkj: ฉันคิดว่ามันสำคัญกว่าที่จะเลือกขนาดบล็อกการอ่านของคุณตามดิสก์ของคุณและเพื่อให้แน่ใจว่ามันมีหลายmd5.block_sizeขนาด
ฮาร์วีย์

6
b''ไวยากรณ์เป็นใหม่ให้ฉัน อธิบายที่นี่
cod3monk3y

1
@ThorSummoner: ไม่จริง แต่จากการทำงานของฉันในการหาขนาดบล็อกที่เหมาะสมสำหรับหน่วยความจำแฟลชฉันขอแนะนำให้เลือกจำนวนเช่น 32k หรือบางสิ่งบางอย่างหารด้วย 4, 8 หรือ 16k ได้อย่างง่ายดาย ตัวอย่างเช่นหากขนาดบล็อกของคุณคือ 8k การอ่าน 32k จะเป็นการอ่าน 4 ครั้งที่ขนาดบล็อกที่ถูกต้อง ถ้าเป็น 16 แล้ว 2 แต่ในแต่ละกรณีเราดีเพราะเราอ่านบล็อกจำนวนเต็มหลายบล็อก
ฮาร์วีย์

1
"ในขณะที่ True" ค่อนข้างไพเราะ
Jürgen A. Erhard

49

นี่คือวิธีการของ @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()

30

ใช้ความคิดเห็น / คำตอบหลายรายการในกระทู้นี้นี่คือทางออกของฉัน:

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()
  • นี่คือ "pythonic"
  • นี่คือฟังก์ชั่น
  • มันหลีกเลี่ยงค่าโดยนัย: ชอบค่าที่ชัดเจนเสมอ
  • จะช่วยให้การเพิ่มประสิทธิภาพการแสดง (สำคัญมาก)

และในที่สุดก็,

- ชุมชนนี้สร้างขึ้นขอบคุณสำหรับคำแนะนำ / ความคิดเห็นของคุณ


3
คำแนะนำเดียว: ทำให้วัตถุ md5 ของคุณเป็นพารามิเตอร์ที่เป็นตัวเลือกของฟังก์ชันเพื่ออนุญาตให้ฟังก์ชันการแปลงแป้นพิมพ์แบบสลับเช่น sha256 เปลี่ยน MD5 ได้อย่างง่ายดาย ฉันจะเสนอสิ่งนี้เป็นการแก้ไขเช่นกัน
Hawkwing

1
ด้วย: ย่อยไม่สามารถอ่านได้ของมนุษย์ hexdigest () ช่วยให้สามารถเข้าใจผลลัพธ์ได้มากกว่าปกติและสามารถแลกเปลี่ยนแฮชได้ง่ายขึ้น
Hawkwing

รูปแบบแฮชอื่น ๆ นั้นอยู่นอกขอบเขตของคำถาม แต่ข้อเสนอแนะนั้นเกี่ยวข้องกับฟังก์ชันทั่วไปมากกว่า ฉันเพิ่มตัวเลือก "ที่มนุษย์อ่านได้" ตามคำแนะนำที่สองของคุณ
Bastien Semene

คุณช่วยอธิบายได้ไหมว่า 'hr' ทำงานที่นี่ได้อย่างไร?
EnemyBagJones

@EnemyBagJones 'hr' ย่อมาจาก Human readable มันจะส่งคืนสตริงของเลขฐานสิบหกที่มีความยาว 32 อักขระ: docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene

8

โซลูชันพกพา Python 2/3

ในการคำนวณ checksum (md5, sha1 ฯลฯ ) คุณจะต้องเปิดไฟล์ในโหมดไบนารีเพราะคุณจะหาผลรวมของค่าไบต์:

ในการเป็นแบบพกพา py27 / py3 คุณควรใช้ioแพ็คเกจเช่นนี้:

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

หากไฟล์ของคุณมีขนาดใหญ่คุณอาจต้องการอ่านไฟล์ด้วยชิ้นส่วนเพื่อหลีกเลี่ยงการจัดเก็บเนื้อหาไฟล์ทั้งหมดในหน่วยความจำ:

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

เคล็ดลับที่นี่คือการใช้iter()ฟังก์ชั่นกับแมวมอง (สตริงว่าง)

ตัววนซ้ำที่สร้างขึ้นในกรณีนี้จะเรียกo [ฟังก์ชัน lambda] โดยไม่มีอาร์กิวเมนต์สำหรับการเรียกแต่ละครั้งไปยังnext()วิธีการ หากค่าที่ส่งคืนเท่ากับ Sentinel StopIterationจะได้รับการเพิ่มขึ้นมิฉะนั้นค่าจะถูกส่งคืน

หากไฟล์ของคุณอยู่จริงๆใหญ่คุณอาจต้องข้อมูลความคืบหน้าการแสดงผล คุณสามารถทำได้โดยการเรียกฟังก์ชั่นโทรกลับซึ่งพิมพ์หรือบันทึกจำนวนไบต์ที่คำนวณได้:

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

รีมิกซ์ของโค้ด Bastien Semene ที่นำความคิดเห็น Hawkwing เกี่ยวกับฟังก์ชันการแปลงแป้นพิมพ์ทั่วไปมาพิจารณา ...

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

คุณไม่สามารถรับ md5 ได้หากไม่ได้อ่านเนื้อหาเต็ม แต่คุณสามารถใช้ฟังก์ชั่นอัปเดตเพื่ออ่านบล็อกเนื้อหาไฟล์โดยบล็อก
m.update (ก); m.update (b) เทียบเท่ากับ m.update (a + b)


0

ฉันคิดว่ารหัสต่อไปนี้เป็น 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()

-1

การใช้คำตอบที่ยอมรับสำหรับ 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

ฉันไม่ชอบลูป อิงจาก @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()

มีเหตุผลอะไรบ้างที่จะมีการแทนที่ลูปที่เรียบง่ายและชัดเจนด้วย functools.reduce abberation ที่มี lambdas หลายอัน? ฉันไม่แน่ใจว่ามีการประชุมเรื่องการเขียนโปรแกรมหรือไม่
Naltharial

ปัญหาหลักของฉันคือhashlibAPI ของ API นั้นเล่นได้ไม่ดีนักกับ Python ที่เหลือ ตัวอย่างเช่นลองทำshutil.copyfileobjสิ่งที่ล้มเหลวในการทำงานอย่างใกล้ชิด แนวคิดต่อไปของฉันคือfold(aka reduce) ซึ่งพับได้รวมกันเป็นวัตถุเดียว ชอบแฮช hashlibไม่ได้ให้ตัวดำเนินการซึ่งทำให้ยุ่งยากเล็กน้อย อย่างไรก็ตามมีการพับ iterables ที่นี่
เซบาสเตียนวากเนอร์

-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
โปรดจัดรูปแบบโค้ดในคำตอบและอ่านหัวข้อนี้ก่อนที่จะตอบคำตอบ: stackoverflow.com/help/how-to-answer
Farside

1
สิ่งนี้จะทำงานไม่ถูกต้องเนื่องจากกำลังอ่านไฟล์ในโหมดข้อความทีละบรรทัดจากนั้นก็ยุ่งกับมันและพิมพ์ md5 ของแต่ละบรรทัดที่ถูกถอดเข้ารหัสเข้ารหัส!
Steve Barnes

-4

ฉันไม่แน่ใจว่าที่นี่มีเรื่องกวนใจมากเกินไป ฉันเพิ่งมีปัญหาเกี่ยวกับ md5 และไฟล์ที่จัดเก็บเป็น blobs บน MySQL ดังนั้นฉันจึงทดลองกับขนาดไฟล์ที่หลากหลายและวิธี Python ที่ตรงไปตรงมา ได้แก่ :

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

ฉันสามารถตรวจพบความแตกต่างของประสิทธิภาพที่สังเกตไม่ได้ด้วยช่วงของขนาดไฟล์ 2Kb ถึง 20Mb ดังนั้นจึงไม่จำเป็นต้อง 'บีบ' การแฮช อย่างไรก็ตามถ้า Linux ต้องไปที่ดิสก์มันอาจจะทำอย่างน้อยก็เช่นเดียวกับความสามารถของโปรแกรมเมอร์โดยเฉลี่ยในการป้องกันไม่ให้ทำเช่นนั้น เมื่อมันเกิดขึ้นปัญหาก็ไม่ได้เกี่ยวกับ md5 หากคุณใช้งาน MySQL อย่าลืมฟังก์ชั่น md5 () และ sha1 ()


2
นี่ไม่ใช่การตอบคำถามและ 20 MB ถือว่าเป็นไฟล์ขนาดใหญ่มากซึ่งอาจไม่เหมาะกับแรมตามที่กล่าวไว้ที่นี่
คริส
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.