使用SimHash算法实现千万级文本数据去重插入(python版代码)

前言,最近在搞大量数据插入MySQL的时候悲催的发现速度越来越慢,因为我的数据来多个源,使用流式更新,而且产品要求在这个表里面不能有数据重复,划重点!衡量数据是否重复的字段是文本内容,字段类型是text,…那么问题来了,如何在千万级数据量实现去重插入呢?而且要快!

自杀式做法

1.管它重复不重复,先插入了再说
2.使用group by 先对不能重复的字段进行分组,在用一个having count()>1把重复的句子给拿出来
3.使用for循环查询每条存在重复记录的句子,保留最小id的句子记录,其余删除。

这样的做法导致我每次对全表进行一次去重,大概需要花费7个小时的时间…
使用SimHash算法实现千万级文本数据去重插入(python版代码)_第1张图片
后面,经过老大的点拨,发现可以用simhash算法进行去重,二话不说,撸起袖子就是干呗!

SimHash式优雅做法

simhash是由 Charikar 在2002年提出来的,参考 《Similarity estimation techniques from rounding algorithms》,这个文章的基本思想就是相似的文档具有相似的hash指纹,那么可以通过其hash指纹值来的对比来实现文档的去重。这个算法被google应用于处理海量文本去重,其效果肯定是大大的有,特别是对于长文本的去重。在github上,找到了大牛写好的simhash包,先结合源码,来谈谈simhash的一个去重原理。

step1 安装simhash包

pip install simhash
(or: easy_install simhash)

step2 特征提取
拿到文档之后,先对文档做特征抽取,抽取出文档中的关键词以及对应的权重,常见的做法有TF-IDF,然后处理的文档如果是中文话,需要进行分词,常用的开源分词包有jieba,snowNLP,甚至可以用sentencePiece自己训练一个分词模型。在demo中,使用的是作者在外部写的一个获得特征的方法,获得一个List:

"""
设置一个长度为3的滑动窗口,并只匹配数字英文加下划线,如输入'你好啊,今天真高兴':
返回['你好啊', '好啊今', '啊今天', '今天真', '天真高', '真高兴']
"""
def get_features(s):
    width = 3
    s = s.lower()
    s = re.sub(r'[^\w]+', '', s)
    return [s[i:i + width] for i in range(max(len(s) - width + 1, 1))]

在源码中,对传入的value做了不同的处理,如果传入的值是simhash或者数字,那就不做处理,如果传入了一个list,那么就进入build_by_features函数中,如果是原始的字符串文本,那么就进入build_by_text函数:

if isinstance(value, Simhash):
    self.value = value.value
elif isinstance(value, basestring):
    self.build_by_text(unicode(value))
elif isinstance(value, collections.Iterable):
    self.build_by_features(value)
elif isinstance(value, numbers.Integral):
    self.value = value
else:
    raise Exception('Bad parameter with type {}'.format(type(value)))

其中的build_by_text函数最终也是调用了build_by_features,在这之前,build_by_text调用了 _tokenize函数,这个函数和之前的get_features函数差不多,这里默认的滑动窗口宽度是4.

 def _tokenize(self, content):
     content = content.lower()
     content = ''.join(re.findall(self.reg, content))
     ans = self._slide(content)
     return ans

之后将得到的词块list进行统计,输出一个字典,key是词块,value是频数,最后将这个字典输入build_by_features函数。build_by_features函数也接受单层list输入,如[‘我’,‘爱’,‘python’],之前会自动给这些词块赋1的权重。

step3 将特征词转换为SimHash值
在这个阶段需要使用到hash,想必大家对hash这个词都是耳熟能详了,但是hash值是怎么算出来的,在这里简单说一下,无论你多长的输入A,都会输出一段固定的值B,B这个值就是A数据的指纹,也就是散列表,我们知道指纹基本上是独一无二的,那么对于hash值来说,要找到一个hash值的两个不同的输入,在计算上是不可能的,因此这也是为什么hash值能够用于去重的原理。在这个simhash中,默认的hash算法是MD5,当然你可以传入sha1, sha256其它的算法,python的hashlib包已经封装好了。其中默认的指纹长度为64。

def _hashfunc(x):
    return int(hashlib.md5(x).hexdigest(), 16)   #后面的16是指16进制,得到转换为10进制的数

计算完hash之后,便按照单词的权重形成加权数字串,再使用位与运算和位或运算得到最终的表示句子指纹的simhash,其中value是就是这里算出来的ans,是一个int型的数值串。

  v = [0] * self.f
        masks = [1 << i for i in range(self.f)]
        if isinstance(features, dict):
            features = features.items()
        for f in features:
            if isinstance(f, basestring):
                h = self.hashfunc(f.encode('utf-8'))
                w = 1
            else:
                assert isinstance(f, collections.Iterable)
                h = self.hashfunc(f[0].encode('utf-8'))
                w = f[1]
            for i in range(self.f):
                v[i] += w if h & masks[i] else -w   #在这一步实现了加权
        ans = 0
        for i in range(self.f):
            if v[i] > 0:
                ans |= masks[i]       #位或运算
        self.value = ans

step4 比较不同文本之间的距离
算好了不同文本的simhash值,然后就可以使用计算之间的距离了,利用位运算得到距离,这里的距离叫做海明距离,比如, 10101 和 00110 从第一位开始依次有第一位、第四、第五位不同,则海明距离为3:

 def distance(self, another):
     assert self.f == another.f
     x = (self.value ^ another.value) & ((1 << self.f) - 1)
     ans = 0
     while x:
         ans += 1
         x &= x - 1
     return ans

小结
以上就是simhash计算的一个基本流程。作者还封装了一个好用的SimhashIndex便于进行大规模数据去重,示例代码如下:

import re
from simhash import Simhash, SimhashIndex
def get_features(s):
    width = 3
    s = s.lower()
    s = re.sub(r'[^\w]+', '', s)
    return [s[i:i + width] for i in range(max(len(s) - width + 1, 1))]

data = {
    1: u'How are you? I Am fine. blar blar blar blar blar Thanks.',
    2: u'How are you i am fine. blar blar blar blar blar than',
    3: u'This is simhash test.',
}
objs = [(str(k), Simhash(get_features(v))) for k, v in data.items()]
index = SimhashIndex(objs, k=3)

print index.bucket_size()

s1 = Simhash(get_features(u'How are you i am fine. blar blar blar blar blar thank'))
print index.get_near_dups(s1)

index.add('4', s1)			
print index.get_near_dups(s1)    

大致流程就是用SimhashIndex初始化数据,其中bucket存储了对应句子的simhash值,get_near_dups()函数返回的是和输入句子相似的句子id的列表,这里默认的海明距离是3(可以修改的,不过论文里提到选择3是一个比较折中的方案),如果拿来去重的话,那就是判断get_near_dups()返回结果是否为空的list,如不为空,那么该句子重复。

使用SimHash算法实现千万级文本数据去重插入(python版代码)_第2张图片
用了simhash后,流式大数据去重不再烦恼!
使用SimHash算法实现千万级文本数据去重插入(python版代码)_第3张图片

参考资料:

  • https://github.com/leonsim/simhash
  • https://leons.im/posts/a-python-implementation-of-simhash-algorithm/
  • http://www.lanceyan.com/tech/arch/simhash_hamming_distance_similarity.html
  • http://www.wwwconference.org/www2007/papers/paper215.pdf

你可能感兴趣的:(python,SimHash,python,文本去重)