在业务记录逐渐增长的前提下,逐渐出现重复项目名称数据和重复内容数据,这些数据导致项目记录质量的下降。为了避免此中情况发生,考虑对关键数据信息进行查重校验,原计划采用第三方标准查重接口,但过程比较繁琐,需要商务对接等时间,所以暂时在自身系统中实现数据查重检验。
当然,实现标准查重类似知网论文查重那种系统就太麻烦了,甚至可以独立出来一套系统了,所以就简单实现查重功能,针对名称 和 大文本内容实现查重。
想要做查重功能就要了解参考其他标准项目是如何实现查重逻辑的。
读取文本数据之后分别计算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 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;
}
文本查重通常包括以下步骤:
- 获取目标文本和基础查重数据。
- 针对文本进行预处理,包括分词、断句、小写化和去除空格等操作。
- 运用算法(如Jaccard)计算文本之间的相似度。
- 选择最大相似度作为它们的最终相似度结果。
在进行预处理时,要根据实际情况选择适当的方式来规范化文本,以便提高查重精度。在算法选择方面,也需要根据具体应用场景来进行评估和调优,以达到最佳效果。
接口代码:
/**
* @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();
}