【问题标题】:Order-invariant hash in PythonPython中的顺序不变哈希
【发布时间】:2018-12-28 01:27:33
【问题描述】:

在 Python 中,我想快速计算文件行的顺序不变散列,作为“唯一”识别其内容的一种方式。例如,这些文件是 select ... from table 的输出,因此行的顺序是随机的。

这是一个实现我想要的示例(使用 hashlib 中的哈希器之一),但代价是必须对行进行排序。 请注意,对行进行排序只是实现目标的一种方式,即获得一个不依赖于文件中行顺序的哈希。但很明显,我想避免 O(n*log(n)) 成本,尤其是。当文件更长时。

def get_hexdigest(filename, hasher, blocksize=65536, order_invariant=False):
    if not os.path.isfile(filename):
        return None
    if order_invariant:
        with open(filename, 'r') as f:
            for line in sorted(f):
                hasher.update(line.encode())
    else:
        with open(filename, 'rb') as f:
            while True:
                buf = f.read(blocksize)
                hasher.update(buf)
                if len(buf) < blocksize:
                    break
    return hasher.hexdigest()

所以,例如1MB,50K 行文件:

%%time
get_hexdigest('some_file', hashlib.sha1())
# Wall time: 1.71 ms

但是:

%%time
get_hexdigest('some_file', hashlib.sha1(), order_invariant=True)
# Wall time: 77.4 ms

有什么更好/更快的方法来做到这一点?

正如in this answer 所指出的,Scala 有一个基于 Murmurhash 的顺序不变哈希,但我认为它是 mmh3 的 32 位版本(对我的使用来说太容易发生冲突),而且我宁愿使用一些标准Python 中可用的库,而不是在 C 或 Cython 中实现某些东西。 Murmurhash3 有一个 128 位版本,但它的输出在 x64 和 x86 上是不同的。我想获得独立于机器的结果。

所以,总而言之,我想:

  • 跨机器架构的一致结果
  • 低冲突率,即至少 128 位具有良好的分散性(但我不需要哈希是加密的)
  • 相当快,即 1MB、50K 行的文件至少在 5ms 以下。
  • 如果可能的话,作为 PyPi 或 Conda 上的库随时可用。
  • 适用于具有重复行的文件(因此,仅对每行哈希进行异或运算是不可能的,因为任何一对相同的行都会相互抵消)。

编辑和注释: 多亏了几个 cmets,上面的代码更新为对内存中的行进行排序。 order_invariant is True 的原始版本是:

    with os.popen('sort {}'.format(filename)) as f:
        for line in f:
            hasher.update(line.encode(encoding='utf-8'))
    return hasher.hexdigest()

相关的挂墙时间(对于上面使用的文件)是 238 毫秒。这现在减少到 77 毫秒,但仍然比不对行进行排序要慢得多。排序将为 n 行添加 n*log(n) 成本。

在读取行时,编码(到 UTF-8)和读取模式 'r' 或 'rb' 是必要的,因为这样我们得到的是字符串而不是字节。我不想依赖假设文件只包含 ASCII 数据。读入'rb' 可能会导致行未正确拆分。当order_invariant 为 False 时,我没有同样的顾虑,因为这样我就不必拆分文件,因此最快的方法是使用大块的二进制数据来更新哈希。

【问题讨论】:

  • 第一个改进:您可能可以使用for lines in sorted(f) 来sort python 行而不是调用外部进程...
  • 附带说明,如果您正在使用行(基于 'order-invariant hash for the lines of a file'),为什么要在“订单敏感”案例?只需用.readline() 输出hasher。
  • 另外:对整个文件进行编码并然后拆分行,然后在每一行上调用.encode:with open(...) as f: for line in sorted(f.read().encode('utf-8').split('\n')): hasher.update(line)(是的,它加载整个文件到内存中,但排序需要这个)。
  • 或者更好的是,以二进制模式打开文件,这样就不会发生解码/重新编码
  • @ThierryLathuille - 他的最小散列单位是行,因此将整个文件读入缓冲区是一个有争议的问题。再说一次,出于某种原因,他也以二进制模式打开文件,几乎可以保证上述内容不会为两个完全相同的文件返回相同的哈希值,而在第一种情况下,他甚至没有逐行散列.

标签: python hash


【解决方案1】:

我认为您应该在 (select ... from table order by ...) 之前对文件进行排序,或者针对您的实际问题提出其他解决方案。

无论如何,在 Python 中使用frozenset 的一种可能方法:

#!/usr/bin/python

lines1 = ['line1', 'line2', 'line3', 'line4']
lines2 = ['line2', 'line1', 'line3', 'line4']  # same as lines1 but different order
lines3 = ['line1', 'line1', 'line3', 'line4', 'line5']


for lines in [lines1, lines2, lines3]:
    print(lines)
    print(hash(frozenset(lines)))
    print('')

输出

['line1', 'line2', 'line3', 'line4']
8013284786872469720

['line2', 'line1', 'line3', 'line4']
8013284786872469720

['line1', 'line1', 'line3', 'line4', 'line5']
7430298023231386903

我怀疑它是否符合您的性能限制。我不知道frozenset() 的时间复杂度(Big O)。 它还假设线条是唯一的。我再次强烈建议以不同的方式解决根本问题。

【讨论】:

  • Frozenset 非常适合这项任务。而且由于在构建冻结集时会消除重复行,因此不会存在两条重复行相互抵消的风险。
  • 缺点是哈希值只有32或64位。
  • 酷,+1 考虑冻结集。但是,我对增加尺寸进行了一些测量,发现这种方法所花费的时间与行数成线性关系。也就是说,整体常数很低。例如,65536 行需要 15 毫秒,这是迄今为止我见过的最好的。但是1180万行需要7.6s,不是最好的。
  • 顺便说一句,文件来自不同的系统,我几乎无法控制;它们有很多,我不能影响它们的制作方式。我希望它们总是被分类。通常不是。
【解决方案2】:

这个 merkle-style map-reduce 怎么样(哈希连接映射哈希,哈希映射步骤后不变的可选排序):

import hashlib

def hasher(data):
    hasher = hashlib.sha1()
    hasher.update(data.encode('utf-8'))
    return hasher.hexdigest()


def get_digest_by_line(filename, line_invariant=False, hasher=hasher):
    with open(filename, 'r') as f:
        hashes = (hasher(line) for line in f)
        if line_invariant:
            hashes = sorted(hashes)
        return hasher(''.join(hashes))

【讨论】:

  • 嘿,好主意。事实上,通过一些次要的模组,它也可以分发给非常大的文件,例如在 map-reduce 方案中,对哈希前缀进行了一些分箱。
【解决方案3】:

感谢大家迄今为止提供的有趣的 cmets 和答案。

此时,大文件(>350K 行)的最佳答案是下面的(a)。它以Murmurhash3为基础,每行加上mmh3.hash128()。对于较小的文件,它是 (b) 以下:the frozenset approach proposed by Rolf 的变体,我对其进行了调整以生成 128 位散列(尽管我不保证这 128 位的质量)。

a) mmh3.hash128() 为每一行添加

import mmh3
def get_digest_mmh3_128_add(filename):
    a = 0
    with open(filename, 'rb') as f:
        for line in f:
            a += mmh3.hash128(line)
    return '{:032x}'.format(a & 0xffffffffffffffffffffffffffffffff)

在我的设置中:恒定每百万行 0.4 秒。

b) 两个frozenset hash

def get_digest_hash_frozenset128(filename):
    with open(filename, 'rb') as f:
        frz = frozenset(f.readlines())
    return '{:032x}'.format(((hash(frz) << 64) + hash(frz.union('not a line'))) & 0xffffffffffffffffffffffffffffffff)

在我的设置中:每百万行 0.2 到 0.6 秒。

备注

  1. 经过考虑,我决定可以以二进制模式读取文件的行,即使它们可能包含 UTF-8 文本。原因是,如果某个 Unicode 字符包含 '\n',则该行会在该点意外拆分。然后该文件将获得与另一个文件相同的摘要,其中该行的两个部分的排列方式不同(甚至分开并放在文件的其他位置),但是这种可能性非常慢,我可以忍受它。

  2. 在 (a) 中添加所有 128 位哈希是使用 Python 的任意精度整数完成的。起初,我试图将总和保持在 128 位(通过与 0xfff...fff 常量重复与-ing)。但事实证明它比让 Python 使用任意精度并在最后进行一次掩码要慢。

  3. 我试图通过采用两个哈希值从 freezeset 的常规哈希中获取 128 位:frozenset 的哈希值,以及来自 freezeset 的另一个哈希值增加了一条不太可能出现在任何文件中的行(种类我猜这与使用不同的种子进行哈希相同)。

完整结果

完整的笔记本可用here。它创建任意大小的伪随机文件,并尝试几种摘要方法,同时测量每种方法所花费的时间。这是在 EC2 实例(r3.4xlarge,使用 EBS 卷来存储伪随机文件)和 Jupyter iPython 笔记本以及 Python 3.6 上运行的。

对于 46341 行,我们得到

fun                              lines millis
get_digest_xxh64_order_sensitive 46341    0.4 *
get_digest_sha1                  46341    1.7 *
get_digest_hash_frozenset64      46341    8.7
get_digest_hash_frozenset128     46341   10.8
get_digest_sha1_by_lines         46341   14.1 *
get_digest_mmh3_128_add_cy       46341   18.6
get_digest_mmh3_128_add          46341   19.7
get_digest_sha1_sort_binary      46341   44.3
get_digest_sha1_sort             46341   65.9

*:这些是顺序相关的,这里只是为了比较。

get_digest_hash_frozenset64 不太合适,因为它只提供 64 位。

get_digest_mmh3_128_add_cy是上面(a)中给出的函数的cythonized版本,但差别不大。

get_digest_xxh64_order_sensitive 非常快,但它取决于顺序。我尝试(未在此处列出)得出一个顺序不变的版本都给出了一些相当缓慢的结果。我认为原因是初始化和最终确定哈希的成本显然很高。

对于较大的文件,get_digest_mmh3_128_add_cy 胜出。这是 1180 万行:

fun                                 lines    millis
get_digest_xxh64_order_sensitive 11863283      97.8 *
get_digest_sha1                  11863283     429.3 *
get_digest_sha1_by_lines         11863283    3453.0 *
get_digest_mmh3_128_add_cy       11863283    4692.8
get_digest_mmh3_128_add          11863283    4956.6
get_digest_hash_frozenset64      11863283    6418.2
get_digest_hash_frozenset128     11863283    7663.6
get_digest_sha1_sort_binary      11863283   27851.3
get_digest_sha1_sort             11863283   34806.4

关注两个领先的竞争者(顺序不变,而不是其他),这里是他们在大小函数(行数)上花费的时间。 y 轴是微秒/行,x 轴是文件的行数。注意get_digest_mmh3_128_add_cy 每行花费的时间是固定的(0.4 us)。

后续步骤

对不起,冗长的回答。这只是一个临时答案,因为我可能会(时间允许)稍后尝试进一步试验 Murmurhash3 的直接实现的 numba 或 Cython(或 C++)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-30
    • 2013-01-31
    • 2021-09-29
    • 1970-01-01
    • 2012-09-16
    相关资源
    最近更新 更多