Python

时间:2017-02-17 20:17:33

标签: python hash

在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上的库。
  • 适用于具有重复行的文件(因此只需对每行哈希进行异或,因为任何一对相同的行都会相互抵消)。

编辑和备注: 感谢几条评论,上面的代码更新为对内存中的行进行排序。 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时,我没有同样的担忧,因为那时我不必拆分文件,因此最快的方法是啜饮大块的二进制数据来更新哈希。

3 个答案:

答案 0 :(得分:2)

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

无论如何,使用frozenset

在Python中可行的方法
#!/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)。 它还假设线条是唯一的。同样,我强烈建议以不同方式解决潜在问题。

答案 1 :(得分:1)

这个 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))

答案 2 :(得分:-1)

感谢所有有趣的评论和答案。

此时,大文件(> 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)两个冻结哈希

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的任意精度int完成的。起初,我试图将总和保持在128位(通过重复和0xfff...fff常数)。但事实证明,比让Python使用任意精度并在结束时进行一次掩蔽要慢一些。

  3. 我试图从冻结集的常规哈希中获取128位,取两个哈希值:冻结集的哈希值,另一个来自冻结集,用不太可能出现在任何文件中的行进行扩充(种类)与使用哈希的不同种子相同,我想)。

  4. 完成结果

    可以使用完整的笔记本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获胜。这是11.8M行:

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

    time of two order-invariant digests in function of size

    后续步骤

    很抱歉这个冗长的回答。这只是一个临时答案,因为我可能(时间允许)稍后尝试使用numba或Cython(或C ++)直接实现Murmurhash3。