在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上是不同的。我希望得到与机器无关的结果。
总而言之,我想:
编辑和备注:
感谢几条评论,上面的代码更新为对内存中的行进行排序。 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时,我没有同样的担忧,因为那时我不必拆分文件,因此最快的方法是啜饮大块的二进制数据来更新哈希。
答案 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秒之间。
备注强>
经过考虑后,我决定以二进制模式读取文件的行是可以的,即使它们可能包含UTF-8文本。原因是,如果某些Unicode字符包含'\n'
,则该行将在该点意外拆分。然后该文件将获得与另一个文件相同的摘要,其中该行的两个部分以不同的方式排列(或者甚至分开并放在文件中的其他位置),但这种可能性非常慢,我可以忍受它
在(a)中添加所有128位哈希是使用Python的任意精度int完成的。起初,我试图将总和保持在128位(通过重复和0xfff...fff
常数)。但事实证明,比让Python使用任意精度并在结束时进行一次掩蔽要慢一些。
我试图从冻结集的常规哈希中获取128位,取两个哈希值:冻结集的哈希值,另一个来自冻结集,用不太可能出现在任何文件中的行进行扩充(种类)与使用哈希的不同种子相同,我想)。
完成结果
可以使用完整的笔记本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)。
后续步骤
很抱歉这个冗长的回答。这只是一个临时答案,因为我可能(时间允许)稍后尝试使用numba或Cython(或C ++)直接实现Murmurhash3。