一文了解Simhash原理和用法-计算文章相似度

Simhash原理

1:背景

SimHash算法是Google在2007年发表的论文《Detecting Near-Duplicates for Web Crawling》中提到的一种指纹生成算法,被应用在Google搜索引擎网页去重的工作之中。SimHash值不但提供了原始值是否相等这一信息,还能通过该值计算出内容的差异程度。

简单的说,SimHash算法主要的工作就是将文本进行降维,生成一个SimHash值,也就是论文中所提及的“指纹”,通过对不同文本的SimHash值进而比较“海明距离”,从而判断两个文本的相似度。

对于文本相似度的问题,常见的解决办法有欧式距离、编辑距离、最长公共子串、余弦算法、Jaccard相似度等方法。但是这些方法并不能对海量数据高效的处理。

比如说,在搜索引擎中,会有很多相似的关键词,用户所需要获取的内容是相似的,但是搜索的关键词却是不同的,例如:
“年龄大于30岁的自然人?”
“年龄大于50岁的人?”
以上两个可以等价的关键词,然而通过普通的hash计算,会产生两个相差甚远的hash串。而通过SimHash计算得到的Hash串会非常的相近,从而可以判断两个文本的相似程度。

2:simhash与hash算法的区别

传统Hash算法只负责将原始内容尽量均匀随机地映射为一个签名值,原理上仅相当于伪随机数产生算法。传统hash算法产生的两个签名,如果不相等,除了说明原始内容不相等外,不再提供任何信息,因为即使原始内容只相差一个字节,所产生的签名也很可能差别很大,
所以传统Hash是无法在签名的维度上来衡量原内容的相似度。而SimHash本身属于一种局部敏感哈希算法,它产生的hash签名在一定程度上可以表征原内容的相似度。

我们主要解决的是文本相似度计算,要比较的是两个文章是否相似,当然我们降维生成了hash签名也是用于这个目的。看到这里,估计大家就明白了,即使把文章中的字符串变成 01 串,我们使用的simhash算法也还是可以用于计算相似度,而传统的hash却不行。

3:原理

SimHash是一种局部敏感hash。我们都知道什么是hash。那什么叫局部敏感呢,假定A、B具有一定的相似性,在hash之后,仍然能保持这种相似性,就称之为局部敏感hash。其主要思想是降维,将高维的特征向量转化成一个f位的指纹(fingerprint),通过算出两个指纹的海明距离(hamming distince)来确定两篇文章的相似度,海明距离越小,相似度越低(根据 Detecting Near-Duplicates for Web Crawling 论文中所说),一般海明距离为3就代表两篇文章相同。
    
举例:
比如,我们得到一个文档的关键词,取得一篇文章关键词集合,又会降低对比效率,我们可以通过hash的方法,把上述得到的关键词集合hash成一串二进制,这样我们直接对比二进制数,看其相似性就可以得到两篇文档的相似性,在查看相似性的时候我们采用海明距离,即在对比二进制的时候,我们看其有多少位不同,就称海明距离为多少。在这里,我是将文章simhash得到一串64位的二进制,一般取海明距离为3作为阈值,即在64位二进制中,只有三位不同,我们就认为两个文档是相似的。当然了,这里可以根据自己的需求来设置阈值。

就这样,我们把一篇文档用一个二进制代表了,也就是把一个文档hash之后得到一串二进制数的算法,称这个hash为simhash。

4:局限性

根据上边的原理,我们可以得到simhash也有其局限性,在处理小于500字的短文本时,simhash的表现并不是很好,所以在使用simhash前一定要注意这个细节。

5:海明距离

海明距离是编辑距离其中之一,在信息编码中,两个合法代码对应位上编码不同的位数称为码距,又称海明距离。即两个码字的对应比特取值不同的比特数称为这两个码字的海明距离。一个有效编码集中任意两个码字的海明距离的最小值称为该编码集的海明距离。例如: 10101 和 00110 从第一位开始依次有第一位、第四、第五位不同,则海明距离为 3。
N位的码字可以用N维空间的超立方体的一个顶点来表示,两个码字之间的海明距离就是超立方体两个顶点之间的一条边,而且是这两个顶点之间的最短距离。

海明距离应用最多的是在海量短文、文本去重上,以其性能优的特点。海明距离主要就是对文本进行向量化,或者说是把文本的特征抽取出来映射成编码,然后再对编码进行异或计算出海明距离。

Simhash 计算步骤

simhash的算法具体分为5个步骤:分词、hash、加权、合并、降维,具体过程如下:

1.分词

给定一段语句或者一段文本,进行分词,得到有效的特征向量,然后为每一个特征向量设置一个5个级别(1—5)权值。例如给定一段语句:“生活本没有路,走的人多了就成了路,要相信阳光总在风雨后”,分词后结果为:生活 没有 成了 相信 阳光 风雨,然后为每个特征向量赋予权值: 生活(5) 没有(2) 成了(1) 相信(2) 阳光(3) 风雨(2),其中括号里的数字代表这个单词在整条语句中的重要程度,数字越大代表越重要。

也可以根据不同的企业自己指定权重,反正就是个比例数字,下边也给出一种常规使用的权重值计算(TF-IDF算法):
TF:词频
IDF:逆向词频

TF-IDF算法

TF-IDF算法是一种用于信息检索和文本挖掘的常用加权技术,其主要思想是:如果某个词或短语在一篇文章中出现的频率高,并且在其他文章中很少出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。它由两部分组成:TF(词频)和IDF(逆向文件频率)。12

TF表示词条在文档中出现的频率,IDF表示词条在整个文档集合中的稀有程度。TF-IDF实际上是TF和IDF的乘积,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。字词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。

具体来说,IDF的具体算法是:IDF(t) = log(语料库中的文档总数 / (含有该词条的文档总数+1 ))。加1是为了防止分母为0,导致结果无法计算。最后,将TF和IDF相乘,就得到了TF-IDF值。

2.hash

通过hash函数计算各个特征向量的hash值,hash值为二进制数01组成的n-bit签名。比如“生活”的hash值Hash(生活)为110101,“没有”的hash值Hash(没有)为“101001”。就这样,字符串就变成了一系列数字。

3.加权

加权步骤是为了突出一些重要的词语,通常通过词频或TF-DF来进行加权处理。

在hash值的基础上,给所有特征向量进行加权,即W = Hash * weight,且遇到1则hash值和权值正相乘,遇到0则hash值和权值负相乘。例如给“生活”的hash值“110101”加权得到:W(生活) = 110101 * 5 = 5 5 -5 5 -5 5,给“没有”的hash值“101001”加权得到:W(没有)=101001 * 2 = 2 -2 2 -2 -2 2,其余特征向量类似此般操作。

4.合并

将上述各个特征向量的加权结果累加,变成只有一个序列串。拿前两个特征向量举例,例如“生活”的“5 5 -5 5 -5 5”和“没有”的“2 -2 2 -2 -2 2”进行累加,得到“5+2, 5-2, -5+2, 5-2, -5-2, 5+2,”,得到“7 3 -3 3 -7 7”。

5.降维

对于n-bit签名的累加结果,如果大于0则置1,否则置0,从而得到该语句的simhash值,最后我们便可以根据不同语句simhash的海明距离来判断它们的相似度。例如把上面计算出来的“9 -9 1 -1 1 9”降维(某位大于0记为1,小于0记为0),得到的01串为:“1 1 0 1 0 1”,从而形成它们的simhash签名。

整个过程的流程图为:
在这里插入图片描述

6:simhash的签名距离计算

对每条文本根据SimHash 算出签名后,再计算两个签名的海明距离(两个二进制异或后1的个数)即可,既两个simhash对应二进制(01串)取值不同的数量称为这两个simhash的海明距离。

举例如下: 10101 和 00110 从第一位开始依次有第一位、第四、第五位不同,则海明距离为3。对于二进制字符串的a和b,海明距离为等于在a XOR b运算结果中1的个数(普遍算法)。

在实际使用过程中,我们可以使用simhash 分块来操作,假设对64位的SimHash,查找海明距离在3以内的所有签名。可以把64位的二进制签名均分成4块,每块16位。根据鸽巢原理(也称抽屉原理,见组合数学),如果两个签名的海明距离在3以内,它们必有一块完全相同。
在此基础上我们可以使用redis和es来进行查询

redis:

  • 我们需要将64位simhash均分为4份,然后每份作为key存储到redis
  • 采用精确匹配的方式查找前16位
  • 找到则拿出来计算与被比较的simahsh距离,小于3则判断为相似(当然具体问题具体分析,这个值可以调整)

es:

  • 将每个文章的64位simhash的值分成四块存入一个数组字段中,创建倒排索引
  • 将需要查询的文章的64位simhash的值分成四块进行terms 查找
  • 只要有一个命中,那么将数据的simhash值取出,进行海明距离计算

Java通过SimHash计算文本内容相似度代码示例

1:导包

首先添加依赖,使用HanLP分词,Jsoup提供正文HTML标签去除服务。

		 <dependency>
            <groupId>com.hankcs</groupId>
            <artifactId>hanlp</artifactId>
            <version>portable-1.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.13.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.2</version>
        </dependency>

2:过滤特殊字符

/**
     * 预处理数据
     *
     * @param textContent
     * @return
     */
    public static String preprocessData(String textContent) {
        if (StringUtils.isBlank(textContent)) {
            return textContent;
        }
        //全角转半角
        textContent = CharUtil.ToDBC(textContent);
        //繁体转换简体
        textContent = HanLP.convertToSimplifiedChinese(textContent);
        //去除各类标签和特殊字符
        textContent = removeTag(textContent);
        return textContent;
    }

    /**
     * 全角转半角字符
     *
     * @param input
     * @return
     */

    public static String ToDBC(String input) {
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] == 12288) {
                //全角空格为12288,半角空格为32
                c[i] = (char) 32;
                continue;
            }
            if (c[i] > 65280 && c[i] < 65375)
            //其他字符半角(33-126)与全角(65281-65374)的对应关系是:均相差65248
            {
                c[i] = (char) (c[i] - 65248);
            }
        }
        return new String(c);
    }

    /**
     * 半角转全角
     *
     * @param input
     * @return
     */

    public static String ToSBC(String input) {
        //半角转全角:
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] == 32) {
                c[i] = (char) 12288;
                continue;
            }
            if (c[i] < 127) {
                c[i] = (char) (c[i] + 65248);
            }
        }
        return new String(c);
    }


    /**
     * 去除标签 和特殊字符
     *
     * @param text
     * @return
     */
    public static String removeTag(String text) {
        if (null == text || text.isEmpty()) {
            return "";
        }
        text = text.replaceAll("[`|+|•|/|<|>|《|》|_|"|·|。|“|”|「|\"|」|:|:|.|。|,|.|;|\\-|?|!|,|;|?|!|\t|\\[|\\]|(|)|{|}|【|】|(|)|||\\|、|]", "");
        text = text.replaceAll("[#|…]", "").replaceAll("&quot &gt", "")
                .replaceAll("\\s+", " ")
                .replaceAll("[^\u4E00-\u9FA5]", "");//去除emoji图像;
        text = text.replaceAll("\\s+", "").replaceAll(" ", "").replaceAll(" ", "");
        text = text.replaceAll("\\s+", "").replaceAll(" +", "").replaceAll("\\u2003", "")
                .replaceAll(" ", "").replaceAll("[\\s*|\t|\r|\n|\r\n|]", "").replaceAll("&nbsp;", "").replaceAll("nbsp", "");
        text = text.replaceAll("[\u007f-\u009f]|\u00ad|[\u0483-\u0489]|[\u0559-\u055a]|\u058a|"
                + "[\u0591-\u05bd]|\u05bf|[\u05c1-\u05c2]|[\u05c4-\u05c7]|[\u0606-\u060a]|[\u063b-\u063f]|\u0674|"
                + "[\u06e5-\u06e6]|\u070f|[\u076e-\u077f]|\u0a51|\u0a75|\u0b44|[\u0b62-\u0b63]|[\u0c62-\u0c63]|"
                + "[\u0ce2-\u0ce3]|[\u0d62-\u0d63]|\u135f|[\u200b-\u200f]|[\u2028-\u202e]|\u2044|\u2071|[\uf701-\uf70e]|"
                + "[\uf710-\uf71a]|\ufb1e|[\ufc5e-\ufc62]|\ufeff|\ufffc", "");
        text = text.replace("0", "").replace("1", "")
                .replace("2", "").replace("3", "").replace("4", "")
                .replace("5", "").replace("6", "").replace("7", "")
                .replace("8", "").replace("9", "").toLowerCase().trim();
        text = text.replace("0", "").replace("1", "")
                .replace("2", "").replace("3", "").replace("4", "")
                .replace("5", "").replace("6", "").replace("7", "")
                .replace("8", "").replace("9", "").toLowerCase().trim();
        return text;
    }

3:计算单个字符的hash向量

 /**
     * 获取一个单词的哈希值
     * 警告:修改该方法会导致计算出的SimHash发生变化。
     *
     * @param word 输入的单词
     * @return 返回哈希
     */
    private static BigInteger getWordHash(String word) {
        if (StringUtils.isBlank(word)) {
            return BIGINT_0;
        }
        char[] sourceArray = word.toCharArray();
        // 经过调优,发现左移位数为11-12左右最优
        // 在哈希词语主要为长度2的中文词时,可以避免高位哈希出现明显偏向
        // 反之,如果左移位数太大,则低位哈希将只和词语最后一个字相关
        BigInteger hash = BigInteger.valueOf(((long) sourceArray[0]) << 12);
        for (char ch : sourceArray) {
            BigInteger chInt = BigInteger.valueOf(ch);
            hash = hash.multiply(BIGINT_1000003).xor(chInt).and(BIGINT_2E64M1);
        }
        hash = hash.xor(BigInteger.valueOf(word.length()));
        return hash;
    }

4:获得一个词语的权重

 /**
     * 获取一个单词的权重。
     * 警告:修改该方法会导致计算出的SimHash发生变化。
     *
     * @param word 输入单词
     * @return 输出权重
     */
    private static int getWordWeight(String word) {
        if (StringUtils.isBlank(word)) {
            return 0;
        }
        int length = word.length();
        if (length == 1) {
            // 只有长度为1的词,哈希后位数不够(40位左右),所以权重必须很低,否则容易导致高位哈希全部为0。
            return 1;
        } else if (word.charAt(0) >= 0x3040) {
            if (length == 2) {
                return 8;
            } else {
                return 16;
            }
        } else {
            if (length == 2) {
                return 2;
            } else {
                return 4;
            }
        }
    }

4:文章分词并计算simhash指纹

 /**
     * 计算一段正文的simHash
     * 警告:修改该方法,修改HanLp分词结果(如新增停用词),会导致计算出的SimHash发生变化。
     *
     * @param text 需要计算的文本
     * @return 返回simHash,64位的0-1字符串。如果文本过短则返回null。
     */
    public static String get(String text) {
        if (text == null) {
            return null;
        }
        text =  preprocessData(text);
        int sumWeight = 0;
        int maxWeight = 0;
        int[] bits = new int[64];
        List<Term> termList = HanLP.segment(text);
        for (Term term : termList) {
            String word = term.word;
            String nature = term.nature.toString();
            // 去除标点符号和停用词,这里可以使用自定义停用词表
            if (nature.startsWith("w") || CoreStopWordDictionary.contains(word)) {
                continue;
            }
            BigInteger wordHash = getWordHash(word);
            int wordWeight = getWordWeight(word);
            if (wordWeight == 0) {
                continue;
            }
            sumWeight += wordWeight;
            if (maxWeight < wordWeight) {
                maxWeight = wordWeight;
            }
            // 逐位将计算好的词哈希乘以权重,记录到保存用的数组上。
            // 如果该位哈希为1,则加上对应的权重,反之减去对应的权重。
            for (int i = 0; i < 64; i++) {
                BigInteger bitMask = BIGINT_1.shiftLeft(63 - i);
                if (wordHash.and(bitMask).signum() != 0) {
                    bits[i] += wordWeight;
                } else {
                    bits[i] -= wordWeight;
                }
            }
        }
        //可以自定义逻辑
        if (3 * maxWeight >= sumWeight || sumWeight < 20) {
            // 文本太短导致哈希不充分,拒绝返回结果(否则可能会有太多碰撞的文档,导致查询性能低下)
            // 暂时定为至少需要凑齐3个大词才允许返回结果
            return null;
        }
 
        // 将保存的位统计结果降维,处理成0/1字符串并返回
        StringBuilder simHashBuilder = new StringBuilder();
        for (int i = 0; i < 64; i++) {
            if (bits[i] > 0) {
                simHashBuilder.append("1");
            } else {
                simHashBuilder.append("0");
            }
        }
        return simHashBuilder.toString();
    }

5:计算两个simhash 的海明距离

 /**
     * 计算两个字符串的汉明距离
     * @param a
     * @param b
     * @return
     */

    public static int hammingDistance(String a, String b) {
        if (a == null || b == null) {
            return 0;
        }
        if (a.length() != b.length()) {
            return -1;
        }
        int disCount = 0;
        for (int i = 0; i < a.length(); i++) {
            if (a.charAt(i) != b.charAt(i)) {
                disCount++;
            }
        }
        return disCount;
    }

完整代码

package com.wkl.kafkademo.simhash;

import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.dictionary.stopword.CoreStopWordDictionary;
import com.hankcs.hanlp.seg.common.Term;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;

import java.math.BigInteger;
import java.util.List;

/**
 * 提供SimHash相关的计算服务
 */
public class SimHashService {

    public static final BigInteger BIGINT_0 = BigInteger.valueOf(0);
    public static final BigInteger BIGINT_1 = BigInteger.valueOf(1);
    public static final BigInteger BIGINT_2 = BigInteger.valueOf(2);
    public static final BigInteger BIGINT_1000003 = BigInteger.valueOf(1000003);
    public static final BigInteger BIGINT_2E64M1 = BIGINT_2.pow(64).subtract(BIGINT_1);

    /**
     * 计算一段正文的simHash
     * 警告:修改该方法,修改HanLp分词结果(如新增停用词),会导致计算出的SimHash发生变化。
     *
     * @param text 需要计算的文本
     * @return 返回simHash,64位的0-1字符串。如果文本过短则返回null。
     */
    public static String get(String text) {
        if (text == null) {
            return null;
        }
        text = preprocessData(text);
        int sumWeight = 0;
        int maxWeight = 0;
        int[] bits = new int[64];
        List<Term> termList = HanLP.segment(text);
        for (Term term : termList) {
            String word = term.word;
            String nature = term.nature.toString();
            // 去除标点符号和停用词,这里可以使用自定义停用词表
            if (nature.startsWith("w") || CoreStopWordDictionary.contains(word)) {
                continue;
            }
            BigInteger wordHash = getWordHash(word);
            int wordWeight = getWordWeight(word);
            if (wordWeight == 0) {
                continue;
            }
            sumWeight += wordWeight;
            if (maxWeight < wordWeight) {
                maxWeight = wordWeight;
            }
            // 逐位将计算好的词哈希乘以权重,记录到保存用的数组上。
            // 如果该位哈希为1,则加上对应的权重,反之减去对应的权重。
            for (int i = 0; i < 64; i++) {
                BigInteger bitMask = BIGINT_1.shiftLeft(63 - i);
                if (wordHash.and(bitMask).signum() != 0) {
                    bits[i] += wordWeight;
                } else {
                    bits[i] -= wordWeight;
                }
            }
        }
//        if (3 * maxWeight >= sumWeight || sumWeight < 20) {
//            // 文本太短导致哈希不充分,拒绝返回结果(否则可能会有太多碰撞的文档,导致查询性能低下)
//            // 暂时定为至少需要凑齐3个大词才允许返回结果
//            return null;
//        }

        // 将保存的位统计结果降维,处理成0/1字符串并返回
        StringBuilder simHashBuilder = new StringBuilder();
        for (int i = 0; i < 64; i++) {
            if (bits[i] > 0) {
                simHashBuilder.append("1");
            } else {
                simHashBuilder.append("0");
            }
        }
        return simHashBuilder.toString();
    }

    /**
     * 预处理数据
     *
     * @param textContent
     * @return
     */
    public static String preprocessData(String textContent) {
        if (StringUtils.isBlank(textContent)) {
            return textContent;
        }
        //全角转半角
        textContent = ToDBC(textContent);
        //繁体转换简体
        textContent = HanLP.convertToSimplifiedChinese(textContent);
        //去除各类标签和特殊字符
        textContent = removeTag(textContent);
        return textContent;
    }

    /**
     * 全角转半角字符
     *
     * @param input
     * @return
     */

    public static String ToDBC(String input) {
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] == 12288) {
                //全角空格为12288,半角空格为32
                c[i] = (char) 32;
                continue;
            }
            if (c[i] > 65280 && c[i] < 65375)
            //其他字符半角(33-126)与全角(65281-65374)的对应关系是:均相差65248
            {
                c[i] = (char) (c[i] - 65248);
            }
        }
        return new String(c);
    }

    /**
     * 半角转全角
     *
     * @param input
     * @return
     */

    public static String ToSBC(String input) {
        //半角转全角:
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] == 32) {
                c[i] = (char) 12288;
                continue;
            }
            if (c[i] < 127) {
                c[i] = (char) (c[i] + 65248);
            }
        }
        return new String(c);
    }


    /**
     * 去除标签 和特殊字符
     *
     * @param text
     * @return
     */
    public static String removeTag(String text) {
        if (null == text || text.isEmpty()) {
            return "";
        }
        text = text.replaceAll("[`|+|•|/|<|>|《|》|_|"|·|。|“|”|「|\"|」|:|:|.|。|,|.|;|\\-|?|!|,|;|?|!|\t|\\[|\\]|(|)|{|}|【|】|(|)|||\\|、|]", "");
        text = text.replaceAll("[#|…]", "").replaceAll("&quot &gt", "")
                .replaceAll("\\s+", " ")
                .replaceAll("[^\u4E00-\u9FA5]", "");//去除emoji图像;
        text = text.replaceAll("\\s+", "").replaceAll(" ", "").replaceAll(" ", "");
        text = text.replaceAll("\\s+", "").replaceAll(" +", "").replaceAll("\\u2003", "")
                .replaceAll(" ", "").replaceAll("[\\s*|\t|\r|\n|\r\n|]", "").replaceAll("&nbsp;", "").replaceAll("nbsp", "");
        text = text.replaceAll("[\u007f-\u009f]|\u00ad|[\u0483-\u0489]|[\u0559-\u055a]|\u058a|"
                + "[\u0591-\u05bd]|\u05bf|[\u05c1-\u05c2]|[\u05c4-\u05c7]|[\u0606-\u060a]|[\u063b-\u063f]|\u0674|"
                + "[\u06e5-\u06e6]|\u070f|[\u076e-\u077f]|\u0a51|\u0a75|\u0b44|[\u0b62-\u0b63]|[\u0c62-\u0c63]|"
                + "[\u0ce2-\u0ce3]|[\u0d62-\u0d63]|\u135f|[\u200b-\u200f]|[\u2028-\u202e]|\u2044|\u2071|[\uf701-\uf70e]|"
                + "[\uf710-\uf71a]|\ufb1e|[\ufc5e-\ufc62]|\ufeff|\ufffc", "");
        text = text.replace("0", "").replace("1", "")
                .replace("2", "").replace("3", "").replace("4", "")
                .replace("5", "").replace("6", "").replace("7", "")
                .replace("8", "").replace("9", "").toLowerCase().trim();
        text = text.replace("0", "").replace("1", "")
                .replace("2", "").replace("3", "").replace("4", "")
                .replace("5", "").replace("6", "").replace("7", "")
                .replace("8", "").replace("9", "").toLowerCase().trim();
        return text;
    }

    /**
     * 获取一个单词的哈希值
     * 警告:修改该方法会导致计算出的SimHash发生变化。
     *
     * @param word 输入的单词
     * @return 返回哈希
     */
    private static BigInteger getWordHash(String word) {
        if (StringUtils.isBlank(word)) {
            return BIGINT_0;
        }
        char[] sourceArray = word.toCharArray();
        // 经过调优,发现左移位数为11-12左右最优
        // 在哈希词语主要为长度2的中文词时,可以避免高位哈希出现明显偏向
        // 反之,如果左移位数太大,则低位哈希将只和词语最后一个字相关
        BigInteger hash = BigInteger.valueOf(((long) sourceArray[0]) << 12);
        for (char ch : sourceArray) {
            BigInteger chInt = BigInteger.valueOf(ch);
            hash = hash.multiply(BIGINT_1000003).xor(chInt).and(BIGINT_2E64M1);
        }
        hash = hash.xor(BigInteger.valueOf(word.length()));
        return hash;
    }

    /**
     * 获取一个单词的权重。
     * 警告:修改该方法会导致计算出的SimHash发生变化。
     *
     * @param word 输入单词
     * @return 输出权重
     */
    private static int getWordWeight(String word) {
        if (StringUtils.isBlank(word)) {
            return 0;
        }
        int length = word.length();
        if (length == 1) {
            // 只有长度为1的词,哈希后位数不够(40位左右),所以权重必须很低,否则容易导致高位哈希全部为0。
            return 1;
        } else if (word.charAt(0) >= 0x3040) {
            if (length == 2) {
                return 8;
            } else {
                return 16;
            }
        } else {
            if (length == 2) {
                return 2;
            } else {
                return 4;
            }
        }
    }
    

    /**
     * 计算两个字符串的汉明距离
     *
     * @param a
     * @param b
     * @return
     */

    public static int hammingDistance(String a, String b) {
        if (a == null || b == null) {
            return -1;
        }
        if (a.length() != b.length()) {
            return -1;
        }
        int disCount = 0;
        for (int i = 0; i < a.length(); i++) {
            if (a.charAt(i) != b.charAt(i)) {
                disCount++;
            }
        }
        return disCount;
    }

    public static void main(String[] args) {
//        String CONTENT1 = "万字人读过连载第九十章一怒冲冠为红颜下一秒就是雷奥的禁区线横穿过人秀扁鹊并不在乎出力对他来说卸车用的力气还没有平时锻炼的大听到这个玩笑夏冬却没有笑反而眼睛一阵酸涩眼泪都快掉下来了永远滴神联盟诞生以来最强的上分机器未尝一败算了想那么多干嘛呢既然有主动送上门的美味佳肴怎么能错过呢于是何言风热烈地回应了起来不过到现在他还不知道小女孩叫什么名字默默听了几分钟何言风的脸色渐渐黑了下来呃怕什么来什么居然真的是苏思青的声音这也是他打算在天道佩恩面前暴露的身份商会会长为钱财战斗的忍者下一瞬间易夏的意识开始浮现出斑斓的幻象所以他还真不知道巴尼会用这办法更没想到莱恩会在自己房间里点蜡烛那你怎么知道兰州拉面出了兰州不好吃了在灵异事件当中什么情况都有可能发生哪怕是自己看到的也不一定就是真的吩咐下去了应对策略之后江明月拍了拍手雷厉风行道好了我们立刻行动起来除开必要的水和食物郝谢尔一家最为珍贵的还是那些动物想要他用沾染隐形荧光水的手触碰书而不被墨竹清洗掉指纹就只有刚才这样的机会两岸的行人都忍不住站在桥上岸边朝着这里打量虽然已经看不清舞蹈者的面目细节但那模糊舞姿依然有一种动人心魄的魔力哎卓越感慨道学习好就是好请假老师都不问理由阿依慕自然明白何言风的意思她点了点头直接开口建议道此时义庄大门前的那只厉鬼正一步一步的朝着公交车的方向走去似乎它正是灵异公交车这一站要接的乘客这海图上所描绘的标记他竟一个都不认识那些似是而非的航线所指向的终点在他印象中本应该是空旷的海面才对那些区域理论上根本没有东西阿布扎克比自然是慷慨的为了完成斯坦福的崛起计划这位俄罗斯大亨非常的慷慨在短短几天时间两人进行了三笔交易肖佩佩目瞪口呆地看着阿依慕她咽了咽口水声音踟蹰道那个姐姐其实你不用这样的他们不敢把我怎么样但他也没有失望毕竟偶遇嘛哪有次次都能成功的虽然只是招收一些帮厨但是要求却不见得简单就夏墨刚来便看见了几个大汉垂头丧气的从厨房里走了出来显然他们的手艺没能满足要求此时此刻仿佛天地之间就只有这一把刀而挡在它面前的自己就如同一只小小的蝼蚁一般威灵顿拿出塔罗牌道明天很适合举行派对黄蓉闻言斩钉截铁道胆敢有违抗此令者杀无赦依次交加一共叠了五层韩元才停下来将叠好的草鞋底展示给直播间里面的观众看没事没事我们也算是这里的半个主人小楼中窗台边缘再次出现了一道瘦弱的身影这让上官家上上下下松了口气算是还是不要了日向藏心中否决这一想法自己还是无法对身边的人下手至少在那些忍兽身上获得足够的数据前只要我知道对方的名字就能预测到他未来一分钟内发生的一切而和往常不同的是这次出现了实体没一会儿同事也看完了何平的信毛春华问道你觉得怎么样另一边是一望无尽的蓝色大海仿佛藏着无尽的秘密这句话有着调侃的意思但也具有一定的真理性音和鬼灯满月间的战斗结束了良久人群才渐渐散去狄仁杰摸着那一枚棋子想起了盗贼退走时预先布置好的魔道猫腻眼眸微微一沉继续道但也因此让我更加确定了他们是有备而来想必只有秘阁内的宝相花机关让他们花费了不少心思所以才耽误了时间被我察觉如果不果断地摘掉耳返他很可能会被里面播放出来的歌曲给带偏了天南域正好拿战舰折损作为不剿灭黑岭山脉的借口你仙炼宗是造战舰的给天南域送过去几艘看天南域到时候还有什么说头这不是猜测她适才真切的感受到了她的力量在里莱面前不堪一击杰米是能力毋容置疑射手榜第一金靴的有力候补这些足以证明他的能力我也不知道啊都十几年没离开沙漠了柒染沐浴在紫色的光芒之中整个人多了一丝神圣的气息甘宁回夏口一边集结手下部队一边散布流言说江东众将不过如此凌操号称大将竟被一箭射死江东鼠辈此生止步江夏否则团藏大人也不会将大部分的精力都放在培养取根身上了除了落后的比较多抱着得之我幸失之我命的态度之外就是何言风和阿依慕对于这个总决赛的胜负心本就没有多强他们能适应只要我们依靠不同的梯队稍稍改变比赛时间即可我们培养的是球员的比赛能力注重的是技战术的吸收以及理解个人能力的开发以及球员间的配合从进入军校之前军校就已经开始培养他们的团队意识蔡瑁毕竟是刘表的小舅子追随刘表这么多年又是蔡氏家族的族长外面客厅已经传来电视的声音夏茴这个家伙用作息再次证明了她不是现代人不用上班没有工作依然每天起这么早华夏美食确实诱人我曾经去过好几次华夏那是一个很繁华和令人向往的国度有美味的食物漂亮的姑娘良好的治安和完善的基础建设双枪齐发能量弹疯狂朝着六臂傀儡倾泻而下这一点还是艾米发现的她将单只满天星的花朵放在水瓶中淡淡的荧光能够照亮两三米范围的距离正好作为照明之用走出火车站马克范迪诺已经等候在这里要是林远是个普通的文科天才校方才懒的去管他听了何言风的决定黄兴云老师再次开口恭维道哈哈哈何老师的创作能力是有目共睹的到时候肯定很受学生们的欢迎标签相关最新章节分钟更新时间";
//        String CONTENT2 = "上善若水水善利万物而不争处众人之所恶故几于道居善地心善渊与善仁言善信政善治事善能动善时夫唯不争故无尤老子你每天喝的可能是死水这是一沟绝望的死水清风吹不起半点漪沦每当路过臭水沟小编都会想起闻一多这首死水有句话说人七天不吃饭可以但三天不喝水就会死没有水没有人能够活下去现在的水安全令我们堪忧条件好一点的家庭买净水器或者购买桶装水以求获得优质的水来源可是这些看起来清澈透明的纯净水也可能不是活水由浊变清由死变活图为成都活水公园在成都的朋友一定知道活水公园备受国内外关注甚至被搬进了世博园它最吸引人的地方是有污水处理的神奇功效能将死水变为活水成都活水公园经过多种净化过程将原来被上游污染源的河水净化后重新流入府河植物种植塘床的鱼鳞式形式模仿黄龙寺钙化盘造型呈自然生长状经过多级过滤吸收转化为较清洁的水质每天活水公园的流量可达立方米向人们演示被污染的水在自然界由浊变清由死变活的过程对人们环境生态观念产生深远的影响图为广州心源自然学园活水还有驱蚊的功能深圳育德华德福幼儿园也引入了该套活水系统这里成了孩子们喜欢的戏水场所听知情人士透露夏季蚊子多的季节在活水的地方竟然没有蚊子活水具有神奇的滋养疗愈作用有关活水的奇迹一天天在创造中乌克兰四岁的唐氏综合征女孩之前只喝山泉水喝了活水后其他水都不喝了自闭症孩子借由活水竟然开口讲话了孩子们一回到家第一时间是坐到家用活水旁边聆听水声活水充满了神奇的魔力图为成都活水公园水能听水能看水知道生命的答案水知道答案细心的你一定会发现以上所有图片中活水的特别之处没错活水循环系统的核心是与太极图近似的字形流淌形态这是水在自然中的流动现象能最大效能的激活水中溶解的氧气对水的生命能量净化激活没有任何其他形状能够达到这种效果活水产品展示你也很容易在生物动力农场与华德福学校发现这个像一连串嵴椎骨一般碟形的流水装置这是人智学系统中独有的它的英文名字为不仅能将污染的水重新净化恢复生命能量与活力还能对人类的身心产生不可思议的疗愈力被运用在医院游泳池面包房室内设计等世界各地的华德福学校都运用了的技术来建造儿童的游戏场地为儿童提供一个健康有生命力的环境图为广州心源自然学园福乐丰系列活水产品于上世纪年代鲁道夫史丹纳博士的弟子数学家他发扬了鲁道夫史丹纳博士关于水的哲学灵性观点进而深入系统研究发现水的流动路径并不是一条直线而是带着一种韵律节奏蜿蜒而行国际水研究中心英国爱默生学院经过几十年的研究而创造发明一种独特的流体表面用射影几何设计而成创造出字形的水流在方寸之间创造出山涧水流的活力给水带来更多品质提升重新建构水分子间结构恢复水的能量提高含氧量如同用大自然自己的方式提升水的品质的研究成果由他的弟子和传人华德福人智学专家约翰威尔克斯所继承并具体化再由约翰威尔克斯的学生将之发扬光大先生现任专门从事研究的英国水基金会董事全球业务的负责人曾经担任年华德福老师年代开始从事人智学活水循环的研究并不断集成吸收现代科学对水研究的最新理解致力于向全球介绍水的生命法则呼吁福斯关注水污染治理与水资源保护并推广的环保与疗愈技术曾在澳大利亚加拿大日本中国台湾等地开展关于水和的讲座咨询是道法自然的产物源自于他们在人智学的启迪下对水之奥秘的洞察约翰威尔克斯和伙伴们用心观察流水始终保持某种律动与漩涡的状态从而觉悟到这是水为了自我净化与自我维护而发展的必由之路水本身就是一位富有活力的雕塑家也是所有生命形态的绘图师因此不仅能将污染的水重新净化恢复生命能量与活力还能对人类的身心产生不可思议的疗愈力世界各地的华德福学校都运用了的技术来建造儿童的游戏场地为儿童提供一个健康有生命力的环境由于富含大自然水流的疗愈力还可运用于人智学医院在公园在院子里同样可以引入让大自然水流滋养着我们水是最佳的载体透过漩涡可将宇宙的能量注入水中含氧量也增加负离子也散发出来活水功能细分说明字在不同地方代表的含义分别华德福代表双纽线优律诗美代表交织与流动在表现思考的时候通常会用直线表现意志的时候通常用圆或这些比较简单的曲线而表现情感的时候就会用到这样比较复杂交错的曲线韵律按摩代表活化身体让身体流动更加顺畅人智医学身体血液运动是以字方式流动在中国舞龙动作有原地八字舞龙行进八字舞龙起伏八字舞龙在民间字代表发代表旺代表吉祥水是雕塑者同时也是塑形者蜿蜒的河流穿梭大好江山风水八字代表发代表旺跑动的字更像一条舞动的龙中国古话遇水则发以水为财家中放置让水财一直在家中流动顺畅景观我不是最美的但是我是最有内涵的声音泉水叮咚平缓急流急促落差大的水流声音放在不同场景都给不同的冲击声音带有水本身节奏发出水本身的声音不因外界而影响带有节奏的水声能深入人心与身体水流动结合调节人的气息空气空字运动水流在不断运动可以产生负离子充满运动能量的负离子会让空气更加有活力水中充满氧气充满正能量喝有活力的水有能量的水进入身体会让身体的循环血液的循环更加有力量小分子团水的渗透力强溶解力好乳化力强我们明显感觉排泄量增加容易带出身体内的垃圾毒素活水重在活山泉清甜因其活大森林地下水树木根深枝繁叶茂空气含氧量高山间小溪涓涓细流溪中鱼儿鱼肥肉嫩皆因水中富含氧气更因水团分子水容易被动植物吸收特别功能活水产品模拟大自然生态水横向字运动可让酸性水运动后变碱性水让水分子与水分子之间更加紧密水中充满氧气充满正能量水挥发到空气中能改善空气质量家中小花小草小小宠物因活水更加有生命力水声具有调节呼吸安静心神的作用让自然的流水声唤醒身心同时整个活水产品以水的流动形态构成太极中的无限会在家里形成一个能量磁场如心脏般跳动的韵律声音轻松改善家庭能量场给家庭带来更多的美与活力可容纳升水横向字运动小时即可使用亦可拿来烧水煮饭水泵耗电量可忽略不计搬动轻松可随意摆放客厅书房和卧室在家便可享受大自然氧吧享受大自然福乐丰活水产品不仅仅是一个水处理产品更是一个大自然的能量师与身心灵的疗愈师将纯净大自然带入家庭搬一片大自然进家邀一片大自然进家图为用户分享备注说明活水产品不是传统的净水产品没有过滤功能但可让水中的物质沉淀下来如果担心水龙头的水不够干净可在水龙头中加过滤后取水放于活水器中或者经活水器处理再放进过滤器中过滤后使用有四种颜色深蓝浅蓝摩卡白金色每种颜色水流的方式都会有一些区别都拥有自己的特色从窑洞运出来后不会进行二次修色保留着自然本色分体深蓝那水如此强劲有力是谁在随那流水不断地向我送来无言的支持与慰藉分体浅蓝蓝色表面的水带着羞涩的气质优雅而又宁静分体摩卡白在流水的映衬下那水下的瓷好似在舞动伴着那节奏像扇动翅膀的鸟又像滑翔着的鱼分体金色年全球首推颜色流水在金色的表面流过可是又好似漂浮着根本不曾触到那表面仿佛只在空中掠过竟此般神奇福乐丰活水产品常见问题向上滑动查看内容福乐丰活水需要接水龙头吗不需要直接倒水进去福乐丰活水需要通电吗需要通电有一个水泵福乐丰活水需要安装过滤装置吗福乐丰活水产品不具备过滤功能如果水杂质含量多需要先过滤后在活化福乐丰活水用在生活哪些方面可以煮饭菜烧水泡茶浇花草喂养小动物活化后如何取用直接用竹管或者竹勺将水取出来福乐丰清洗时间要求没有固定要求视具体情况来定福乐丰活水原理未经处理的水通常以水团大分子形式存在没有活力口感差这也是大自然中的山泉水喝起来更清甜的原因经处理后的水重新呈现出生命力以活跃的小分子存在并且富含氧分子如同雨后山林中的负离子一样活水器如同自然的搬运工将山泉水带回家庭水团小分子是什么水在天然的情况下都是小分子团的经典的例子就是冰山雪水小分子团的渗透力好溶解力强所以你在山上洗澡不需要肥皂而且全世界的长寿村都在山上不在山下因为这些小分子团的水能够清除身体的毒素世界卫生组织的研究人员发现这些长寿村居民的排便没有恶臭而且这些研究人员到了长寿村一个星期后发现自己的排便也没有恶臭福乐丰活水功能福乐丰活水产品模拟大自然生态将大自然的流水音律天籁搬到家庭中来水声具有调节呼吸安静心神的作用让自然的流水声唤醒身心同时整个活水产品以水的流动形态构成太极中的无限会在家里形成一个能量磁场给家庭带来更多的美与活力水需要运动多长时间才能够达到活化效果正常运动个小时就可以达到最佳效果活化后水的氧气保留时间多长未活化的水接到杯子里面马上就变成死水没有活力没有氧气含量而活化后的水未煮开可以氧气保留天上述福乐丰家用陶瓷活水产品在欧洲乃至全球各地已安装上万套以下为我们收集到的客户对产品的使用体验反馈它喷涌如天籁般的声音令我陶醉美国感谢它给我的生命带来的平和与宁静泰国在活水中感受到那么多的不一样这令人感到神奇太棒了英国陶瓷活水产品很容易的在家里装好它纯净的声音是如此的美丽与神奇新西兰我们感受了活水安装后带来水的改进它提升了家庭的能量与和谐这是能量的转变器澳洲活水设计紧凑易于安装它字形的流动形态与舒缓的旋转韵律带来美妙的视觉澳洲福乐丰活水产品对改善家庭的风水有很好的作用某中医传承人中国以上信息收集学卓翻译学卓特别致谢本次支持平台海淘天使您可识别以下二维码关注后咨询相关事宜海淘天使有爱的跨境教育商城提供人本教育书籍华德福艺术用品有机活力农耕产品等高品质教育与生活类产品的海内外直购点击阅读原文进入团购页面特享团购优惠原创文章作者手帕网如若转载请注明出处";
        String CONTENT1 = "上善若水水善利万物而不争这是一段测试Simhash的文字";
        String CONTENT2 = "上善若水水善利万物而不争这是一段测试Simhash的话";

        String simHashStr1 = get(CONTENT1);
        String simHashStr2 = get(CONTENT2);
        System.out.println("simHashStr1:" + simHashStr1);
        System.out.println("simHashStr2:" + simHashStr2);

        int hammingDistance = hammingDistance(simHashStr1, simHashStr2);

        System.out.println(hammingDistance);
    }
}

优化使用工具类-增加停用词和自定义权重

package com.wkl.kafkademo.simhash;

import com.hankcs.hanlp.HanLP;
import com.hankcs.hanlp.seg.common.Term;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.util.*;
import java.util.regex.Pattern;

public class SimHashUtil {

    private static Logger log = LoggerFactory.getLogger(SimHashUtil.class);

    // 停用词
    private static Set<String> stopWordsSet = new HashSet<>();
    // 词频权重
    private static Map<String, Double> idfMap = new HashMap<String, Double>();

    // 平均idf
    private static double idfAverage = loadIdfDict(idfMap);


    public static final int HASH_BITS = 64;
    public static final BigInteger FNV_64_INIT = new BigInteger("14695981039346656037");
    public static final BigInteger FNV_64_PRIME = new BigInteger("1099511628211");
    public static final BigInteger MASK_64 = BigInteger.ONE.shiftLeft(HASH_BITS).subtract(BigInteger.ONE);

    static {
        // 加载停用词
        loadStopWords();
    }

    // 加载额外停用词
    static Pattern pattern = Pattern.compile("([0-9]*)|([0-9]*[年|月|日])");

    /**
     * 判断word是否无效,
     * 目前处理数据为:数字;*年;*月;*日
     * @param word
     * @return true:无效
     */
    private static boolean isInvalid(String word) {
        if (word.length() < 2) {
            return true;
        }
        return pattern.matcher(word).matches();

    }

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

    /**
     * 获取 字符串的64位的 simHash 值
     * @param textContent
     * @return
     */
    public static String getSimHashStr(String textContent) {
        textContent = preprocessData(textContent);
        List<Term> segment = HanLP.segment(textContent);
        Map<String, Integer> wordMap = new HashMap<>();
        Map<String, Double> tfidfMap = new HashMap<>();
        if (CollectionUtils.isEmpty(stopWordsSet)) {
            loadStopWords();
        }
        if (CollectionUtils.isNotEmpty(segment)) {
            for (Term term : segment) {
                String word = term.word.replace(" ", "");
                boolean contains = stopWordsSet.contains(term.word);
                if (contains || isInvalid(word)) {
                    continue;
                }
                if (wordMap.containsKey(word)) {
                    wordMap.put(word, wordMap.get(word) + 1);
                } else {
                    wordMap.put(word, 1);
                }
            }
        }
        wordMap.forEach((k, v) -> {
            if (idfMap.containsKey(k)) {
                double idf = v * idfMap.get(k);
                tfidfMap.put(k, idf);
            } else {
                double idf = v * idfAverage;
                tfidfMap.put(k, idf);
            }
        });

        return analysisSimHash(tfidfMap);
    }


    /**
     * 预处理数据
     * @param textContent
     * @return
     */
    public static String preprocessData(String textContent) {
        if (StringUtils.isBlank(textContent)) {
            return textContent;
        }
        //全角转半角
        textContent = ToDBC(textContent);
        //繁体转换简体
        textContent = HanLP.convertToSimplifiedChinese(textContent);
        //去除各类标签和特殊字符
        textContent = removeTag(textContent);
        return textContent;
    }

    /**
     * 全角转半角字符
     *
     * @param input
     * @return
     */

    public static String ToDBC(String input) {
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] == 12288) {
                //全角空格为12288,半角空格为32
                c[i] = (char) 32;
                continue;
            }
            if (c[i] > 65280 && c[i] < 65375)
            //其他字符半角(33-126)与全角(65281-65374)的对应关系是:均相差65248
            {
                c[i] = (char) (c[i] - 65248);
            }
        }
        return new String(c);
    }

    /**
     * 半角转全角
     *
     * @param input
     * @return
     */

    public static String ToSBC(String input) {
        //半角转全角:
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) {
            if (c[i] == 32) {
                c[i] = (char) 12288;
                continue;
            }
            if (c[i] < 127) {
                c[i] = (char) (c[i] + 65248);
            }
        }
        return new String(c);
    }


    /**
     * 去除标签 和特殊字符
     *
     * @param text
     * @return
     */
    public static String removeTag(String text) {
        if (null == text || text.isEmpty()) {
            return "";
        }
        text = text.replaceAll("[`|+|•|/|<|>|《|》|_|"|·|。|“|”|「|\"|」|:|:|.|。|,|.|;|\\-|?|!|,|;|?|!|\t|\\[|\\]|(|)|{|}|【|】|(|)|||\\|、|]", "");
        text = text.replaceAll("[#|…]", "").replaceAll("&quot &gt", "")
                .replaceAll("\\s+", " ")
                .replaceAll("[^\u4E00-\u9FA5]", "");//去除emoji图像;
        text = text.replaceAll("\\s+", "").replaceAll(" ", "").replaceAll(" ", "");
        text = text.replaceAll("\\s+", "").replaceAll(" +", "").replaceAll("\\u2003", "")
                .replaceAll(" ", "").replaceAll("[\\s*|\t|\r|\n|\r\n|]", "").replaceAll("&nbsp;", "").replaceAll("nbsp", "");
        text = text.replaceAll("[\u007f-\u009f]|\u00ad|[\u0483-\u0489]|[\u0559-\u055a]|\u058a|"
                + "[\u0591-\u05bd]|\u05bf|[\u05c1-\u05c2]|[\u05c4-\u05c7]|[\u0606-\u060a]|[\u063b-\u063f]|\u0674|"
                + "[\u06e5-\u06e6]|\u070f|[\u076e-\u077f]|\u0a51|\u0a75|\u0b44|[\u0b62-\u0b63]|[\u0c62-\u0c63]|"
                + "[\u0ce2-\u0ce3]|[\u0d62-\u0d63]|\u135f|[\u200b-\u200f]|[\u2028-\u202e]|\u2044|\u2071|[\uf701-\uf70e]|"
                + "[\uf710-\uf71a]|\ufb1e|[\ufc5e-\ufc62]|\ufeff|\ufffc", "");
        text = text.replace("0", "").replace("1", "")
                .replace("2", "").replace("3", "").replace("4", "")
                .replace("5", "").replace("6", "").replace("7", "")
                .replace("8", "").replace("9", "").toLowerCase().trim();
        text = text.replace("0", "").replace("1", "")
                .replace("2", "").replace("3", "").replace("4", "")
                .replace("5", "").replace("6", "").replace("7", "")
                .replace("8", "").replace("9", "").toLowerCase().trim();
        return text;
    }

    /**
     * 加载 idf 值
     * @param idfMap
     * @return
     */
    private static double loadIdfDict(Map<String, Double> idfMap) {
        InputStreamReader in;
        long st1 = System.currentTimeMillis();
        double idf = 0.0;
        double idfSum = 0.0;
        int lineno = 0;
        String[] arrStrings = null;
        String line = null;
        try {
            in = new InputStreamReader(new FileInputStream("data/idfold.utf8"), "UTF-8");
            BufferedReader bf = new BufferedReader(in);
            while ((line = bf.readLine()) != null) {
                if (line.isEmpty()) {
                    continue;
                }
                arrStrings = line.split(" ");
                if (arrStrings.length != 2) {
                    continue;
                }
                idf = Double.valueOf(arrStrings[1]);
                idfMap.put(arrStrings[0], idf);
                idfSum += idf;
                lineno++;
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
            log.error("数据格式错误:" + e.getMessage());

        } catch (IOException e) {

            e.printStackTrace();
            log.error("IO错误:" + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("读取不到加载idf语料词典: " + e.toString());
        }
        long st2 = System.currentTimeMillis();
        log.info("加载idf语料词典消耗时间: " + (st2 - st1) + "ms");
        return idfSum / lineno;
    }


    //加载停用词
    public static void loadStopWords() {
        InputStreamReader in;
        long st1 = System.currentTimeMillis();
        String line = null;
        try {
            in = new InputStreamReader(new FileInputStream("data/stopword.dic"), "UTF-8");
            BufferedReader bf = new BufferedReader(in);
            while ((line = bf.readLine()) != null) {
                if (line.isEmpty()) {
                    continue;
                }
                stopWordsSet.add(line);
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
            log.error("数据格式错误:" + e.getMessage());
        } catch (IOException e) {
            e.printStackTrace();
            log.error("IO错误:" + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
            log.error("读取不到文件: " + e.toString());
        }
        long st2 = System.currentTimeMillis();
        log.info("加载 stopword消耗时间: " + (st2 - st1) + "ms");
    }

    /**
     * 根据所有 词频 和权重获取一个 64位的hash 值
     * @param wordInfos
     * @return
     */
    private static String analysisSimHash(Map<String, Double> wordInfos) {
        double[] featureVector = new double[HASH_BITS];
        Set<String> words = wordInfos.keySet();
        for (String word : words) {
            BigInteger wordhash = fnv1aHash64(word);
            //获取每一位的hash值是0还是1,使用与该位的1与的操作,节约时间
            for (int i = 0; i < HASH_BITS; i++) {
                BigInteger bitmask = BigInteger.ONE.shiftLeft(HASH_BITS - i - 1);
                if (wordhash.and(bitmask).signum() != 0) {
                    featureVector[i] += wordInfos.get(word);
                } else {
                    featureVector[i] -= wordInfos.get(word);
                }
            }
        }
        StringBuffer hashBuffer = new StringBuffer();
        for (int i = 0; i < HASH_BITS; i++) {
            if (featureVector[i] >= 0) {
                hashBuffer.append("1");
            } else {
                hashBuffer.append("0");
            }
        }
        return hashBuffer.toString();
    }

    /**
     * 计算两个字符串的汉明距离
     *
     * @param a
     * @param b
     * @return
     */

    public static int hammingDistance(String a, String b) {
        if (a == null || b == null) {
            return 0;
        }
        if (a.length() != b.length()) {
            return -1;
        }
        int disCount = 0;
        for (int i = 0; i < a.length(); i++) {
            if (a.charAt(i) != b.charAt(i)) {
                disCount++;
            }
        }
        return disCount;
    }


    public static void main(String[] args) {
        String CONTENT = "万字人读过连载第九十章一怒冲冠为红颜下一秒就是雷奥的禁区线横穿过人秀扁鹊并不在乎出力对他来说卸车用的力气还没有平时锻炼的大听到这个玩笑夏冬却没有笑反而眼睛一阵酸涩眼泪都快掉下来了永远滴神联盟诞生以来最强的上分机器未尝一败算了想那么多干嘛呢既然有主动送上门的美味佳肴怎么能错过呢于是何言风热烈地回应了起来不过到现在他还不知道小女孩叫什么名字默默听了几分钟何言风的脸色渐渐黑了下来呃怕什么来什么居然真的是苏思青的声音这也是他打算在天道佩恩面前暴露的身份商会会长为钱财战斗的忍者下一瞬间易夏的意识开始浮现出斑斓的幻象所以他还真不知道巴尼会用这办法更没想到莱恩会在自己房间里点蜡烛那你怎么知道兰州拉面出了兰州不好吃了在灵异事件当中什么情况都有可能发生哪怕是自己看到的也不一定就是真的吩咐下去了应对策略之后江明月拍了拍手雷厉风行道好了我们立刻行动起来除开必要的水和食物郝谢尔一家最为珍贵的还是那些动物想要他用沾染隐形荧光水的手触碰书而不被墨竹清洗掉指纹就只有刚才这样的机会两岸的行人都忍不住站在桥上岸边朝着这里打量虽然已经看不清舞蹈者的面目细节但那模糊舞姿依然有一种动人心魄的魔力哎卓越感慨道学习好就是好请假老师都不问理由阿依慕自然明白何言风的意思她点了点头直接开口建议道此时义庄大门前的那只厉鬼正一步一步的朝着公交车的方向走去似乎它正是灵异公交车这一站要接的乘客这海图上所描绘的标记他竟一个都不认识那些似是而非的航线所指向的终点在他印象中本应该是空旷的海面才对那些区域理论上根本没有东西阿布扎克比自然是慷慨的为了完成斯坦福的崛起计划这位俄罗斯大亨非常的慷慨在短短几天时间两人进行了三笔交易肖佩佩目瞪口呆地看着阿依慕她咽了咽口水声音踟蹰道那个姐姐其实你不用这样的他们不敢把我怎么样但他也没有失望毕竟偶遇嘛哪有次次都能成功的虽然只是招收一些帮厨但是要求却不见得简单就夏墨刚来便看见了几个大汉垂头丧气的从厨房里走了出来显然他们的手艺没能满足要求此时此刻仿佛天地之间就只有这一把刀而挡在它面前的自己就如同一只小小的蝼蚁一般威灵顿拿出塔罗牌道明天很适合举行派对黄蓉闻言斩钉截铁道胆敢有违抗此令者杀无赦依次交加一共叠了五层韩元才停下来将叠好的草鞋底展示给直播间里面的观众看没事没事我们也算是这里的半个主人小楼中窗台边缘再次出现了一道瘦弱的身影这让上官家上上下下松了口气算是还是不要了日向藏心中否决这一想法自己还是无法对身边的人下手至少在那些忍兽身上获得足够的数据前只要我知道对方的名字就能预测到他未来一分钟内发生的一切而和往常不同的是这次出现了实体没一会儿同事也看完了何平的信毛春华问道你觉得怎么样另一边是一望无尽的蓝色大海仿佛藏着无尽的秘密这句话有着调侃的意思但也具有一定的真理性音和鬼灯满月间的战斗结束了良久人群才渐渐散去狄仁杰摸着那一枚棋子想起了盗贼退走时预先布置好的魔道猫腻眼眸微微一沉继续道但也因此让我更加确定了他们是有备而来想必只有秘阁内的宝相花机关让他们花费了不少心思所以才耽误了时间被我察觉如果不果断地摘掉耳返他很可能会被里面播放出来的歌曲给带偏了天南域正好拿战舰折损作为不剿灭黑岭山脉的借口你仙炼宗是造战舰的给天南域送过去几艘看天南域到时候还有什么说头这不是猜测她适才真切的感受到了她的力量在里莱面前不堪一击杰米是能力毋容置疑射手榜第一金靴的有力候补这些足以证明他的能力我也不知道啊都十几年没离开沙漠了柒染沐浴在紫色的光芒之中整个人多了一丝神圣的气息甘宁回夏口一边集结手下部队一边散布流言说江东众将不过如此凌操号称大将竟被一箭射死江东鼠辈此生止步江夏否则团藏大人也不会将大部分的精力都放在培养取根身上了除了落后的比较多抱着得之我幸失之我命的态度之外就是何言风和阿依慕对于这个总决赛的胜负心本就没有多强他们能适应只要我们依靠不同的梯队稍稍改变比赛时间即可我们培养的是球员的比赛能力注重的是技战术的吸收以及理解个人能力的开发以及球员间的配合从进入军校之前军校就已经开始培养他们的团队意识蔡瑁毕竟是刘表的小舅子追随刘表这么多年又是蔡氏家族的族长外面客厅已经传来电视的声音夏茴这个家伙用作息再次证明了她不是现代人不用上班没有工作依然每天起这么早华夏美食确实诱人我曾经去过好几次华夏那是一个很繁华和令人向往的国度有美味的食物漂亮的姑娘良好的治安和完善的基础建设双枪齐发能量弹疯狂朝着六臂傀儡倾泻而下这一点还是艾米发现的她将单只满天星的花朵放在水瓶中淡淡的荧光能够照亮两三米范围的距离正好作为照明之用走出火车站马克范迪诺已经等候在这里要是林远是个普通的文科天才校方才懒的去管他听了何言风的决定黄兴云老师再次开口恭维道哈哈哈何老师的创作能力是有目共睹的到时候肯定很受学生们的欢迎标签相关最新章节分钟更新时间";
        String CONTENT2 = "上善若水水善利万物而不争处众人之所恶故几于道居善地心善渊与善仁言善信政善治事善能动善时夫唯不争故无尤老子你每天喝的可能是死水这是一沟绝望的死水清风吹不起半点漪沦每当路过臭水沟小编都会想起闻一多这首死水有句话说人七天不吃饭可以但三天不喝水就会死没有水没有人能够活下去现在的水安全令我们堪忧条件好一点的家庭买净水器或者购买桶装水以求获得优质的水来源可是这些看起来清澈透明的纯净水也可能不是活水由浊变清由死变活图为成都活水公园在成都的朋友一定知道活水公园备受国内外关注甚至被搬进了世博园它最吸引人的地方是有污水处理的神奇功效能将死水变为活水成都活水公园经过多种净化过程将原来被上游污染源的河水净化后重新流入府河植物种植塘床的鱼鳞式形式模仿黄龙寺钙化盘造型呈自然生长状经过多级过滤吸收转化为较清洁的水质每天活水公园的流量可达立方米向人们演示被污染的水在自然界由浊变清由死变活的过程对人们环境生态观念产生深远的影响图为广州心源自然学园活水还有驱蚊的功能深圳育德华德福幼儿园也引入了该套活水系统这里成了孩子们喜欢的戏水场所听知情人士透露夏季蚊子多的季节在活水的地方竟然没有蚊子活水具有神奇的滋养疗愈作用有关活水的奇迹一天天在创造中乌克兰四岁的唐氏综合征女孩之前只喝山泉水喝了活水后其他水都不喝了自闭症孩子借由活水竟然开口讲话了孩子们一回到家第一时间是坐到家用活水旁边聆听水声活水充满了神奇的魔力图为成都活水公园水能听水能看水知道生命的答案水知道答案细心的你一定会发现以上所有图片中活水的特别之处没错活水循环系统的核心是与太极图近似的字形流淌形态这是水在自然中的流动现象能最大效能的激活水中溶解的氧气对水的生命能量净化激活没有任何其他形状能够达到这种效果活水产品展示你也很容易在生物动力农场与华德福学校发现这个像一连串嵴椎骨一般碟形的流水装置这是人智学系统中独有的它的英文名字为不仅能将污染的水重新净化恢复生命能量与活力还能对人类的身心产生不可思议的疗愈力被运用在医院游泳池面包房室内设计等世界各地的华德福学校都运用了的技术来建造儿童的游戏场地为儿童提供一个健康有生命力的环境图为广州心源自然学园福乐丰系列活水产品于上世纪年代鲁道夫史丹纳博士的弟子数学家他发扬了鲁道夫史丹纳博士关于水的哲学灵性观点进而深入系统研究发现水的流动路径并不是一条直线而是带着一种韵律节奏蜿蜒而行国际水研究中心英国爱默生学院经过几十年的研究而创造发明一种独特的流体表面用射影几何设计而成创造出字形的水流在方寸之间创造出山涧水流的活力给水带来更多品质提升重新建构水分子间结构恢复水的能量提高含氧量如同用大自然自己的方式提升水的品质的研究成果由他的弟子和传人华德福人智学专家约翰威尔克斯所继承并具体化再由约翰威尔克斯的学生将之发扬光大先生现任专门从事研究的英国水基金会董事全球业务的负责人曾经担任年华德福老师年代开始从事人智学活水循环的研究并不断集成吸收现代科学对水研究的最新理解致力于向全球介绍水的生命法则呼吁福斯关注水污染治理与水资源保护并推广的环保与疗愈技术曾在澳大利亚加拿大日本中国台湾等地开展关于水和的讲座咨询是道法自然的产物源自于他们在人智学的启迪下对水之奥秘的洞察约翰威尔克斯和伙伴们用心观察流水始终保持某种律动与漩涡的状态从而觉悟到这是水为了自我净化与自我维护而发展的必由之路水本身就是一位富有活力的雕塑家也是所有生命形态的绘图师因此不仅能将污染的水重新净化恢复生命能量与活力还能对人类的身心产生不可思议的疗愈力世界各地的华德福学校都运用了的技术来建造儿童的游戏场地为儿童提供一个健康有生命力的环境由于富含大自然水流的疗愈力还可运用于人智学医院在公园在院子里同样可以引入让大自然水流滋养着我们水是最佳的载体透过漩涡可将宇宙的能量注入水中含氧量也增加负离子也散发出来活水功能细分说明字在不同地方代表的含义分别华德福代表双纽线优律诗美代表交织与流动在表现思考的时候通常会用直线表现意志的时候通常用圆或这些比较简单的曲线而表现情感的时候就会用到这样比较复杂交错的曲线韵律按摩代表活化身体让身体流动更加顺畅人智医学身体血液运动是以字方式流动在中国舞龙动作有原地八字舞龙行进八字舞龙起伏八字舞龙在民间字代表发代表旺代表吉祥水是雕塑者同时也是塑形者蜿蜒的河流穿梭大好江山风水八字代表发代表旺跑动的字更像一条舞动的龙中国古话遇水则发以水为财家中放置让水财一直在家中流动顺畅景观我不是最美的但是我是最有内涵的声音泉水叮咚平缓急流急促落差大的水流声音放在不同场景都给不同的冲击声音带有水本身节奏发出水本身的声音不因外界而影响带有节奏的水声能深入人心与身体水流动结合调节人的气息空气空字运动水流在不断运动可以产生负离子充满运动能量的负离子会让空气更加有活力水中充满氧气充满正能量喝有活力的水有能量的水进入身体会让身体的循环血液的循环更加有力量小分子团水的渗透力强溶解力好乳化力强我们明显感觉排泄量增加容易带出身体内的垃圾毒素活水重在活山泉清甜因其活大森林地下水树木根深枝繁叶茂空气含氧量高山间小溪涓涓细流溪中鱼儿鱼肥肉嫩皆因水中富含氧气更因水团分子水容易被动植物吸收特别功能活水产品模拟大自然生态水横向字运动可让酸性水运动后变碱性水让水分子与水分子之间更加紧密水中充满氧气充满正能量水挥发到空气中能改善空气质量家中小花小草小小宠物因活水更加有生命力水声具有调节呼吸安静心神的作用让自然的流水声唤醒身心同时整个活水产品以水的流动形态构成太极中的无限会在家里形成一个能量磁场如心脏般跳动的韵律声音轻松改善家庭能量场给家庭带来更多的美与活力可容纳升水横向字运动小时即可使用亦可拿来烧水煮饭水泵耗电量可忽略不计搬动轻松可随意摆放客厅书房和卧室在家便可享受大自然氧吧享受大自然福乐丰活水产品不仅仅是一个水处理产品更是一个大自然的能量师与身心灵的疗愈师将纯净大自然带入家庭搬一片大自然进家邀一片大自然进家图为用户分享备注说明活水产品不是传统的净水产品没有过滤功能但可让水中的物质沉淀下来如果担心水龙头的水不够干净可在水龙头中加过滤后取水放于活水器中或者经活水器处理再放进过滤器中过滤后使用有四种颜色深蓝浅蓝摩卡白金色每种颜色水流的方式都会有一些区别都拥有自己的特色从窑洞运出来后不会进行二次修色保留着自然本色分体深蓝那水如此强劲有力是谁在随那流水不断地向我送来无言的支持与慰藉分体浅蓝蓝色表面的水带着羞涩的气质优雅而又宁静分体摩卡白在流水的映衬下那水下的瓷好似在舞动伴着那节奏像扇动翅膀的鸟又像滑翔着的鱼分体金色年全球首推颜色流水在金色的表面流过可是又好似漂浮着根本不曾触到那表面仿佛只在空中掠过竟此般神奇福乐丰活水产品常见问题向上滑动查看内容福乐丰活水需要接水龙头吗不需要直接倒水进去福乐丰活水需要通电吗需要通电有一个水泵福乐丰活水需要安装过滤装置吗福乐丰活水产品不具备过滤功能如果水杂质含量多需要先过滤后在活化福乐丰活水用在生活哪些方面可以煮饭菜烧水泡茶浇花草喂养小动物活化后如何取用直接用竹管或者竹勺将水取出来福乐丰清洗时间要求没有固定要求视具体情况来定福乐丰活水原理未经处理的水通常以水团大分子形式存在没有活力口感差这也是大自然中的山泉水喝起来更清甜的原因经处理后的水重新呈现出生命力以活跃的小分子存在并且富含氧分子如同雨后山林中的负离子一样活水器如同自然的搬运工将山泉水带回家庭水团小分子是什么水在天然的情况下都是小分子团的经典的例子就是冰山雪水小分子团的渗透力好溶解力强所以你在山上洗澡不需要肥皂而且全世界的长寿村都在山上不在山下因为这些小分子团的水能够清除身体的毒素世界卫生组织的研究人员发现这些长寿村居民的排便没有恶臭而且这些研究人员到了长寿村一个星期后发现自己的排便也没有恶臭福乐丰活水功能福乐丰活水产品模拟大自然生态将大自然的流水音律天籁搬到家庭中来水声具有调节呼吸安静心神的作用让自然的流水声唤醒身心同时整个活水产品以水的流动形态构成太极中的无限会在家里形成一个能量磁场给家庭带来更多的美与活力水需要运动多长时间才能够达到活化效果正常运动个小时就可以达到最佳效果活化后水的氧气保留时间多长未活化的水接到杯子里面马上就变成死水没有活力没有氧气含量而活化后的水未煮开可以氧气保留天上述福乐丰家用陶瓷活水产品在欧洲乃至全球各地已安装上万套以下为我们收集到的客户对产品的使用体验反馈它喷涌如天籁般的声音令我陶醉美国感谢它给我的生命带来的平和与宁静泰国在活水中感受到那么多的不一样这令人感到神奇太棒了英国陶瓷活水产品很容易的在家里装好它纯净的声音是如此的美丽与神奇新西兰我们感受了活水安装后带来水的改进它提升了家庭的能量与和谐这是能量的转变器澳洲活水设计紧凑易于安装它字形的流动形态与舒缓的旋转韵律带来美妙的视觉澳洲福乐丰活水产品对改善家庭的风水有很好的作用某中医传承人中国以上信息收集学卓翻译学卓特别致谢本次支持平台海淘天使您可识别以下二维码关注后咨询相关事宜海淘天使有爱的跨境教育商城提供人本教育书籍华德福艺术用品有机活力农耕产品等高品质教育与生活类产品的海内外直购点击阅读原文进入团购页面特享团购优惠原创文章作者手帕网如若转载请注明出处";
        String simHashStr = SimHashUtil.getSimHashStr(CONTENT);
        String simHashStr2 = SimHashUtil.getSimHashStr(CONTENT2);

        System.out.println("simHashStr:" + simHashStr);
        System.out.println("simHashStr2:" + simHashStr2);

        int hammingDistance = SimHashUtil.hammingDistance(simHashStr, simHashStr2);
        System.out.println(hammingDistance);
    }
}

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苍煜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值