您可以制作一组到目前为止看到的行,为了节省内存,存储每行的哈希而不是行本身(以极小的误报机会为代价):
seen = set()
with open('src-text.txt', 'r') as fin, open('src-text-unique.txt', 'w') as fout:
for line in fin:
h = hash(line)
if h not in seen:
fout.write(line)
seen.add(h)
注意事项:
- 如果您担心 64 位哈希冲突,您可以使用不同的哈希,例如
hashlib.md5() 或 hashlib.sha256()。
- 如果您没有足够的内存来存储行的哈希值,您可以使用 BloomFilter 来代替内存的有限使用(以较高的误报率为代价)。
- 作为第三种选择,在某种程度上受到@dawg 的“装饰线条”想法的启发,如果您的内存实在太紧而无法存储所有哈希或足够大的BloomFilter,您可以将文件拆分为@987654326 @,根据
hash(line) % n_parts;在这些临时文件中,将原始行号与每一行一起存储。然后分别对每个部分应用重复数据删除,然后合并(已经排序的部分文件)。这避免了n log n 排序,而是O(n)。然而,该技术不适用于流处理,而前两个(散列集或 BloomFilter)适用,直到不同项目的数量太大并导致 MemoryError(散列集)或太大误报率 (BloomFilter)。
附录:碰撞率和内存大小的简短分析:
假设您的 32GB 文件平均有 60 个字符的行,并且 10% 的行是重复的(90% 不同的行)。
这会导致:
n_distinct = int(0.9 * 32 * 1024**3 / 60)
>>> n_distinct
515_396_075
换句话说,大约有 1/2 亿条不同的行。这就是我们必须存储在内存中的哈希数,以及 BloomFilter 分析中的 n 值。
哈希的大小和碰撞概率:
参考Birthday attack中描述的用于估计碰撞概率的众所周知的公式,并使用泰勒展开的前两项 1 - e-x ≈ x - x2/2,我们得到n 位和k 不同项的分布良好的哈希的冲突概率为:
def prob_collision(n_bits, n_distinct):
n, k = n_bits, n_distinct # usual notation
x = k**2 / 2**(n + 1)
return x - x**2 / 2 # approx of 1 - exp(-x)
- 使用内置的
hash(),每个散列是一个64位整数:8字节。所以我们需要 3.8GiB 的内存。整个文件中至少发生一次冲突(意思是:至少有一行被错误地识别为已被看到而实际上并未被看到)的概率为 0.7%。
- 使用
md5(128 位),您需要双倍内存,7.7GiB,但冲突概率为 3.9*10-22。
布隆过滤器的大小和碰撞概率:
具有m 位的Bloom Filter 并打算存储多达n 项应使用最小化误报率的最佳散列函数数量:k = m / n * log(2)。使用该数量的函数,误报率 (FPR) 大致为:(1 - e-kn/m)k。至少发生一次碰撞的概率(远)小于FPR * n(它大致是 FPR 对 0..n_distinct 中的 n 的积分)。
def bloom_k(m, n):
return int(np.ceil(m / n * np.log(2)))
def bloom_false_positive_rate(m, n, k=None):
k = bloom_k(m, n) if k is None else k
fpos = np.power(1 - np.exp(-k*n/m), k)
return fpos
def bloom_prob_collision(m, n, k=None):
# too lazy to figure out the integral, so
# using this instead which is slow as molasses
k = bloom_k(m, n) if k is None else k
i = np.arange(0, n + 1)
p_coll = np.sum(np.power(1 - np.exp(-k*i/m), k))
return p_coll
- 使用与上面提到的
n_distinct 64 位散列 (3.8Gib) 相同的内存量,我们得到m = 32_641_751_449 位、k = 44 散列函数,并且至少发生一次冲突的概率要小得多:大约 0.0001%。