【问题标题】:How can I calculate a hash for a filesystem-directory using Python?如何使用 Python 计算文件系统目录的哈希?
【发布时间】:2014-09-16 05:33:52
【问题描述】:

我正在使用此代码来计算文件的哈希值:

m = hashlib.md5()
with open("calculator.pdf", 'rb') as fh:
    while True:
        data = fh.read(8192)
        if not data:
            break
        m.update(data)
    hash_value = m.hexdigest()

    print  hash_value

当我在文件夹“文件夹”上尝试时,我得到了

IOError: [Errno 13] Permission denied: folder

如何计算文件夹的哈希值?

【问题讨论】:

  • 出于什么目的?唯一标识?使用完整的文件夹路径或 inode。识别其内容?然后遍历其全部内容并对其进行哈希处理。
  • 必须计算其所有文件及其子文件夹文件的哈希值。
  • 康拉德是正确的,因为这个问题有很多模棱两可的地方。他没有列出的另一种可能性是散列目录条目元数据,这可用于快速/粗略检查内容是否已更改。顺便说一句,某些操作系统确实允许您“打开”一个目录,就像它是一个文本文件一样,并且上面的文件代码对于目录“文件”流产生的任何元数据都已经“工作”了。就目前而言,除非明确需求或目标,否则该问题值得结束。
  • this gist 带有 imo 清洁器代码。还有一个指定的包checksumdirdirtools 具有散列功能。

标签: python hash


【解决方案1】:

使用可用于计算目录的校验和/哈希的 checksumdir python 包。可通过https://pypi.python.org/pypi/checksumdir/1.0.5获得它

用法:

import checksumdir
hash = checksumdir.dirhash("c:\\temp")
print hash

【讨论】:

  • 注意:checksumdir 未经测试(即使声称是稳定的)。使用它(可以说)比使用配方更可靠,这至少会迫使您阅读配方。
  • 它似乎不适用于不同的操作系统(在 MacOS 和 Debian 下获得不同的哈希值):(
【解决方案2】:

这是一个使用 pathlib.Path 而不是依赖 os.walk 的实现。它在迭代之前对目录内容进行排序,因此它应该可以在多个平台上重复。它还使用文件/目录的名称更新哈希,因此添加空文件和目录将更改哈希。

带有类型注释的版本(Python 3.6 或更高版本):

import hashlib
from _hashlib import HASH as Hash
from pathlib import Path
from typing import Union


def md5_update_from_file(filename: Union[str, Path], hash: Hash) -> Hash:
    assert Path(filename).is_file()
    with open(str(filename), "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash.update(chunk)
    return hash


def md5_file(filename: Union[str, Path]) -> str:
    return str(md5_update_from_file(filename, hashlib.md5()).hexdigest())


def md5_update_from_dir(directory: Union[str, Path], hash: Hash) -> Hash:
    assert Path(directory).is_dir()
    for path in sorted(Path(directory).iterdir(), key=lambda p: str(p).lower()):
        hash.update(path.name.encode())
        if path.is_file():
            hash = md5_update_from_file(path, hash)
        elif path.is_dir():
            hash = md5_update_from_dir(path, hash)
    return hash


def md5_dir(directory: Union[str, Path]) -> str:
    return str(md5_update_from_dir(directory, hashlib.md5()).hexdigest())

没有类型注释:

import hashlib
from pathlib import Path


def md5_update_from_file(filename, hash):
    assert Path(filename).is_file()
    with open(str(filename), "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash.update(chunk)
    return hash


def md5_file(filename):
    return md5_update_from_file(filename, hashlib.md5()).hexdigest()


def md5_update_from_dir(directory, hash):
    assert Path(directory).is_dir()
    for path in sorted(Path(directory).iterdir()):
        hash.update(path.name.encode())
        if path.is_file():
            hash = md5_update_from_file(path, hash)
        elif path.is_dir():
            hash = md5_update_from_dir(path, hash)
    return hash


def md5_dir(directory):
    return md5_update_from_dir(directory, hashlib.md5()).hexdigest()

如果您只需要对目录进行哈希处理,则为精简版:

def md5_update_from_dir(directory, hash):
    assert Path(directory).is_dir()
    for path in sorted(Path(directory).iterdir(), key=lambda p: str(p).lower()):
        hash.update(path.name.encode())
        if path.is_file():
            with open(path, "rb") as f:
                for chunk in iter(lambda: f.read(4096), b""):
                    hash.update(chunk)
        elif path.is_dir():
            hash = md5_update_from_dir(path, hash)
    return hash


def md5_dir(directory):
    return md5_update_from_dir(directory, hashlib.md5()).hexdigest()

用法:md5_hash = md5_dir("/some/directory")

【讨论】:

  • 您是否通过设计使用文件的绝对路径初始化哈希,而不是说来自根目录的相对路径?
  • @nimig18 我什么都不做;只有文件名(不包括路径)包含在哈希中。
  • 文件的排序对于可重复性非常重要,因为 Path iterdir 或 os.walk 都不能保证一定的顺序,并且会受到底层操作系统实现的影响。但是,仅按不区分大小写的路径排序是不够的,因为 sorted 是就地排序的,如果 linux 中的两个文件夹仅按大小写不同,sorted 可以根据 iterdir/os.walk 返回的特定顺序在不同时间返回不同的顺序.正确的解决方案是先按大小写排序,然后再按不区分大小写。 @danmou 也许你可以更新这个?
  • @MatiasGrioni 我写这段代码已经有一段时间了,我不记得为什么我首先要进行不区分大小写的排序。普通的区分大小写的排序不应该解决问题吗?
  • 这取决于您希望散列具有什么特征。没有“自然”的方法来散列文件夹,因此您应该事先为您的应用程序定义一些策略/特征,并确保算法适用于它。
【解决方案3】:

这个Recipe 提供了一个很好的功能来做你所要求的。正如您最初的问题所要求的那样,我已将其修改为使用 MD5 哈希而不是 SHA1

def GetHashofDirs(directory, verbose=0):
  import hashlib, os
  SHAhash = hashlib.md5()
  if not os.path.exists (directory):
    return -1

  try:
    for root, dirs, files in os.walk(directory):
      for names in files:
        if verbose == 1:
          print 'Hashing', names
        filepath = os.path.join(root,names)
        try:
          f1 = open(filepath, 'rb')
        except:
          # You can't open the file for some reason
          f1.close()
          continue

        while 1:
          # Read file in as little chunks
          buf = f1.read(4096)
          if not buf : break
          SHAhash.update(hashlib.md5(buf).hexdigest())
        f1.close()

  except:
    import traceback
    # Print the stack traceback
    traceback.print_exc()
    return -2

  return SHAhash.hexdigest()

你可以这样使用它:

print GetHashofDirs('folder_to_hash', 1)

输出看起来像这样,因为它对每个文件进行哈希处理:

...
Hashing file1.cache
Hashing text.txt
Hashing library.dll
Hashing vsfile.pdb
Hashing prog.cs
5be45c5a67810b53146eaddcae08a809

此函数调用的返回值作为散列返回。在这种情况下,5be45c5a67810b53146eaddcae08a809

【讨论】:

  • import traceback 的目的是什么?
  • @begueradj,在这种情况下,traceback 用于在散列时发生错误时打印堆栈跟踪
  • 只是忽略一个您无法打开的文件对我来说听起来并不正确。此外,you cannot guarantee 例如在不同的文件系统上,os.walk 将以相同的顺序导航文件。
  • 这是递归的吗?
  • @TheQuantumPhysicist 是的,因为os.walk 是。
【解决方案4】:

我不喜欢答案中引用的食谱是如何编写的。我正在使用一个更简单的版本:

import hashlib
import os


def hash_directory(path):
    digest = hashlib.sha1()

    for root, dirs, files in os.walk(path):
        for names in files:
            file_path = os.path.join(root, names)

            # Hash the path and add to the digest to account for empty files/directories
            digest.update(hashlib.sha1(file_path[len(path):].encode()).digest())

            # Per @pt12lol - if the goal is uniqueness over repeatability, this is an alternative method using 'hash'
            # digest.update(str(hash(file_path[len(path):])).encode())

            if os.path.isfile(file_path):
                with open(file_path, 'rb') as f_obj:
                    while True:
                        buf = f_obj.read(1024 * 1024)
                        if not buf:
                            break
                        digest.update(buf)

    return digest.hexdigest()

我发现每当遇到类似alias 的东西时通常会抛出异常(显示在os.walk() 中,但您不能直接打开它)。 os.path.isfile() 检查解决了这些问题。

如果在我试图散列的目录中有一个实际文件并且无法打开,则跳过该文件并继续不是一个好的解决方案。这会影响哈希的结果。最好完全杀死哈希尝试。在这里,try 语句将围绕对我的 hash_directory() 函数的调用进行包装。

>>> try:
...   print(hash_directory('/tmp'))
... except:
...   print('Failed!')
... 
e2a075b113239c8a25c7e1e43f21e8f2f6762094
>>> 

【讨论】:

  • 好东西,但请注意,如果您添加空文件或目录,此哈希不会改变。
  • 我为每个file_pathdir_path 添加了digest.update(str(hash(file_path[len(path):])).encode())。哈希确保这个哈希将依赖于PYTHONHASHSEED,所以伪造这个哈希计算真的很困难。
  • @pt12lol 我真的很喜欢这个想法——我没有考虑过这种可能性——但我认为使用hash 不是正确的解决方案。该函数的结果在 Python 2 和 Python 3 之间会有所不同(我刚刚在我的 Mac 上尝试过)。再次使用hashlib 怎么样? python digest.update(hashlib.sha1(file_path.encode()).digest())
  • 难怪这个哈希值在 Python 2 和 3 之间是不同的,因为 Python 3 引入了变量 PYTHONHASHSEED,它在每次 Python 运行时都不同,而 hash 依赖于它。我想你用硬编码值断言目录哈希,digest 在这种情况下肯定更好。老实说,我的代码不依赖于硬编码值,所以我不关心它,只关心唯一性。就我而言,hash 甚至比 digest 更安全。
  • 选择 1024*1024 作为缓冲区大小有什么特别的原因吗?
【解决方案5】:

我不断看到此代码通过各种论坛传播。

ActiveState recipe answer 有效,但正如 Antonio 所指出的,由于无法以相同的顺序显示文件(尝试一下),因此不能保证在文件系统之间可重复。一种解决方法是改变

for root, dirs, files in os.walk(directory):
  for names in files:

for root, dirs, files in os.walk(directory):
  for names in sorted(files): 

(是的,我在这里很懒惰。这仅对文件名进行排序,而不对目录进行排序。同样的原则适用)

【讨论】:

    【解决方案6】:

    使用校验和目录https://pypi.org/project/checksumdir/

    
    directory  = '/path/to/directory/'
    md5hash    = dirhash(directory, 'md5')
    

    【讨论】:

      【解决方案7】:

      我对安迪的反应进行了进一步优化。

      以下是python3而不是python2的实现。它使用 SHA1,处理一些需要编码的情况,经过 linted,并包含一些 docstrings。

      #!/usr/bin/env python3
      # -*- coding: utf-8 -*-
      """dir_hash: Return SHA1 hash of a directory.
      - Copyright (c) 2009 Stephen Akiki, 2018 Joe Flack
      - MIT License (http://www.opensource.org/licenses/mit-license.php)
      - http://akiscode.com/articles/sha-1directoryhash.shtml
      """
      import hashlib
      import os
      
      
      def update_hash(running_hash, filepath, encoding=''):
          """Update running SHA1 hash, factoring in hash of given file.
      
          Side Effects:
              running_hash.update()
          """
          if encoding:
              file = open(filepath, 'r', encoding=encoding)
              for line in file:
                  hashed_line = hashlib.sha1(line.encode(encoding))
                  hex_digest = hashed_line.hexdigest().encode(encoding)
                  running_hash.update(hex_digest)
              file.close()
          else:
              file = open(filepath, 'rb')
              while True:
                  # Read file in as little chunks.
                  buffer = file.read(4096)
                  if not buffer:
                      break
                  running_hash.update(hashlib.sha1(buffer).hexdigest())
              file.close()
      
      
      def dir_hash(directory, verbose=False):
          """Return SHA1 hash of a directory.
      
          Args:
              directory (string): Path to a directory.
              verbose (bool): If True, prints progress updates.
      
          Raises:
              FileNotFoundError: If directory provided does not exist.
      
          Returns:
              string: SHA1 hash hexdigest of a directory.
          """
          sha_hash = hashlib.sha1()
      
          if not os.path.exists(directory):
              raise FileNotFoundError
      
          for root, dirs, files in os.walk(directory):
              for names in files:
                  if verbose:
                      print('Hashing', names)
                  filepath = os.path.join(root, names)
                  try:
                      update_hash(running_hash=sha_hash,
                                  filepath=filepath)
                  except TypeError:
                      update_hash(running_hash=sha_hash,
                                  filepath=filepath,
                                  encoding='utf-8')
      
          return sha_hash.hexdigest()
      

      【讨论】:

        猜你喜欢
        • 2011-12-19
        • 2011-08-26
        • 2010-10-20
        • 2011-11-05
        • 2012-06-10
        • 1970-01-01
        • 2018-09-18
        • 2021-07-29
        相关资源
        最近更新 更多