在Python 3中使用hashlib计算文件的MD5摘要

25

使用Python 2.7,以下代码可以计算文件内容的MD5十六进制摘要。

(编辑:实际上并不是这样,正如其他答案所示,我之前的想法是错误的。)

import hashlib

def md5sum(filename):
    f = open(filename, mode='rb')
    d = hashlib.md5()
    for buf in f.read(128):
        d.update(buf)
    return d.hexdigest()

如果我使用Python3运行该代码,它会引发TypeError异常:

    d.update(buf)
TypeError: object supporting the buffer API required

我发现只需将代码更改为以下内容,就可以在python2和python3中运行:

def md5sum(filename):
    f = open(filename, mode='r')
    d = hashlib.md5()
    for buf in f.read(128):
        d.update(buf.encode())
    return d.hexdigest()

现在我仍然想知道为什么原来的代码停止工作。似乎当使用二进制模式修饰符打开文件时,它返回整数而不是编码为字节的字符串(我这么说是因为type(buf)返回int)。这种行为有解释吗?


1
相关:https://dev59.com/gFTTa4cB1Zd3GeqPqC0T/ - jfs
2
如果你进行更大的读取,接近文件系统的文件块大小,会更快吗?(例如,在Linux ext3上为1024字节,在Windows NTFS上为4096字节或更多) - rakslice
同时:生成文件的MD5校验和 - maxschlepzig
3个回答

37

我认为您想让for循环连续调用f.read(128)。可以使用iter()functools.partial()来实现:

import hashlib
from functools import partial

def md5sum(filename):
    with open(filename, mode='rb') as f:
        d = hashlib.md5()
        for buf in iter(partial(f.read, 128), b''):
            d.update(buf)
    return d.hexdigest()

print(md5sum('utils.py'))

这会在某些Python实现中泄漏文件句柄。你至少应该调用close - phihag
1
我已经添加了 with 语句以正确关闭文件。 - jfs
1
@phihag:真的有Python实现会导致自动关闭的文件句柄泄漏吗?我认为这只是延迟了对这些文件句柄的释放,直到垃圾回收。 - kriss
@J.F.Sebastian 添加with语句“改进”了代码,但以混淆OP问题的答案为代价。很多人会被with语句的语义所困扰或分心,因此它不适用于回答迭代基础知识的问题。那些在“泄漏文件句柄”上纠结的人正在浪费时间,这在实际代码中几乎从不重要。with语句很好,但自动文件关闭是一个单独的主题,不值得从一个关于按块读取文件的基本问题的明确答案中分心。 - Raymond Hettinger
5
如果你不喜欢它,只需还原更改。我认为这个变化太小了,不值得讨论。虽然我强烈不同意你的理由。公共代码应该遵循最佳实践,特别是针对初学者。如果最佳实践对于这样一个常见任务来说太难遵循(尽管我不认为是这种情况),那么语言应该改变。 - jfs
显示剩余3条评论

10
for buf in f.read(128):
  d.update(buf)

该函数逐个使用文件的前128个字节值来顺序更新哈希。由于遍历一个bytes对象会产生int对象,因此您将得到以下调用,这会导致在Python3中出现错误。

d.update(97)
d.update(98)
d.update(99)
d.update(100)

这并不是你想要的。

相反,你需要:

def md5sum(filename):
  with open(filename, mode='rb') as f:
    d = hashlib.md5()
    while True:
      buf = f.read(4096) # 128 is smaller than the typical filesystem block
      if not buf:
        break
      d.update(buf)
    return d.hexdigest()

1
如果打开一个巨大的文件,这会吃掉整个RAM。这就是为什么我们需要缓冲的原因。 - Umur Kontacı
@fastreload 已经添加了那个;). 由于原始解决方案甚至不能处理大于128字节的文件,我认为内存不是一个问题,但我还是添加了缓冲读取。 - phihag
2
@fastreload 在Python 2中,代码没有崩溃,因为迭代str会产生str。但对于大于128字节的文件,结果仍然是错误的。当然,您可以根据需要调整缓冲区大小(除非您有快速SSD,否则CPU无论如何都会感到无聊,而好的操作系统会预加载文件的下一组字节)。Python 2.7绝对没有这样的故障保护机制;那将违反read的约定。OP只是没有将脚本的结果与规范的md5sum或具有128个相同首字节的两个文件的脚本结果进行比较。 - phihag
是的,我的原始代码确实存在问题(但还没有在实际应用中出现)。我只是没有在具有相同开头的大文件上进行测试。当它运行得太快时,我应该猜到确实存在一个实际问题。 - kriss
当它说“迭代字节会产生str对象”时,这个答案是不正确的。list(b'abc') --> [97, 98, 99] - Raymond Hettinger
显示剩余2条评论

2

在提问后,我最终将代码更改为以下版本(我认为易于理解)。但我可能会使用functools.partial建议的版本进行更改。

import hashlib

def chunks(filename, chunksize):
    f = open(filename, mode='rb')
    buf = "Let's go"
    while len(buf):
        buf = f.read(chunksize)
        yield buf

def md5sum(filename):
    d = hashlib.md5()
    for buf in chunks(filename, 128):
        d.update(buf)
    return d.hexdigest()

如果文件长度不是块大小的倍数,则此方法将起作用,读取操作实际上将在最后一次读取中返回一个较短的缓冲区。终止条件是通过空缓冲区来判断的,这就是为什么示例代码中有“not buf”条件(它有效)。 - user109839
@Mapio:我的代码确实存在一种错误,但并不是你所说的那样。文件长度是无关紧要的。上面的代码可以正常工作,只要没有部分读取返回不完整的缓冲区即可。如果发生部分读取,则会过早停止(但考虑到部分缓冲区)。在某些情况下可能会发生部分读取,比如如果程序在读取时接收到受控中断信号,然后在返回中断后继续读取。 - kriss
好的,在上面的评论中,当提到“上面的代码”时,我指的是旧版本。这个当前版本现在可以工作了(即使它不是最好的解决方案)。 - kriss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接