生成文件的MD5校验和

472 投票
9 回答
453019 浏览
提问于 2025-04-16 02:28

有没有简单的方法可以在Python中生成(和检查)一组文件的MD5校验和?(我正在做一个小程序,想确认这些文件的校验和)。

9 个回答

36

我知道我并没有提供什么全新的内容,但在我还不能评论之前就先写了这个回答,而且代码部分让事情更清楚了。总之,专门来回答@Nemo在Omnifarious的回答中的问题:

我最近在想校验和的事情(我来这里是想找一些关于块大小的建议),发现这个方法可能比你想象的要快。以下是对一个大约11MB文件进行校验和计算的几种方法中,最快的(但也算是比较典型的)timeit.timeit/usr/bin/time的结果:

$ ./sum_methods.py
crc32_mmap(filename) 0.0241742134094
crc32_read(filename) 0.0219960212708
subprocess.check_output(['cksum', filename]) 0.0553209781647
md5sum_mmap(filename) 0.0286180973053
md5sum_read(filename) 0.0311000347137
subprocess.check_output(['md5sum', filename]) 0.0332629680634
$ time md5sum /tmp/test.data.300k
d3fe3d5d4c2460b5daacc30c6efbc77f  /tmp/test.data.300k

real    0m0.043s
user    0m0.032s
sys     0m0.010s
$ stat -c '%s' /tmp/test.data.300k
11890400

看起来Python和/usr/bin/md5sum处理一个11MB的文件大约都需要30毫秒。相关的md5sum函数(上面列表中的md5sum_read)和Omnifarious的实现非常相似:

import hashlib
def md5sum(filename, blocksize=65536):
    hash = hashlib.md5()
    with open(filename, "rb") as f:
        for block in iter(lambda: f.read(blocksize), b""):
            hash.update(block)
    return hash.hexdigest()

当然,这些结果都是单次运行的(mmap的方式在进行几十次运行时通常会快一点),而且我的实现通常在缓冲区用完后会多一个f.read(blocksize),但结果是相对稳定的,显示出命令行的md5sum并不一定比Python的实现要快……

补充:抱歉耽搁了这么久,最近没看这个,但为了回答@EdRandall的问题,我会写下一个Adler32的实现。不过,我还没有对它进行基准测试。它基本上和CRC32的实现是一样的:只不过是用zlib.adler32()来替代初始化、更新和摘要的调用:

import zlib
def adler32sum(filename, blocksize=65536):
    checksum = zlib.adler32("")
    with open(filename, "rb") as f:
        for block in iter(lambda: f.read(blocksize), b""):
            checksum = zlib.adler32(block, checksum)
    return checksum & 0xffffffff

注意,这个必须从空字符串开始,因为Adler和从零开始和从""的和(是1)确实是不同的——而CRC可以从0开始。AND操作是为了确保它返回一个32位的无符号整数,这样可以确保在不同版本的Python中返回相同的值。

336

有一种方法在内存使用上不太高效

单个文件:

import hashlib
def file_as_bytes(file):
    with file:
        return file.read()

print hashlib.md5(file_as_bytes(open(full_path, 'rb'))).hexdigest()

文件列表:

[(fname, hashlib.md5(file_as_bytes(open(fname, 'rb'))).digest()) for fname in fnamelst]

不过要记住,MD5已经被认为是不安全的,所以不应该再用它。因为分析安全漏洞非常复杂,而且很难预测你的代码将来可能会遇到的安全问题。个人认为,应该把它从库里彻底移除,这样所有使用它的人都得更新。所以,你应该这样做:

[(fname, hashlib.sha256(file_as_bytes(open(fname, 'rb'))).digest()) for fname in fnamelst]

如果你只想要128位的摘要,可以用.digest()[:16]

这样你会得到一个包含元组的列表,每个元组里有文件名和它的哈希值。

我再次强烈质疑你使用MD5的理由。你至少应该使用SHA1,但考虑到最近发现的SHA1漏洞,可能连它也不太安全。有些人认为只要不把MD5用于“加密”目的就没问题。但事情往往比你最初想的要复杂,你随意的漏洞分析可能完全不靠谱。最好养成一开始就使用正确算法的习惯。其实只不过是打不同的字母而已,没那么难。

这里有一种更复杂但内存高效的方法:

import hashlib

def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
    for block in bytesiter:
        hasher.update(block)
    return hasher.hexdigest() if ashexstr else hasher.digest()

def file_as_blockiter(afile, blocksize=65536):
    with afile:
        block = afile.read(blocksize)
        while len(block) > 0:
            yield block
            block = afile.read(blocksize)


[(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.md5()))
    for fname in fnamelst]

而且,再次强调,MD5已经不安全,真的不应该再用了:

[(fname, hash_bytestr_iter(file_as_blockiter(open(fname, 'rb')), hashlib.sha256()))
    for fname in fnamelst]

如果你只想要128位的摘要,可以在调用hash_bytestr_iter(...)后加上[:16]

663

你可以使用 hashlib.md5() 这个工具。

要注意的是,有时候你可能无法一次性把整个文件放进内存里。在这种情况下,你需要分块读取文件,每次读取4096字节,然后把这些数据逐个传给 md5 方法:

import hashlib
def md5(fname):
    hash_md5 = hashlib.md5()
    with open(fname, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_md5.update(chunk)
    return hash_md5.hexdigest()

注意: hash_md5.hexdigest() 会返回一个 十六进制字符串,这是对数据的摘要表示。如果你只需要原始的字节数据,可以使用 return hash_md5.digest(),这样就不用再转换了。

撰写回答