功能背景:
在业务记录逐渐增长的前提下,逐渐出现重复项目名称数据和重复内容数据,这些数据导致项目记录质量的下降。为了避免此中情况发生,考虑对关键数据信息进行查重校验,原计划采用第三方标准查重接口,但过程比较繁琐,需要商务对接等时间,所以暂时在自身系统中实现数据查重检验。
当然,实现标准查重类似知网论文查重那种系统就太麻烦了,甚至可以独立出来一套系统了,所以就简单实现查重功能,针对名称 和 大文本内容实现查重。
查重逻辑:
想要做查重功能就要了解参考其他标准项目是如何实现查重逻辑的。
通常的做法是:
- 分割文本:将待检查的文本按照一定规则(如空格、标点等)进行分割,得到文本中的单词、短语或句子。
- 去除停用词:将一些常见但无实际意义的词(如“the”、“a”、“an”、“and”等)从分割后的单词中去除,以减少误判和提高效率。
- 计算相似度:通过比较待检查文本与已有文本的相似度来判断是否存在重复内容。相似度计算可以基于单词、短语或句子等进行,常用的算法包括余弦相似度、Jaccard相似度、编辑距离等。
- 设定阈值:根据实际需求设定一个阈值,判断待检查文本与已有文本的相似度是否超过该阈值。如果超过,则认为存在重复内容。
- 返回结果:将判断结果返回给用户,通常包括是否存在重复内容、重复率等信息。
另外一个就是:
读取文本数据之后分别计算SimHash值,之后查重文本和原始文本根据SimHash算出海明距离,进而得到相似度和查重率。
简单说下原理:
- SimHash是一种文本特征提取方式,它将文本映射成一个固定位数的二进制数列,这个二进制数列称为SimHash值。SimHash的原理是通过计算文本中各个关键词的hash值,然后将这些hash值加权求和,最终得到一个n位的二进制数列。
- 假设我们有两段文本A和B,我们可以分别计算它们的SimHash值,然后通过计算它们的海明距离来判断它们的相似度。海明距离是指两个二进制数列中,对应位置上不同的比特位的个数。我们可以用下面的公式来计算海明距离:
- hamming_distance = sum(1 for x, y in zip(a, b) if x != y)
- 其中a和b分别是两个SimHash值。假设我们假设SimHash计算出来的值是128位的二进制数列,那么它们之间的海明距离就在0-128之间。海明距离越小,说明两个文本在内容上越相似,反之则越不相似。我们可以基于海明距离来设置一个阈值,比如如果海明距离小于10,我们就认为这两个文本相似。在实际应用中,阈值的设置需要根据具体的应用场景来确定。
- 对于文本查重任务,我们可以先将所有文本的SimHash值计算出来,然后两两比较每一对SimHash值的海明距离,即可得到每一对文本之间的相似度。在完成这个过程之后,我们可以选择一个相似度阈值,比如90%,来判断哪些文本是重复的,哪些是不重复的。
- 具体来说,对于一个包含n篇文本的集合,我们需要计算出n*(n-1)/2个SimHash值,然后对于每一对SimHash值,计算它们的海明距离。最后我们可以将所有海明距离小于设定阈值的文本对输出,这些文本对就是重复的。
接口逻辑:
字符串查重的过程一般可以分为四步:
- 读取目标字符串和基础记录字符串。
- 对它们进行预处理,比如将所有字母转为小写,去除空格和特殊符号等。
- 应用字符串查重算法进行对比,得到它们的相似度。
- 选取最大相似度作为它们的最终相似度结果。
在对目标字符串和基础记录字符串进行预处理时,需要注意选择合适的方式来规范化这些字符串,例如字母大小写、空格和特殊符号等。这有助于减小字符串之间的差异,提高字符串查重的精度和效率。
对于算法的选择,可以根据具体的应用场景来决定。常见的算法包括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<String> 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<String> processedList = projectNameList.stream()
.map(str -> StringUtils.replaceAll(str, "[^a-zA-Z0-9\\u4E00-\\u9FA5]", "").toLowerCase())
.collect(Collectors.toList());
// 3.使用算法对基础数据循环比对
AtomicReference<Double> 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;
}
文本查重通常包括以下步骤:
- 获取目标文本和基础查重数据。
- 针对文本进行预处理,包括分词、断句、小写化和去除空格等操作。
- 运用算法(如Jaccard)计算文本之间的相似度。
- 选择最大相似度作为它们的最终相似度结果。
在进行预处理时,要根据实际情况选择适当的方式来规范化文本,以便提高查重精度。在算法选择方面,也需要根据具体应用场景来进行评估和调优,以达到最佳效果。
接口代码:
/**
* @Author Jiangfy
* @Description 检验文本查重率
* @Param targetStr
* @return
**/
@Override
public ResultBean calculateDuplicationRate(ServiceContext ServiceContext, String targetStr){
StringUtils.isBlankAssert(targetStr, "查重目标字符串不能为空");
ResultBean resultBean = new ResultBean();
//1.查询当前所有项目名称数据
List<String> 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<String> finalTargetStrSet = TechUtil.splitSentences(targetStr);
Set<Set<String>> processedSet = TechUtil.splitSentences(new HashSet<>(abstractStrList));
//todo 考虑将processedSet结果放入缓存 设置过期时间 避免重复查数据库
//3.使用算法对两组文本基础数据循环比对
AtomicReference<Double> 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<java.lang.String>
**/
public static Set<String> splitSentences(String text) {
// 定义正则表达式,用于匹配句子的标点符号
String regex = "[。!?,;:“”‘’【】《》()\\[\\]{}.,;:\"'?!]";
// 使用正则表达式将字符串分句
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
int start = 0;
Set<String> 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<java.util.Set<java.lang.String>>
**/
public static Set<Set<String>> splitSentences(Set<String> 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<String> set1, Set<String> set2) {
if (set1.size() == 0 && set2.size() == 0) {
return 1.0;
}
// 都为空相似度为 1
if (set1.size() == 0 || set2.size() == 0) {
return 0.0;
}
Set<String> intersection = new HashSet<>(set1);
intersection.retainAll(set2); // 计算交集
Set<String> union = new HashSet<>(set1);
union.addAll(set2); // 计算并集
return (double) intersection.size() / union.size();
}