Java实现文本查重(相似度) 无三方工具版本

功能背景:

在业务记录逐渐增长的前提下,逐渐出现重复项目名称数据和重复内容数据,这些数据导致项目记录质量的下降。为了避免此中情况发生,考虑对关键数据信息进行查重校验,原计划采用第三方标准查重接口,但过程比较繁琐,需要商务对接等时间,所以暂时在自身系统中实现数据查重检验。

当然,实现标准查重类似知网论文查重那种系统就太麻烦了,甚至可以独立出来一套系统了,所以就简单实现查重功能,针对名称 和 大文本内容实现查重。

 查重逻辑:

想要做查重功能就要了解参考其他标准项目是如何实现查重逻辑的。

通常的做法是:
  1. 分割文本:将待检查的文本按照一定规则(如空格、标点等)进行分割,得到文本中的单词、短语或句子。
  2. 去除停用词:将一些常见但无实际意义的词(如“the”、“a”、“an”、“and”等)从分割后的单词中去除,以减少误判和提高效率。
  3. 计算相似度:通过比较待检查文本与已有文本的相似度来判断是否存在重复内容。相似度计算可以基于单词、短语或句子等进行,常用的算法包括余弦相似度、Jaccard相似度、编辑距离等。
  4. 设定阈值:根据实际需求设定一个阈值,判断待检查文本与已有文本的相似度是否超过该阈值。如果超过,则认为存在重复内容。
  5. 返回结果:将判断结果返回给用户,通常包括是否存在重复内容、重复率等信息。
 另外一个就是​​​​​​​:

读取文本数据之后分别计算SimHash值,之后查重文本和原始文本根据SimHash算出海明距离,进而得到相似度和查重率。

简单说下原理:
  1. SimHash是一种文本特征提取方式,它将文本映射成一个固定位数的二进制数列,这个二进制数列称为SimHash值。SimHash的原理是通过计算文本中各个关键词的hash值,然后将这些hash值加权求和,最终得到一个n位的二进制数列。
  2. 假设我们有两段文本A和B,我们可以分别计算它们的SimHash值,然后通过计算它们的海明距离来判断它们的相似度。海明距离是指两个二进制数列中,对应位置上不同的比特位的个数。我们可以用下面的公式来计算海明距离:
  3. hamming_distance = sum(1 for x, y in zip(a, b) if x != y)
  4. 其中a和b分别是两个SimHash值。假设我们假设SimHash计算出来的值是128位的二进制数列,那么它们之间的海明距离就在0-128之间。海明距离越小,说明两个文本在内容上越相似,反之则越不相似。我们可以基于海明距离来设置一个阈值,比如如果海明距离小于10,我们就认为这两个文本相似。在实际应用中,阈值的设置需要根据具体的应用场景来确定。
  5. 对于文本查重任务,我们可以先将所有文本的SimHash值计算出来,然后两两比较每一对SimHash值的海明距离,即可得到每一对文本之间的相似度。在完成这个过程之后,我们可以选择一个相似度阈值,比如90%,来判断哪些文本是重复的,哪些是不重复的。
  6. 具体来说,对于一个包含n篇文本的集合,我们需要计算出n*(n-1)/2个SimHash值,然后对于每一对SimHash值,计算它们的海明距离。最后我们可以将所有海明距离小于设定阈值的文本对输出,这些文本对就是重复的。

接口逻辑:

字符串查重的过程一般可以分为四步:

  1. 读取目标字符串和基础记录字符串。
  2. 对它们进行预处理,比如将所有字母转为小写,去除空格和特殊符号等。
  3. 应用字符串查重算法进行对比,得到它们的相似度。
  4. 选取最大相似度作为它们的最终相似度结果。

在对目标字符串和基础记录字符串进行预处理时,需要注意选择合适的方式来规范化这些字符串,例如字母大小写、空格和特殊符号等。这有助于减小字符串之间的差异,提高字符串查重的精度和效率。

对于算法的选择,可以根据具体的应用场景来决定。常见的算法包括SimHash算法、Jaccard相似度算法、Levenshtein距离算法等。这些算法都有各自的优缺点,需要根据实际情况进行选择和调优。

接口代码:

/**
 * @Author Jiangfy
 * @Description 字符串查重率计算
 * @Param
 * @return
 **/
@Override
public ResultBean checkDuplicateRate(ServiceContext ServiceContext, String targetStr) {
    StringUtils.isBlankAssert(targetStr, "查重目标字符串不能为空");
    ResultBean resultBean = new ResultBean();

    // 1.查询当前所有项目名称数据
    List projectNameList = new ArrayList<>();
    ResultBean resultBeanProjName = projectMapper.queryProjectByCond();
    JSONArray dataArray = resultBeanProjName.getJSONArray("resultset");
    for (Object obj : dataArray) {
        JSONObject jsonObj = (JSONObject) obj;
        String projectName = jsonObj.getString("name");
        projectNameList.add(projectName);
    }

    // 2.先对名称和基础数据进行预处理
    String finalTargetStr = StringUtils.replaceAll(targetStr, "[^a-zA-Z0-9\\u4E00-\\u9FA5]", "").toLowerCase();
    List processedList = projectNameList.stream()
            .map(str -> StringUtils.replaceAll(str, "[^a-zA-Z0-9\\u4E00-\\u9FA5]", "").toLowerCase())
            .collect(Collectors.toList());

    // 3.使用算法对基础数据循环比对
    AtomicReference maxSimilarity = new AtomicReference<>(0.0);
    processedList.parallelStream().forEach(name -> {
        double sim = Util.similarity(finalTargetStr, name, false);
        maxSimilarity.updateAndGet(current -> Double.compare(sim, current) > 0 ? sim : current);
    });

    // 格式化保留两位小数
    DecimalFormat df = new DecimalFormat("0.00"); // 保留两位小数
    double roundedValue = Double.parseDouble(df.format(maxSimilarity.get())) * 100; // 获取保留两位小数后的值  并转换百分比

    // 4.取相似度最高值最终结果
    resultBean.setSuccess();
    resultBean.addParam("maxSimilarity", roundedValue);
    return resultBean;
}

计算相似度工具方法:

/**
 * @Author Jiangfy
 * @Description //莱文斯坦距离算法来计算两个字符串之间的相似度
 * @Param [strA, strB, isRegex]
 * @return double
 **/
public static double similarity(String strA, String strB ,boolean isRegex) {
    // 移除字符串中的非字母、数字、汉字部分
    if (isRegex){
        strA = strA.replaceAll("[^a-zA-Z0-9\\u4E00-\\u9FA5]", "");
        strB = strB.replaceAll("[^a-zA-Z0-9\\u4E00-\\u9FA5]", "");
    }

    // 计算莱文斯坦距离
    int[][] distance = new int[strA.length() + 1][strB.length() + 1];
    for (int i = 0; i <= strA.length(); i++) {
        distance[i][0] = i;
    }
    for (int j = 0; j <= strB.length(); j++) {
        distance[0][j] = j;
    }
    for (int i = 1; i <= strA.length(); i++) {
        for (int j = 1; j <= strB.length(); j++) {
            if (strA.charAt(i - 1) == strB.charAt(j - 1)) {
                distance[i][j] = distance[i - 1][j - 1];
            } else {
                distance[i][j] = Math.min(distance[i - 1][j] + 1, Math.min(distance[i][j - 1] + 1, distance[i - 1][j - 1] + 1));
            }
        }
    }

    // 计算相似度
    int maxLen = Math.max(strA.length(), strB.length());
    return (maxLen - distance[strA.length()][strB.length()]) / (double) maxLen;
}

文本查重通常包括以下步骤:

  1. 获取目标文本和基础查重数据。
  2. 针对文本进行预处理,包括分词、断句、小写化和去除空格等操作。
  3. 运用算法(如Jaccard)计算文本之间的相似度。
  4. 选择最大相似度作为它们的最终相似度结果。

在进行预处理时,要根据实际情况选择适当的方式来规范化文本,以便提高查重精度。在算法选择方面,也需要根据具体应用场景来进行评估和调优,以达到最佳效果。

接口代码:

 /**
 * @Author Jiangfy
 * @Description 检验文本查重率
 * @Param targetStr
 * @return
 **/
@Override
public ResultBean calculateDuplicationRate(ServiceContext ServiceContext, String targetStr){
    StringUtils.isBlankAssert(targetStr, "查重目标字符串不能为空");
    ResultBean resultBean = new ResultBean();
    //1.查询当前所有项目名称数据
    List abstractStrList = new ArrayList<>();
    ResultBean resultBeanAbstract = projectServiceMapper.queryProjectByCond();
    JSONArray dataArray = resultBeanAbstract.getJSONArray("resultset");
    for (Object obj : dataArray) {
        JSONObject jsonObj = (JSONObject) obj;
        String abstractStr = jsonObj.getString("abstract");
        abstractStrList.add(abstractStr);
    }
    // 使用removeIf和StringUtils来删除空字符串
    abstractStrList.removeIf(uString::isBlank);
    //2. 根据标点符号进行断句  此处原计划采用三方分词工具暂时按照标点符号断句
    Set finalTargetStrSet = TechUtil.splitSentences(targetStr);
    Set> processedSet = TechUtil.splitSentences(new HashSet<>(abstractStrList));
    //todo 考虑将processedSet结果放入缓存 设置过期时间 避免重复查数据库
    //3.使用算法对两组文本基础数据循环比对
    AtomicReference maxSimilarity = new AtomicReference<>(0.0);
    processedSet.parallelStream().forEach(set -> {
        double sim = Util.calculateJaccardSimilarity(finalTargetStrSet, set);
        maxSimilarity.updateAndGet(current -> Double.compare(sim, current) > 0 ? sim : current);
    });
    //格式化保留两位小数
    DecimalFormat df = new DecimalFormat("0.00"); // 保留两位小数
    double roundedValue = Double.parseDouble(df.format(maxSimilarity.get())) * 100; // 获取保留两位小数后的值  并转换百分比
    //4.取相似度最高值最终结果
    resultBean.setSuccess();
    resultBean.addParam("maxSimilarity",roundedValue);
    return resultBean;
}

文本分句工具方法:

/**
 * @Author Jiangfy
 * @Description 文本字符串根据标点符号分句
 * @Param [text]
 * @return java.util.Set
 **/
public static Set splitSentences(String text) {
    // 定义正则表达式,用于匹配句子的标点符号
    String regex = "[。!?,;:“”‘’【】《》()\\[\\]{}.,;:\"'?!]";

    // 使用正则表达式将字符串分句
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(text);
    int start = 0;
    Set sentences = new HashSet<>();

    while (matcher.find()) {
        String sentence = text.substring(start, matcher.start()).trim();
        if (!sentence.isEmpty()) {
            sentences.add(sentence.toLowerCase().replaceAll("\\s+", ""));
        }
        start = matcher.end();
    }

    // 处理最后一个句子
    String lastSentence = text.substring(start).trim();
    if (!lastSentence.isEmpty()) {
        sentences.add(lastSentence.toLowerCase().replaceAll("\\s+", ""));
    }

    return sentences;
}


/**
 * @Author Jiangfy
 * @Description 文本集合根据标点符号分句
 * @Param [inputSet]
 * @return java.util.Set>
 **/
public static Set> splitSentences(Set inputSet) {
    return inputSet.stream()
            .map(str -> Arrays.stream(str.split("[。!?,;:“”‘’【】《》()\\[\\]{}.,;:\"'?!]"))
                    .map(String::toLowerCase)
                    .map(sentence -> sentence.replaceAll("\\s+", ""))//去除空格
                    .collect(Collectors.toSet()))
            .collect(Collectors.toSet());
}

Jaccard计算相似度方法:

/**
 * @Author Jiangfy
 * @Description Jaccard计算相似度 double
 * @Param [set1, set2]
 * @return double
 **/
public static double calculateJaccardSimilarity(Set set1, Set set2) {
    if (set1.size() == 0 && set2.size()  == 0) {
        return 1.0;
    }
    // 都为空相似度为 1
    if (set1.size()  == 0 || set2.size()  == 0) {
        return 0.0;
    }
    Set intersection = new HashSet<>(set1);
    intersection.retainAll(set2); // 计算交集

    Set union = new HashSet<>(set1);
    union.addAll(set2); // 计算并集

    return (double) intersection.size() / union.size();
}

你可能感兴趣的:(后端,java,开发语言)