在Python中获取大文件的MD5哈希


188

我使用过hashlib(在Python 2.6 / 3.0中取代了md5),如果我打开一个文件并将其内容放入hashlib.md5()函数中,它就可以正常工作。

问题在于非常大的文件,其大小可能超过RAM大小。

如何在不将整个文件加载到内存的情况下获取文件的MD5哈希?


20
我会改写:“如何在不将整个文件加载到内存的情况下获取文件的MD5?”
XTL 2012年

Answers:


147

将文件分成8192字节的块(或128字节的其他倍数),然后使用连续将其馈送到MD5 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的这一重要说明,使用(128,8192,32768)对184MB文件进行的测试需要(0m9.230s,0m2.547s,0m2.429s),我将使用8192,因为较高的值会产生明显的影响。
JustRegisterMe

如果可以,则应使用hashlib.blake2b代替md5。与MD5不同,BLAKE2是安全的,而且速度更快。
鲍里斯(Boris)

2
@鲍里斯,您实际上不能说BLAKE2是安全的。您只能说它还没有损坏。
vy32

@ vy32您也不能说肯定会被破坏。我们将在100年后看到它,但它至少比MD5更好,后者绝对是不安全的。
鲍里斯

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
需要注意的重要一点是,必须以二进制模式(即通过传递rbopen函数)打开传递给该函数的文件。
Frerich Raabe 2011年

11
这是一个简单的加法运算,但使用hexdigest代替digest会产生一个十六进制哈希,看起来像大多数哈希示例一样。
tchaymore

不是if len(data) < block_size: break吗?
Erik Kaplun

2
Erik,不,为什么会这样?目标是将所有字节都馈送到MD5,直到文件末尾。获得部分块并不意味着不应将所有字节都馈入校验和。

2
@ user2084795 open 始终打开位置设置为文件开头的新鲜文件句柄(除非您打开要追加的文件)。
史蒂夫·巴恩斯

110

下面,我结合了评论中的建议。谢谢大家!

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及以上

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(无“ while为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()函数需要一个空字节字符串来使返回的迭代器在EOF处停止,因为read()返回b''(而不仅仅是“)。


17
更好的是,使用128*md5.block_size代替8192
mrkj 2011年

1
mrkj:我认为更重要的是根据磁盘选择读取的块大小,然后确保它是的倍数md5.block_size
哈维

6
b''语法是新的我。在这里解释。
cod3monk3y 2014年

1
@ThorSummoner:并非如此,但是从我的工作中找到了闪存的最佳块大小,我建议您选择一个数字,例如32k或容易被4、8或16k整除的数字。例如,如果块大小为8k,则以正确的块大小读取32k将是4次读取。如果它是16,则为2。但是在每种情况下,我们都很好,因为我们碰巧正在读取整数倍的块。
哈维2015年

1
“ while True”是相当pythonic的。
尔根A.艾哈德

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,2013年

1
还:摘要不是人类可读的。hexdigest()可以提供更易于理解,通常可重新识别的输出以及更轻松的哈希交换
Hawkwing,2013年

其他哈希格式不在问题范围内,但建议与更通用的功能有关。根据您的第二个建议,我添加了“可读”选项。
Bastien Semene 2013年

您能否详细说明“ hr”在这里的运作方式?
EnemyBagJones

@EnemyBagJones'hr'表示人类可读。它返回一个由32个字符长度的十六进制数字组成的字符串:docs.python.org/2/library/md5.html#md5.md5.hexdigest
Bastien Semene,

8

Python 2/3便携式解决方案

要计算校验和(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()方法的调用都没有参数;如果返回的值等于哨兵,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(a); 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包含多个lambda的替代替换简单明了的循环?我不确定在编程方面是否有任何约定尚未打破。
Naltharial

我的主要问题是hashlibs API无法与其余Python真正配合使用。例如,让我们来看看哪一个shutil.copyfileobj几乎无法正常工作。我的下一个想法是fold(aka reduce)将可迭代对象折叠成单个对象。像例如哈希。hashlib没有提供运算符,这使它有点麻烦。尽管如此,这里还是折叠了一个可迭代对象。
塞巴斯蒂安·瓦格纳

-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!
史蒂夫·巴恩斯

-4

我不确定这里是否有太多大惊小怪的事情。我最近在md5上遇到问题,并且在MySQL上将这些文件存储为blob,因此我尝试了各种文件大小和简单的Python方法,即:

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

在文件大小为2Kb到20Mb的范围内,我无法检测到明显的性能差异,因此无需“分块”哈希。无论如何,如果Linux必须使用磁盘,那么它至少可以做到与普通程序员阻止磁盘使用的能力相同。碰巧,问题与md5无关。如果您使用的是MySQL,请不要忘记已经存在md5()和sha1()函数。


2
这不能回答问题,并且20 MB几乎不被认为是一个很大的文件,可能无法容纳到RAM中,如本文所述。
克里斯(Chris)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.