simHash的概念及介绍,我就不赘述了,搜一下到处是,我也是查了一些资料加上自己业务需求,最后整理了一份java实现的工具方法,如有不妥指出,欢迎指出。
由于需要要求15天内的数据去重,日记越累数据比较多,所以采用了64位分四段的方式进行比较,但是为了较少缓存,在存储的时候转成了BigInteger类型进行存储,所以在计算海明距离的时候,用了两种方法实现。
话不多说,直接上代码吧:
package com.jandmin.test.simhash;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.hankcs.hanlp.seg.common.Term;
import com.hankcs.hanlp.tokenizer.IndexTokenizer;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.safety.Whitelist;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* SimHash 去重实现类
*
* @Author: JandMin
* @since: 1.0.0
* @Date: 2019/2/20
*/
@Component
public class SimHashHelper {
/**
* 指纹的长度
*/
private int bitLength = 64;
private int overCount = 5;
/**
* 十进制的指纹
*/
private BigInteger intSimHash;
/**
* 二进制的指纹
*/
private String strSimHash;
/**
* 二进制指纹的4个子指纹
*/
private List shortSimHashList;
public BigInteger getIntSimHash(){
return this.intSimHash;
}
public String getStrSimHash() {
return this.strSimHash;
}
public List getShortSimHashList() {
return shortSimHashList;
}
public int getBitLength() {
return bitLength;
}
/**
* 下面这两个目前没有用到,保留下来是为了后期可扩展
*
* 停用的词性
*/
private static Set stopNatures = Sets.newHashSet();
/**
* 特殊词的权重
*/
private static Map weightOfNature = Maps.newHashMap();
static {
// 停用词性为w:标点
stopNatures.add("w");
// 这里将n:名词设置为2。(默认权重为1)
weightOfNature.put("n",2);
}
public SimHashHelper(){}
public SimHashHelper(int bitLength){
this.bitLength = bitLength;
}
public static void main(String[] args) {
String str1 = "新华社北京12月6日电 12月6日,结束了对西班牙、阿根廷、巴拿马、葡萄牙国事访问并出席二十国集团领导人第十三次峰会后,国家主席习近平回到北京。习近平主席夫人彭丽媛,中共中央政治局委员、中央书记处书记、中央办公厅主任丁薛祥,中共中央政治局委员、中央外事工作委员会办公室主任杨洁篪,国务委员兼外交部部长王毅,全国政协副主席、国家发展和改革委员会主任何立峰等陪同人员同机抵达。当地时间5日下午,习近平离开里斯本启程回国。葡萄牙外长席尔瓦等到机场送行。专机离开葡萄牙领空前,两架葡萄牙空军战机升空护航";
String str2 = "新华社北京12月6日电 12月6日,在结束对西班牙、阿根廷、葡萄牙等国事访问并出席二十国集团领导人第十三次峰会后,国家主席习近平回到北京。习近平主席夫人彭丽媛,中共中央政治局委员、中央书记处书记、中央办公厅主任丁薛祥,中共中央政治局委员、中央外事工作委员会办公室主任杨洁篪,国务委员兼外交部部长王毅,全国政协副主席、国家发展和改革委员会主任何立峰等陪同人员同机抵达。当地时间5日下午,习近平离开里斯本启程回国。葡萄牙外长席尔瓦等到机场送行。专机离开葡萄牙领空前,两架葡萄牙空军战机升空护航。";
String str3 = "杭州报社北京12月6日电 12月6日,在结束对西班牙、阿根廷、巴拿马、葡萄牙国事访问并出席二十国集团领导人第十三次峰会后,国家主席习近平回到北京。习近平主席夫人彭丽媛,中共中央政治局委员、中央书记处书记、中央办公厅主任丁薛祥,中共中央政治局委员、中央外事工作委员会办公室主任杨洁篪,国务委员兼外交部部长王毅,全国政协副主席、国家发展和改革委员会主任何立峰等陪同人员同机抵达。当地时间5日下午,习近平离开里斯本启程回国。葡萄牙外长席尔瓦等到机场送行。专机离开葡萄牙领空前,两架葡萄牙空军战机升空护航。";
String str5 = "CTO 职场解惑指南系列(一)";//4059115178153530904 4
String str6 = "CTO 职场解惑指南系列(二)";//4059115179294365212
String str7 = "你妈妈喊你回家吃饭哦,回家罗回家罗";//4237218111462 1
String str8 = "你妈妈叫你回家吃饭啦,回家罗回家罗";//4237218095078
SimHashHelper simHashHelper1 = new SimHashHelper();
System.out.println(simHashHelper1.simHash(str7));
System.out.println(simHashHelper1.getIntSimHash());
System.out.println(simHashHelper1.getIntSimHash(simHashHelper1.getStrSimHash()));
System.out.println(simHashHelper1.getStrSimHash());
SimHashHelper simHashHelper2 = new SimHashHelper();
System.out.println(simHashHelper2.simHash(str8));
System.out.println(simHashHelper2.getStrSimHash());
System.out.println(simHashHelper2.getIntSimHash());
System.out.println(simHashHelper2.getIntSimHash(simHashHelper2.getStrSimHash()));
System.out.println("================================");
System.out.println(simHashHelper2.hammingDistance(simHashHelper2.getStrSimHash(),simHashHelper1.getStrSimHash()));
System.out.println(simHashHelper1.hammingDistance(simHashHelper2.getIntSimHash(),simHashHelper1.getIntSimHash()));
}
/**
* 过滤
* @date: 2019/2/20
* @param content 待分词的文本
* @return java.lang.String
* @throws
*/
public String preProcess(String content) {
if(StringUtils.isBlank(content)){
return "";
}
// 过滤掉所有的HTML的tag
content = Jsoup.clean(content, Whitelist.none());
String[] strings = {"\n","\r","\\r","\\n","\\t"," "," "};
for (String s:strings) {
content = content.replace(s,"");
}
content = content.replaceAll("[\\p{P}+~$`^=|<>~`$^+=|<>¥×]", "");
return content;
}
/**
* 主方法,计算simHash值
* @date: 2019/2/20
* @param content
* @return java.util.List
* @throws
*/
public List simHash(String content) {
content = preProcess(content);
if(StringUtils.isBlank(content)){
return Lists.newArrayList();
}
// 词的权重
Map wordCount = Maps.newHashMap();
// 定义特征向量/数组
int[] hashValue = new int[this.bitLength];
// 分词:索引分词
List indexList = IndexTokenizer.segment(content);
for (Term term : indexList){
String word = term.word;
String nature = term.nature.toString();
// 过滤停用词性
if (stopNatures.contains(nature)) {
continue;
}
// 加权
if (wordCount.containsKey(word)) {
int count = wordCount.get(word);
if(count > this.overCount) {
continue;
}
wordCount.put(word,count+1);
} else {
wordCount.put(word,1);
}
// 计算hash,将每一个分词hash为一组固定长度的数列.比如 64bit 的一个整数.
BigInteger t = this.hash(word);
for (int i = 0; i < this.bitLength; i++) {
BigInteger bitmask = new BigInteger("1").shiftLeft(i);
// 对每一个分词hash后的数列进行判断,如果是1000...1,那么数组的第一位和末尾一位加1,
// 中间的62位减一,也就是说,逢1加1,逢0减1.一直到把所有的分词hash数列全部判断完毕.
int weight = 1;
if (weightOfNature.containsKey(nature)) {
weight = weightOfNature.get(nature);
}
if (t.and(bitmask).signum() != 0) {
// 这里是计算整个文档的所有特征的向量和
hashValue[i] += weight;
} else {
hashValue[i] -= weight;
}
}
}
BigInteger fingerprint = new BigInteger("0");
StringBuffer simHashBuffer = new StringBuffer();
// 合并,降维
for (int i = 0; i < this.bitLength; i++) {
// 最后对数组进行判断,大于0的记为1,小于等于0的记为0,得到一个 64bit 的数字指纹
if (hashValue[i] >= 0) {
fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i));
simHashBuffer.append("1");
} else {
simHashBuffer.append("0");
}
}
this.strSimHash = simHashBuffer.toString();
this.intSimHash = fingerprint;
List simHashList = new ArrayList();
simHashList.add(simHashBuffer.substring(0,this.bitLength/4));
simHashList.add(simHashBuffer.substring(this.bitLength/4,this.bitLength/4*2));
simHashList.add(simHashBuffer.substring(this.bitLength/4*2,this.bitLength/4*3));
simHashList.add(simHashBuffer.substring(this.bitLength/4*3,this.bitLength));
this.shortSimHashList = simHashList;
return simHashList;
}
/**
* 计算每个词的hash
* @date: 2019/2/20
* @param source
* @return java.math.BigInteger
* @throws
*/
private BigInteger hash(String source) {
if (null == source || source.length() == 0) {
return new BigInteger("0");
} else {
char[] sourceArray = source.toCharArray();
BigInteger x = BigInteger.valueOf(((long) sourceArray[0]) << 7);
BigInteger m = new BigInteger("1000003");
BigInteger mask = new BigInteger("2").pow(this.bitLength).subtract(new BigInteger("1"));
for (char item : sourceArray) {
BigInteger temp = BigInteger.valueOf((long) item);
x = x.multiply(m).xor(temp).and(mask);
}
x = x.xor(new BigInteger(String.valueOf(source.length())));
if (x.equals(new BigInteger("-1"))) {
x = new BigInteger("-2");
}
return x;
}
}
/**
* 计算hamming距离
* @date: 2019/2/20
* @param bigInteger1
* @param bigInteger2
* @return int
* @throws
*/
public int hammingDistance(BigInteger bigInteger1,BigInteger bigInteger2) {
BigInteger x = bigInteger1.xor(bigInteger2);
int tot = 0;
// 统计x中二进制位数为1的个数
// 我们想想,一个二进制数减去1,那么,从最后那个1(包括那个1)后面的数字全都反了,对吧,然后,n&(n-1)就相当于把后面的数字清0,
// 我们看n能做多少次这样的操作就OK了。
while (x.signum() != 0) {
tot += 1;
x = x.and(x.subtract(new BigInteger("1")));
}
return tot;
}
/**
* 计算二进制的hamming距离
* @date: 2019/2/20
* @param str1
* @param str2
* @return int
* @throws
*/
public int hammingDistance(String str1, String str2) {
int distance;
if (str1.length() != str2.length()) {
distance = -1;
} else {
distance = 0;
for (int i = 0; i < str1.length(); i++) {
if (str1.charAt(i) != str2.charAt(i)) {
distance++;
}
}
}
return distance;
}
/**
* 根据二进制simHash 获取 intSimHash
* @date: 2019/2/20
* @param strSimHash
* @return java.math.BigInteger
* @throws
*/
public BigInteger getIntSimHash(String strSimHash){
BigInteger fingerprint = new BigInteger("0");
StringBuffer simHashBuffer = new StringBuffer();
char[] hashValue = strSimHash.toCharArray();
for (int i = 0; i < this.bitLength; i++) {
if (hashValue[i] == '1') {
fingerprint = fingerprint.add(new BigInteger("1").shiftLeft(i));
}
}
return fingerprint;
}
}