前言,最近在搞大量数据插入MySQL的时候悲催的发现速度越来越慢,因为我的数据来多个源,使用流式更新,而且产品要求在这个表里面不能有数据重复,划重点!衡量数据是否重复的字段是文本内容,字段类型是text,…那么问题来了,如何在千万级数据量实现去重插入呢?而且要快!
1.管它重复不重复,先插入了再说
2.使用group by 先对不能重复的字段进行分组,在用一个having count(
3.使用for循环查询每条存在重复记录的句子,保留最小id的句子记录,其余删除。
这样的做法导致我每次对全表进行一次去重,大概需要花费7个小时的时间…
后面,经过老大的点拨,发现可以用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,如不为空,那么该句子重复。
参考资料: