【问题标题】:Get MD5 hash of big files in Python在 Python 中获取大文件的 MD5 哈希
【发布时间】:2010-11-11 00:00:30
【问题描述】:

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

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

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

【问题讨论】:

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

标签: python md5 hashlib


【解决方案1】:

如果不阅读完整内容,您将无法获得它的 md5。但您可以使用update 函数逐块读取文件内容。
m.update(a); m.update(b) 等价于 m.update(a+b)

【讨论】:

    【解决方案2】:

    将文件分成 8192 字节的块(或 128 字节的其他倍数),并使用 update() 将它们连续提供给 MD5。

    这利用了 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
    

    【讨论】:

    • 您可以同样有效地使用 128 的任意倍数的块大小(例如 8192、32768 等),这比一次读取 128 个字节要快得多。
    • 感谢 jmanning2k 的重要提示,使用 (128, 8192, 32768) 对 184MB 文件进行测试需要 (0m9.230s, 0m2.547s, 0m2.429s),我将使用 8192 作为较高的值会产生不明显的影响。
    • 如果可以,您应该使用hashlib.blake2b 而不是md5。与 MD5 不同,BLAKE2 是安全的,而且速度更快。
    • @Boris,您实际上不能说 BLAKE2 是安全的。你只能说它还没有被破坏。
    • @vy32 你也不能说它肯定会坏掉。我们将在 100 年后看到,但它至少比绝对不安全的 MD5 更好。
    【解决方案3】:

    您需要以合适大小的块读取文件:

    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 提供的 cmets - 我对此进行了测试,发现它在我的 Python 2.7.2 windows 安装中是正确的

    我使用“jacksum”工具交叉检查了结果。

    jacksum -a md5 <filename>
    

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

    【讨论】:

    • 需要注意的是,传递给此函数的文件必须以二进制模式打开,即将rb 传递给open 函数。
    • 这是一个简单的加法,但使用 hexdigest 而不是 digest 会产生一个“看起来”像大多数哈希示例一样的十六进制哈希。
    • 不应该是if len(data) &lt; block_size: break吗?
    • 埃里克,不,为什么会这样?目标是将所有字节提供给 MD5,直到文件结束。获得部分块并不意味着不应将所有字节都提供给校验和。
    • @user2084795 open 总是打开一个新的文件句柄,并将位置设置为文件的开头,(除非您打开一个文件进行追加)。
    【解决方案4】:

    下面我结合了 cmets 的建议。谢谢大家!

    Python
    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''(不仅仅是'')。

    【讨论】:

    • 更好的是,使用128*md5.block_size 之类的东西而不是8192
    • mrkj:我认为更重要的是根据你的磁盘选择你的读块大小,然后确保它是md5.block_size的倍数。
    • b'' 语法对我来说是新的。解释here
    • @ThorSummoner:不是真的,但根据我为闪存寻找最佳块大小的工作,我建议选择 32k 之类的数字或容易被 4、8 或 16k 整除的数字。例如,如果您的块大小为 8k,则读取 32k 将是正确块大小的 4 次读取。如果是 16,则为 2。但在每种情况下,我们都很好,因为我们碰巧正在读取整数倍数的块。
    • "while True" 非常符合 Python 风格。
    【解决方案5】:

    这是我的@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()
    

    【讨论】:

      【解决方案6】:

      在此线程中使用多个评论/答案,这是我的解决方案:

      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”
      • 这是一个函数
      • 它避免了隐式值:总是更喜欢显式值。
      • 它允许(非常重要的)性能优化

      最后,

      - 这是由社区构建的,感谢大家的建议/想法。

      【讨论】:

      • 一个建议:让你的 md5 对象成为函数的可选参数,以允许备用散列函数,例如 sha256 轻松替换 MD5。我也会建议将此作为编辑。
      • 另外:摘要不是人类可读的。 hexdigest() 允许更易于理解、通常可识别的输出以及更容易的散列交换
      • 其他哈希格式超出了问题的范围,但该建议与更通用的功能相关。根据您的第二个建议,我添加了一个“人类可读”选项。
      • 您能否详细说明“hr”在这里的运作方式?
      • @EnemyBagJones 'hr' 代表人类可读。它返回一个 32 字符长度的十六进制数字字符串:docs.python.org/2/library/md5.html#md5.md5.hexdigest
      【解决方案7】:

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

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

      在 2Kb 到 20Mb 的文件大小范围内,我无法检测到明显的性能差异,因此无需“分块”散列。无论如何,如果 Linux 必须进入磁盘,它可能会做到这一点,至少与普通程序员阻止它这样做的能力一样。事实上,问题与 md5 无关。如果您使用的是 MySQL,请不要忘记已经存在的 md5() 和 sha1() 函数。

      【讨论】:

      • 这不是在回答问题,而且 20 MB 很难被认为是一个非常大的文件,它可能不适合这里讨论的 RAM。
      【解决方案8】:

      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
      

      【讨论】:

        【解决方案9】:
        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
        

        【讨论】:

        • 请格式化答案中的代码,并在给出答案之前阅读本节:stackoverflow.com/help/how-to-answer
        • 这将无法正常工作,因为它正在以文本模式逐行读取文件,然后将其弄乱并打印每个剥离、编码的行的 md5!
        【解决方案10】:

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

        【讨论】:

          【解决方案11】:

          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() 函数与sentinel(空字符串)一起使用。

          在这种情况下创建的迭代器将调用 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
          

          【讨论】:

            【解决方案12】:

            我认为下面的代码更 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()
            

            【讨论】:

              【解决方案13】:

              我不喜欢循环。基于@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()
              

              【讨论】:

              • 用包含多个 lambda 的 functools.reduce 缩写替换简单而清晰的循环有什么可能的原因?我不确定是否有任何关于编程的约定没有被打破。
              • 我的主要问题是hashlibs API 不能很好地与 Python 的其余部分配合使用。例如,让我们以 shutil.copyfileobj 为例,它几乎无法正常工作。我的下一个想法是fold(又名reduce),它将可迭代对象一起折叠成单个对象。就像例如一个哈希。 hashlib 不提供运算符,这使得这有点麻烦。尽管如此,这里还是折叠了一个可迭代对象。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-12-18
              • 1970-01-01
              • 2015-12-21
              • 2016-02-29
              • 2019-04-24
              • 2010-11-16
              相关资源
              最近更新 更多