海量文本去重simhash算法(python&scala)

1.python(Numpy实现)

具体公式见reference中的论文。

# -*- coding: utf-8 -*-
"""
Created on Mon May 19 09:32:00 2018

@author: wangyao
"""
import jieba
#simhash值直接用包计算,pip install simhash
from simhash import Simhash
import re
import numpy as np
import pandas as pd

#停用词
stopwords = [line.strip() for line in open('Stopwords.txt', 'r', encoding='utf-8').readlines()] 

#文本预处理+特征提取
def get_features(s):
    width = 3
    string = ''
    s = re.sub(r'[^\w]+', '', s)
    s = jieba.lcut(s)
    X,Y = ['\u4e00','\u9fa5']
    s = [ i for i in s if len(i) > 1  and X<=i<=Y and i not in stopwords]
    for i in s:
        string += i
    if string:
        return [string[i:i + width] for i in range(max(len(string) - width + 1, 1))]
    else:
        print ("请输入中文文档")

#list1 = df.content.apply(lambda x: isinstance(x, str))
        
#文本预处理
def Pre_Processing(s):
    string = ''
    s = re.sub(r'[^\w]+', '', s)
    s = jieba.lcut(s)
    X,Y = ['\u4e00','\u9fa5']
    s = [i for i in s if len(i) > 1  and X<=i<=Y and i not in stopwords]
    string = string.join(s)
    if string:
        return string
    else:
        print('请勿输入空字符串或者完全由停用词组成的无意义的句子')

#simhash包自带的汉明距离
def hanming_simhash(s1,s2):
    hanmingdistance =  Simhash(Pre_Processing(s1)).distance(Simhash(Pre_Processing(s2)))
    #return hanming_distance
    return 1-hanmingdistance/64

#将字符串转化为hashcode
def ToSimhashcode(s):
    if type(s) == str:
        return Simhash(get_features(s)).value
    else:
        print('输入的句子格式需要是字符串')

#自己写的汉明距离
def hanming_distance(s1,s2):
    if type(s1) == str and type(s2) == str:
        hanmingdistance = bin(int(hex(Simhash(get_features(s1)).value),16)^int(hex(Simhash(get_features(s2)).value),16)).count('1')
    elif type(s1) == int and type(s2) == int:
        hanmingdistance = bin(int(hex(s1),16)^int(hex(s2),16)).count('1')
    else:
        print ('s1和s2需要是相同的数据类型!')
    #return hanming_distance
    return 1-hanmingdistance/64

#直接填入文本列表,生成相似度矩阵(文本较少的时候可以使用此函数)
def simhash_simmatrix_doc(doc_list):
    simhash_corpus = []
    for i in range(len(doc_list)):
        simhash_corpus.append([])
    for i in range(len(doc_list)):
        print (i)
        for j in range(i,len(doc_list)):
            simhash_corpus[j].append(hanming_distance(doc_list[i],doc_list[j]))
    x = len(simhash_corpus)-1
    for i in range(len(simhash_corpus)-1):
        simhash_corpus[i].extend(x*[0])
        x = x - 1
    return np.array(simhash_corpus) + np.array(simhash_corpus).T

#填入文本的hashcode,生成相似度矩阵
def simhash_simmatrix_hashcode(hashcode_list):
    simhash_corpus = []
    for i in range(len(hashcode_list)):
        simhash_corpus.append([])
    for i in range(len(hashcode_list)):
        print (i)
        for j in range(i,len(hashcode_list)):
            simhash_corpus[j].append(hanming_distance(hashcode_list[i],hashcode_list[j]))
    x = len(simhash_corpus)-1
    for i in range(len(simhash_corpus)-1):
        simhash_corpus[i].extend(x*[0])
        x = x - 1
    return np.array(simhash_corpus) + np.array(simhash_corpus).T

#过滤文本生成的相似度矩阵,
def DuplicateContent_filtering_doc(doc_list,sim = 0.8):
    if sim>1 or sim<0:
        print ('错误的取值范围!!范围应该是[0,1]')
    sim_matrix = simhash_simmatrix_doc(doc_list)>sim
    return sim_matrix

#过滤hashcode生成的相似度矩阵
def DuplicateContent_label_hashcode(hashcode_array,sim = 0.8):
    if sim>1 or sim<0:
        print ('错误的取值范围!!范围应该是[0,1]')
    sim_matrix = hashcode_array>sim
    return sim_matrix

#列表求交集
def list_intersection(list1,list2):
    list3 = list(set(list1) & set(list2))
    return list3

#清洗重复的文章列表
def clean_Duplicate_List(Duplicate_List):
    Duplicate_List = [list(set(i)) for i in Duplicate_List]
    Duplicate = []
    for i in range(len(Duplicate_List)):
        temp = []
        for j in range(len(Duplicate_List)):
            if list_intersection(Duplicate_List[i],Duplicate_List[j]) != []:
                temp.extend(Duplicate_List[i])
                temp.extend(Duplicate_List[j])
        Duplicate.append(temp)
    Duplicate = [list(set(i)) for i in Duplicate]
    NoDuplicate = []
    for i in  Duplicate:
        if i in NoDuplicate:
            pass
        else:
            NoDuplicate.append(i)
    NoDuplicate = [list(set(i)) for i in NoDuplicate]
    return NoDuplicate

#读入相似度矩阵,返回非重复的文章ID和重复的文章ID
def DuplicateContent_filtering_hashcode(matrix):
    NoDuplicateList = []
    DuplicateList = []
    NoDuplicate_List = []
    Duplicate_List = []
    for i in range(len(matrix)):
        DuplicateList.append([])
    for i in range(len(matrix)):
        NoDuplicateList.append([])
        if (matrix[i] == True).sum() <= 1:
            NoDuplicateList[i].append(ID[i]) 
        else:
            for j in range(len(matrix[i])):
                if matrix[i][j]==True  and i < j:
                    DuplicateList[i].extend([i,j])
    for i in DuplicateList:
        if i != []:
            Duplicate_List.append(i)
    else:
        pass
    for i in NoDuplicateList:
        if i != []:
            NoDuplicate_List.append(i)
    else:
        pass
    return  NoDuplicate_List,clean_Duplicate_List(Duplicate_List)

短文本,如果文本很短,可以直接调用simhash_simmatrix_doc,直接读入字符串即可产生结果。

df = pd.read_excel('00.xlsx')
df.content = df.content.apply(Pre_Processing)
content_list = df.content.tolist()
simhash_simmatrix_doc(content_list)

如果需要存进hive表中,需要先把矩阵转换成pandas中的dataframe再转化为spark中的dataframe。

//转化
values=pandas_df.values.tolist()
cols_name=list(pandas_df.columns)
spark_df  = spark.createDataFrame(values, cols_name)
turn_pandas_df = spark_df.toPandas()

如果是是直接使用spark,两两比较可以使用reducebykey来聚合,查找的时候才需要使用索引。如果是在python中按照上面的代码两两比较会非常耗时,所以比较的时候就需要引入simhash的索引。
网上的例子:

from simhash import Simhash, SimhashIndex
# 建立索引
data = {
u'1': u'How are you I Am fine . blar blar blar blar blar Thanks .'.lower().split(),
u'2': u'How are you i am fine .'.lower().split(),
u'3': u'This is simhash test .'.lower().split(),
}
objs = [(id, Simhash(sent)) for id, sent in data.items()]
index = SimhashIndex(objs, k=10) # k是容忍度;k越大,检索出的相似文本就越多
# 检索
s1 = Simhash(u'How are you . blar blar blar blar blar Thanks'.lower().split())
print index.get_near_dups(s1)
# 增加新索引
index.add(u'4', s1)

2.scala实现

scala中没有simhash包,需要自己实现FNVHash来给文本转码。
目前用的比较多的是FNV-1和FNV-1a,FNV-0已弃用。

FNV-1:

hash = offset_basis
for each octet_of_data to be hashed
    hash = hash * FNV_prime
    hash = hash xor octet_of_data
return hash

FNV-1a:

hash = offset_basis
for each octet_of_data to be hashed
    hash = hash xor octet_of_data
    hash = hash * FNV_prime
return hash

FNV-1a算法只是将相乘和异或运算进行了顺序调换。

import java.math.BigInteger

object FNVHash {
  val HASH_BITS = 64

  val FNV_64_INIT = new BigInteger("14695981039346656037")
  val FNV_64_PRIME = new BigInteger("1099511628211")
  val MASK_64: BigInteger = BigInteger.ONE.shiftLeft(HASH_BITS).subtract(BigInteger.ONE)


  val str = "电风扇卡机是可敬的搞屎棍的"
  val str1 = "电风扇卡机是可敬的搞屎棍的对对对"
  /**
    * fnv-1 hash算法,将字符串转换为64位hash值
    *
    * @param str str
    * @return
    */
  def fnv1Hash64(str: String): BigInteger = {
    var hash = FNV_64_INIT
    val len = str.length
    for (i <- 0 until len) {
      hash = hash.multiply(FNV_64_PRIME)
      hash = hash.xor(BigInteger.valueOf(str.charAt(i)))
    }
    hash = hash.and(MASK_64)
    hash
  }

  /**
    * fnv-1a hash算法,将字符串转换为64位hash值
    *
    * @param str str
    * @return
    */
  def fnv1aHash64(str: String): BigInteger = {
    var hash = FNV_64_INIT
    val len = str.length
    var i = 0
    for (i <- 0 until len) {
      hash = hash.xor(BigInteger.valueOf(str.charAt(i)))
      hash = hash.multiply(FNV_64_PRIME)
    }
    hash = hash.and(MASK_64)
    hash
  }

  /**
    * 返回二进制串hash距离
    *
    * @param str1 str1
    * @param str2 str2
    * @return
    */
  def getDistance(str1: String, str2: String): Int = {
    var distance = 0
    if (str1.length != str2.length) distance = -1
    else {
      distance = 0
      val len = str1.length
      for (i <- 0 until len) {
        if (str1.charAt(i) != str2.charAt(i)) distance += 1
      }
    }
    distance
  }

  def hashContent_d(content: Array[String]): BigInteger = {
    val featureVector: Array[Double] = new Array[Double](FNVHash.HASH_BITS)
    val wordInfos = content.map((_, 1)).groupBy(_._1).
      map(r => (r._1, r._2.length.toDouble)).
      map(r => {
        val wordHash = FNVHash.fnv1aHash64(r._1)
        for (i <- 0 until FNVHash.HASH_BITS) {
          val bitmask: BigInteger = BigInteger.ONE.shiftLeft(FNVHash.HASH_BITS - i - 1)
          if (wordHash.and(bitmask).signum != 0)
            featureVector(i) += r._2.toDouble
          else
            featureVector(i) -= r._2.toDouble
        }
      })
    var signature: BigInteger = BigInteger.ZERO
    for (i <- 0 until FNVHash.HASH_BITS) {
      if (featureVector(i) >= 0) {
        signature = signature.add(BigInteger.ONE.shiftLeft(FNVHash.HASH_BITS - i - 1))
      }
    }
    signature
  }


  def hashContent(content: Array[String]): BigInteger = {
    val featureVector: Array[Double] = new Array[Double](HASH_BITS)
    val wordInfos = content.map((_, 1)).groupBy(_._1).
      map(r => (r._1, r._2.length.toDouble)).
      map(r => {
        val wordHash = fnv1aHash64(r._1)
        for (i <- 0 until HASH_BITS) {
          val bitmask: BigInteger = BigInteger.ONE.shiftLeft(HASH_BITS - i - 1)
          if (wordHash.and(bitmask).signum != 0)
            featureVector(i) += r._2.toDouble
          else
            featureVector(i) -= r._2.toDouble
        }
      })
    var signature: BigInteger = BigInteger.ZERO
    for (i <- 0 until HASH_BITS) {
      if (featureVector(i) >= 0) {
        signature = signature.add(BigInteger.ONE.shiftLeft(HASH_BITS - i - 1))
      }
    }
    signature
  }

  def hashContentStr(content: String): BigInteger = {
    hashContent(content.split(" "))
  }

  // 判断汉明距离是否大于kBits
  def distance(hash1: Long, hash2: Long, kBits: Int): Boolean = {
    var n = hash1 ^ hash2
    for (i <- 0 until kBits if n > 0) {
      n = n & (n - 1)
    }
    n == 0
  }
}

Reference:

《Similarity estimation techniques from rounding algorithms》
https://www.cnblogs.com/maybe2030/p/5203186.html#_label4

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266