lambdaQuery()
+ .eq(PlatformDatasetQuestionDO::getDatasetId, datasetid)
+ );
+ return count;
+ }
+
+ @Override
+ public Long getCountByDatasetid(LambdaQueryWrapper query){
+ Long count = platformDatasetQuestionMapper.selectCount(query);
+ return count;
+ }
+
+}
diff --git a/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/DataProcessPlatformUtil.java b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/DataProcessPlatformUtil.java
new file mode 100644
index 000000000..465ab2e85
--- /dev/null
+++ b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/DataProcessPlatformUtil.java
@@ -0,0 +1,799 @@
+package cn.iocoder.yudao.module.mdpf.util;
+
+
+import com.github.houbb.opencc4j.util.ZhConverterUtil;
+import com.github.houbb.sensitive.word.core.SensitiveWordHelper;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+ @Slf4j
+ public class DataProcessPlatformUtil {
+
+ /*
+ * ---------------------------------------------------------------
+ * 🔖 【 异常清洗配置 】
+ * ---------------------------------------------------------------
+ */
+
+ /**
+ * 移除不可见字
+ * 移除ASCII中的一些不可见字符, 如0-32 和127-160这两个范围
+ *
+ * @param input
+ * @return
+ */
+ public static String removeNonVisibleAsciiChars (String input) {
+ // 使用StringBuilder来构建正则表达式,因为我们需要动态地添加字符范围
+ StringBuilder regex = new StringBuilder();
+ regex.append("[\\x00-\\x1F]"); // 0-31范围的字符
+ regex.append("|"); // OR 操作符
+ regex.append("[\\x7F-\\xA0]"); // 127-160范围的字符
+
+ // 使用replaceAll方法和构建的正则表达式来移除不可见字符
+ return input.replaceAll(regex.toString(), "");
+ }
+
+ /**
+ * 移除不可见字符
+ *
+ * 将不同的unicode空格比如 u2008,转成正常的空格
+ *
+ * @param input
+ * @return
+ */
+ public static String convertUnicodeSpacesToNormalSpaces (String input) {
+ // Unicode空格字符的正则表达式,包括但不限于u2008等
+ String unicodeSpacesRegex = "[\\u0020\\u00A0\\u1680\\u2000-\\u200A\\u2028\\u2029\\u202F\\u205F\\u3000]";
+
+ // 使用正则表达式替换匹配的Unicode空格字符为普通空格
+ return input.replaceAll(unicodeSpacesRegex, " ");
+ }
+
+ /**
+ * 移除不可见字符
+ *
+ * 去除乱码和无意义的unicode
+ *
+ * @param input
+ * @return
+ */
+ public static String removeNonPrintableUnicodeChars (String input) {
+ // 构建一个正则表达式,匹配所有非打印ASCII和非打印Unicode字符
+ // \p{C} 匹配所有控制字符和格式字符
+ // \p{Zs} 匹配所有空白分隔符(比如U+2000到U+200F之间的字符)
+ // 注意:有些空白字符可能是有意义的,比如空格(U+0020),所以这里的选择要谨慎
+ // 如果你确定某些空白字符是无意义的,可以将其添加到正则表达式中
+ String regex = "[\\p{C}\\p{Zs}&&[^\\s]]+|\\u0000"; // \\u0000 是NULL字符,通常是无意义的
+
+ // 使用replaceAll方法移除匹配的字符
+ // 注意:这里使用了两个替换步骤,因为直接替换可能会导致正则表达式匹配问题
+ // 首先替换掉所有匹配的字符为一个占位符(比如"*"),然后再替换掉占位符为空字符串
+ // 这样做是为了避免在替换过程中正则表达式匹配到已经被替换掉的部分
+ // 但在这种情况下,由于我们使用的是字符类匹配,其实直接替换为空字符串也是可以的
+ // 下面的代码为了演示这种可能的复杂性而保留了两步替换的逻辑
+ String intermediate = input.replaceAll(regex, "*"); // 这一步其实是多余的,但为了说明而保留
+ return intermediate.replaceAll("[*]+", ""); // 这一步实际上完成了去除非打印字符的任务
+
+ // 简化版:直接替换为空字符串
+ // return input.replaceAll(regex, "");
+ }
+
+ /**
+ * 繁体转简体
+ *
+ * 繁体转简体,如“不經意,妳的笑容”清洗成“不经意,你的笑容”
+ *
+ * @param input
+ * @return
+ */
+ public static String traditionalToSimplified (String input) {
+ return ZhConverterUtil.toSimple(input);
+ }
+
+ // 使用正则表达式匹配HTML标签
+ private static final String HTML_TAG_REGEX = "<[^>]+>";
+
+ /**
+ * 去除网页标识符
+ *
+ * 移除文档中的html标签,如,等
+ *
+ * @param input
+ * @return
+ */
+ public static String removeHtmlTags (String input) {
+ if (input == null || input.isEmpty()) {
+ return input;
+ }
+ // 使用replaceAll方法替换匹配的HTML标签为空字符串
+ return input.replaceAll(HTML_TAG_REGEX, "");
+ }
+
+ // 这是一个简化的正则表达式,用于匹配常见的emoji表情符号。
+ // 请注意,它可能不会涵盖所有可能的emoji,因为Unicode标准在不断发展。
+ private static final String EMOJI_REGEX = "[\\uD83C-\\uD83D\\uD83E-\\uD83F\\u2600-\\u27FF"
+ + "\\u2B00-\\u2BFF\\u2F00-\\u2FFF\\u3000-\\u303F"
+ + "\\u3200-\\u32FF\\uA490-\\uA4CF\\uA900-\\uA97F"
+ + "\\uAC00-\\uAC7F\\uAC80-\\uACFF\\uD700-\\uD7AF"
+ + "\\uF900-\\uFAFF\\uFB00-\\uFB4F\\uFB50-\\uFDFF"
+ + "\\uFE00-\\uFE6F\\uFE70-\\uFEFF\\uFF00-\\uFFEF]";
+
+ /**
+ * 去除表情
+ *
+ * 去除文档中的表情,如‘🐰’、‘👵’等
+ *
+ * @param input
+ * @return
+ */
+ public static String removeEmojis (String input) {
+ if (input == null || input.isEmpty()) {
+ return input;
+ }
+ Pattern pattern = Pattern.compile(EMOJI_REGEX, Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE);
+ Matcher matcher = pattern.matcher(input);
+ return matcher.replaceAll("");
+ }
+
+ // 正则表达式用于匹配中文词汇(这里假设词汇由连续的中文字符组成)
+ private static final String CHINESE_WORD_REGEX = "[\\u4e00-\\u9fff]+";
+
+ // 方法:计算字符串中的中文字符数量
+ // 注意:这里假设输入字符串只包含中文字符和可能的分隔符(如空格、标点符号等)
+ // 并且中文字符在UTF-16编码中占用两个char,但被视为一个逻辑字符
+ private static int countChineseChars (String input) {
+ // 使用正则表达式匹配中文词汇,并计算匹配到的字符总数(这里需要除以2,因为每个中文字符占用两个char)
+ // 但为了简化,我们可以直接遍历字符,检查每个字符是否在中文范围内
+ int count = 0;
+ for (char c : input.toCharArray()) {
+ if (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
+ || Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
+ // 可以根据需要添加更多Unicode块
+ ) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /*
+ * ---------------------------------------------------------------
+ * 🔖 【 过滤配置 】
+ * ---------------------------------------------------------------
+ */
+
+ /**
+ * 检查文档的词数目
+ * 词数目不在指定范围会被过滤掉,如中文[1,1000000]
+ *
+ * @param text
+ * @param minChars
+ * @param maxChars
+ * @return
+ */
+ public static List filterWords (String text, int minChars, int maxChars) {
+ List result = new ArrayList<>();
+ Pattern pattern = Pattern.compile(CHINESE_WORD_REGEX);
+ Matcher matcher = pattern.matcher(text);
+
+ while (matcher.find()) {
+ String word = matcher.group();
+ int chineseCharCount = countChineseChars(word); // 计算中文字符数量
+ if (chineseCharCount >= minChars && chineseCharCount <= maxChars) {
+ result.add(word);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 检查文档的字重复率
+ *
+ * 如果字重复率太高,意味着文档中重复的字太多,文档会被过滤掉
+ *
+ *
+ * @param content 文档行
+ * @param threshold 设置字重复率的阈值,例如10%
+ * @return true表示字重复率低于阈值,false表示字重复率高于阈值,文档会被过滤掉
+ */
+ public static boolean calculateCharacterRepetitionRate(String content, double threshold) {
+ // 输入校验
+ if (content == null || content.trim().isEmpty()) {
+ return false;
+ }
+
+ // 预处理(去空格、标点等)
+ String processedContent = content
+ .replaceAll("\\s+", "")
+ .replaceAll("[\\pP\\pS]", "");
+
+ // 短文本不检查
+ if (processedContent.length() < 5) {
+ return false;
+ }
+
+ // 统计字符频率
+ Map charCount = new HashMap<>();
+ char[] chars = processedContent.toCharArray();
+ for (char c : chars) {
+ if (isChineseCharacter(c)) { // 可选:仅统计中文
+ charCount.put(c, charCount.getOrDefault(c, 0) + 1);
+ }
+ }
+
+ // 计算重复率(方式1:传统重复率)
+ int totalChars = chars.length;
+ double repetitionRate = (double) (totalChars - charCount.size()) / totalChars;
+
+ // 将重复率转换为百分比(0~100),以便与阈值直接比较
+ double repetitionPercent = repetitionRate * 100;
+
+ // 调试日志(输出百分比)
+ log.info("总字数: {}", totalChars);
+ log.info("重复字数: {}", totalChars - charCount.size());
+ log.info("字重复率: {}%", String.format("%.2f", repetitionPercent));
+
+ // 比较前可添加浮点数容差(可选)
+ final double EPSILON = 0.0001;
+ return repetitionPercent - threshold > EPSILON;
+ }
+
+
+ // 判断是否为中文字符(可选)
+ private static boolean isChineseCharacter(char c) {
+ Character.UnicodeScript sc = Character.UnicodeScript.of(c);
+ return sc == Character.UnicodeScript.HAN;
+ }
+
+ // 简单的基于空格和标点符号的分词方法
+ private static List tokenize (String text) {
+ // 使用正则表达式匹配非单词字符(包括空格、标点符号等),并将它们作为分隔符
+ Pattern pattern = Pattern.compile("\\W+");
+ String[] words = pattern.split(text.toLowerCase()); // 转换为小写以进行不区分大小写的比较
+ List tokens = new ArrayList<>();
+ for (String word : words) {
+ if (!word.isEmpty()) { // 排除空字符串
+ tokens.add(word);
+ }
+ }
+ return tokens;
+ }
+
+ // 方法:计算文档的词重复率
+
+ /**
+ * 检查文档的词重复率
+ *
+ * 如果词重复率太高,意味着文档中重复的词太多,文档会被过滤掉
+ *
+ * @param content
+ * @param threshold
+ * @return
+ */
+ public static boolean calculateWordRepetitionRate (String content, double threshold) {
+ // 分词
+ List words = tokenize(content);
+
+ // 统计词出现次数
+ Map wordCount = new HashMap<>();
+ for (String word : words) {
+ wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
+ }
+
+ // 计算重复词数和总词数
+ int totalWords = words.size();
+ int repeatedWords = 0;
+ for (int count : wordCount.values()) {
+ if (count > 1) {
+ repeatedWords += (count - 1); // 只计算重复的部分
+ }
+ }
+
+ // 计算词重复率
+ double repetitionRate = (double) repeatedWords / totalWords;
+
+ // 打印重复率和阈值,方便调试
+ log.info("词重复率: " + repetitionRate);
+ log.info("阈值: " + threshold);
+
+ // 如果重复率超过阈值,返回true表示需要过滤掉文档
+ return repetitionRate > threshold;
+ }
+
+ /**
+ * 检查文档的特殊字符率
+ * 如果特殊字符率太高,意味着文档中特殊字符太多,文档会被过滤掉
+ *
+ * @param content
+ * @param threshold
+ * @return
+ */
+ /**
+ * 检测文本中特殊字符率是否超过阈值(阈值范围0~100.00)
+ * @param content 待检测文本
+ * @param threshold 百分比阈值(如传入10表示10%)
+ * @return 超过阈值返回true
+ */
+ public static boolean checkSpecialCharacterRate(String content, double threshold) {
+ // 参数校验
+ if (content == null || content.isEmpty()) {
+ log.warn("输入内容为空");
+ return false;
+ }
+ if (threshold < 0 || threshold > 100) {
+ throw new IllegalArgumentException("阈值必须是0~100之间的数值");
+ }
+
+ // 预处理:去除所有空白字符(可选)
+ String processedContent = content.replaceAll("\\s+", "");
+ int totalCharCount = processedContent.length();
+
+ // 空文本或纯空白内容处理
+ if (totalCharCount == 0) {
+ log.info("有效字符数为0");
+ return false;
+ }
+
+ // 统计特殊字符(非字母、数字、汉字)
+ // 正则说明:
+ // [^a-zA-Z0-9\\p{Script=Han}] → 排除字母数字和汉字
+ // 如需包含其他语言字符,需调整正则
+ Pattern pattern = Pattern.compile("[^a-zA-Z0-9\\p{Script=Han}]");
+ Matcher matcher = pattern.matcher(processedContent);
+
+ int specialCharCount = 0;
+ while (matcher.find()) {
+ specialCharCount++;
+ }
+
+ // 计算特殊字符率(转换为百分比)
+ double specialCharRatePercent = (double) specialCharCount / totalCharCount * 100;
+
+ // 调试日志(保留2位小数)
+ DecimalFormat df = new DecimalFormat("0.00");
+ log.info("特殊字符检测结果: {}/{}={}% (阈值: {}%)",
+ specialCharCount,
+ totalCharCount,
+ df.format(specialCharRatePercent),
+ df.format(threshold));
+
+ // 浮点数精确比较(添加1e-6容差)
+ final double EPSILON = 1e-6;
+ return specialCharRatePercent - threshold > EPSILON;
+ }
+
+ /**
+ * 检查文档的色情暴力词率
+ *
+ * 如果色情暴力词率太高,文档会被过滤掉,取值范围[0,100]。
+ *
+ *
+ * @param content 文本内容
+ * @param threshold 阈值
+ * @return 是否过滤文档
+ */
+ public static boolean checkSensitiveWordRate (String content, double threshold) {
+ // TODO: 先使用 sensitive-word 处理,有修改再调整
+
+ // 检测是否包含色情暴力词
+ boolean isFalse = SensitiveWordHelper.contains(content);
+ if (!isFalse) {
+ return false;
+ }
+
+ //返回所有敏感词
+ List wordList = SensitiveWordHelper.findAll(content);
+ log.info("返回所有敏感词====>>>>{}", wordList);
+
+ // 统计敏感词的字符数量
+ int sensitiveWordLength = 0;
+ for (String word : wordList) {
+ sensitiveWordLength += word.length();
+ }
+ // 计算文档的总字符数(不包括换行符等空白字符,可以根据需要调整)
+ // 或者使用 content.replaceAll("\\s+", "").length() 来排除空白字符
+ int totalCharCount = content.length();
+
+ // 计算敏感词长度占总长度的百分比
+ double specialCharRate = ((double) sensitiveWordLength / totalCharCount) * 100;
+
+ // 打印敏感词字符率和阈值,方便调试
+ log.info("敏感词字符率: {}", String.format("%.3f", specialCharRate));
+ log.info("阈值: {}", threshold);
+
+ // 如果敏感词字符率超过阈值,返回true表示需要过滤掉文档
+ return specialCharRate > threshold;
+ }
+ /*
+ * ---------------------------------------------------------------
+ * 🔖 【 去重配置 】
+ * ---------------------------------------------------------------
+ */
+
+ /**
+ * 相似度去重配置
+ *
+ * @param contentMap 文本内容列表
+ * @param threshold 相似度阈值
+ * @return 是否需要去重
+ */
+ /**
+ * 基于SimHash的文本相似度去重
+ * @param contentMap 文本集合(Key: 文档ID, Value: 文本内容)
+ * @param threshold 相似度阈值(0~1,如0.8表示80%相似)
+ * @return 需要删除的文档ID列表
+ */
+ public static List similarityDeduplication(Map contentMap, double threshold) {
+ // 参数校验
+ if (contentMap == null || contentMap.isEmpty()) {
+ return Collections.emptyList();
+ }
+ if (threshold < 0 || threshold > 1) {
+ throw new IllegalArgumentException("相似度阈值必须在0~1之间");
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ // 1. 按文档ID排序(保持处理顺序确定性)
+ LinkedHashMap sortedMap = contentMap.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ Map.Entry::getValue,
+ (e1, e2) -> e1,
+ LinkedHashMap::new));
+
+ // 2. 并行计算SimHash(提升大数据量性能)
+ Map simHashMap = sortedMap.entrySet().parallelStream()
+ .collect(Collectors.toMap(
+ Map.Entry::getKey,
+ entry -> HammingUtils.getSimHash(entry.getValue()),
+ (e1, e2) -> e1,
+ LinkedHashMap::new));
+
+ // 3. 相似度检测
+ List duplicateKeys = new ArrayList<>();
+ List processedIds = new ArrayList<>(simHashMap.keySet());
+
+ for (int i = 0; i < processedIds.size(); i++) {
+ Long currentId = processedIds.get(i);
+ if (duplicateKeys.contains(currentId)) {
+ continue;
+ }
+
+ String hash1 = simHashMap.get(currentId);
+
+ // 只与后续未处理的文档比较
+ for (int j = i + 1; j < processedIds.size(); j++) {
+ Long compareId = processedIds.get(j);
+ if (duplicateKeys.contains(compareId)) {
+ continue;
+ }
+
+ double similarity = HammingUtils.getSimilarity(
+ hash1,
+ simHashMap.get(compareId));
+
+ log.debug("文档 {} 与 {} 的相似度: {:.2f}%",
+ currentId, compareId, similarity * 100);
+
+ if (similarity > threshold) {
+ duplicateKeys.add(compareId);
+ log.info("标记为相似: {} ≈ {} (相似度: {:.2f}%)",
+ currentId, compareId, similarity * 100);
+ }
+ }
+ }
+
+ // 4. 性能日志
+ long cost = System.currentTimeMillis() - startTime;
+ log.info("去重完成: 总数={}, 重复数={}, 耗时={}ms",
+ contentMap.size(),
+ duplicateKeys.size(),
+ cost);
+
+ return duplicateKeys;
+ }
+
+ /*
+ * ---------------------------------------------------------------
+ * 🔖 【 去隐私配置 】
+ * ---------------------------------------------------------------
+ */
+ // 定义一个正则表达式来匹配电子邮件地址
+ private static final String EMAIL_REGEX =
+ "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}";
+
+ // 编译正则表达式为Pattern对象
+ private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
+
+ // 去除文本中的电子邮件地址
+ private static String removeEmails (String text) {
+ Matcher matcher = EMAIL_PATTERN.matcher(text);
+ // 使用空字符串替换匹配的电子邮件地址
+ return matcher.replaceAll("");
+ }
+
+ /**
+ * 去除Email
+ *
+ * 去除email地址
+ *
+ * @param content
+ */
+ public static String processFile (String content) {
+
+ // 去除电子邮件地址
+ String modifiedContent = removeEmails(content);
+
+ // 或者打印到控制台以查看结果
+ log.info("去除电子邮件地址:{}", modifiedContent);
+ return modifiedContent;
+ }
+
+ // 定义一个正则表达式来匹配IPv4地址
+ private static final String IPV4_REGEX =
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
+ "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
+
+ // 定义一个正则表达式来匹配IPv6地址
+ // 这个正则表达式相对简单,可能无法匹配所有复杂的IPv6地址格式
+ // 但它可以匹配常见的IPv6地址,如2001:0db8:85a3:0000:0000:8a2e:0370:7334
+ private static final String IPV6_REGEX =
+ "([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4})";
+
+ // 编译IPv4正则表达式为Pattern对象
+ private static final Pattern IPV4_PATTERN = Pattern.compile(IPV4_REGEX);
+
+ // 编译IPv6正则表达式为Pattern对象
+ private static final Pattern IPV6_PATTERN = Pattern.compile(IPV6_REGEX);
+
+ /**
+ * 去除文本中的IPv4和IPv6地址
+ */
+ public static String removeIPAddresses (String text) {
+ Matcher ipv4Matcher = IPV4_PATTERN.matcher(text);
+ text = ipv4Matcher.replaceAll("");
+ Matcher ipv6Matcher = IPV6_PATTERN.matcher(text);
+ return ipv6Matcher.replaceAll("");
+ }
+
+ /**
+ * 手机号码的正则表达式
+ */
+ private static final String MOBILE_REGEX = "1\\d{10}";
+
+ /**
+ * 国内电话号码的正则表达式
+ */
+ private static final String DOMESTIC_PHONE_REGEX = "(\\d{4}-|\\d{3}-)?(\\d{8}|\\d{7})";
+
+ private static final String HOTLINE_REGEX = "^\\d{3,4}(-\\d{3,4})+$";
+ /**
+ * 电话号码(400)的正则表达式
+ */
+ private static final String PHONE_REGEX = "400(-\\d{3,4}){2}|^800(-\\d{3,4}){2}";
+
+ /**
+ * 信用卡号的正则表达式
+ */
+ private static final String CREDIT_CARD_REGEX = "^([1-9]{1})(\\d{15}|\\d{18})$";
+
+ /**
+ * 十六进制散列的正则表达式(32或24 位十六进制数,用于 SHA-256 等)
+ */
+ private static final String HASH_REGEX = "[a-fA-F0-9]{32}|[a-fA-F0-9]{24}";
+
+ // 编译正则表达式为Pattern对象
+ private static final Pattern MOBILE_PATTERN = Pattern.compile(MOBILE_REGEX);
+ private static final Pattern DOMESTIC_PHONE_PATTERN = Pattern.compile(DOMESTIC_PHONE_REGEX);
+ private static final Pattern PHONE_PATTERN = Pattern.compile(PHONE_REGEX);
+ private static final Pattern HOTLINE_PATTERN = Pattern.compile(HOTLINE_REGEX);
+ private static final Pattern CREDIT_CARD_PATTERN = Pattern.compile(CREDIT_CARD_REGEX);
+ private static final Pattern HASH_PATTERN = Pattern.compile(HASH_REGEX);
+
+ // 定义一个年份格式
+ private static final DateTimeFormatter YEAR_FORMAT = DateTimeFormatter.ofPattern("yyyy");
+
+ // 定义一个集合来存储要跳过的年份(这里我们假设跳过当前年份和前几年的范围)
+ private static final Set YEARS_TO_SKIP = new HashSet<>();
+
+ static {
+ int currentYear = Year.now().getValue();
+ for (int i = currentYear - 5; i <= currentYear + 5; i++) {
+ YEARS_TO_SKIP.add(String.valueOf(i));
+ }
+ }
+
+ /**
+ * 去除数字
+ *
+ * 去除数字和字母数字标识符,如电话号码、信用卡号、十六进制散列等,同时跳过年份和简单数字的实例
+ *
+ * @param text
+ * @return
+ */
+ public static String removeIdentifiers (String text) {
+ // 使用正则表达式匹配电话号码
+ text = removePhone(text);
+
+ // 使用正则表达式匹配信用卡号
+ text = removeCreditCard(text);
+
+ // 使用正则表达式匹配十六进制散列
+ text = removeHashMatcher(text);
+
+ // // 使用StringBuilder和StringBuilder的replace方法去除其他数字,但跳过年份和简单数字
+ // // TODO: 这里目前有bug,先注释掉了。
+ // StringBuilder sb = new StringBuilder(text);
+ // int index = 0;
+ // while ((index = findNextNumberToReplace(sb.toString())) != -1) {
+ // String number = sb.substring(index, findEndOfNumber(sb.toString(), index));
+ // if (!isYear(number) && !isSimpleNumber(number)) {
+ // sb.replace(index, index + number.length(), "");
+ // }
+ // }
+ return text;
+ }
+
+
+ /**
+ * 去除电话号码
+ *
+ * @param text 文本
+ * @return 去除电话号码后的文本
+ */
+ private static String removePhone (String text) {
+ // 手机号码的正则表达式
+ Matcher mobileMatcher = MOBILE_PATTERN.matcher(text);
+ text = mobileMatcher.replaceAll("");
+
+ // 国内电话号码的正则表达式
+ Matcher domesticPhoneMatcher = DOMESTIC_PHONE_PATTERN.matcher(text);
+ text = domesticPhoneMatcher.replaceAll("");
+
+ // 电话号码(400)的正则表达式
+ Matcher phoneMatcher = PHONE_PATTERN.matcher(text);
+ text = phoneMatcher.replaceAll("");
+
+ // 热线电话格式的正则表达式
+ Matcher hotlinePhoneMatcher = HOTLINE_PATTERN.matcher(text);
+ text = hotlinePhoneMatcher.replaceAll("");
+
+ return text;
+ }
+
+ /**
+ * 去除信用卡号
+ *
+ * @param text 文本
+ * @return 去除信用卡号后的文本
+ */
+ private static String removeCreditCard (String text) {
+ Matcher creditCardMatcher = CREDIT_CARD_PATTERN.matcher(text);
+ text = creditCardMatcher.replaceAll("");
+ return text;
+ }
+
+ /**
+ * 去除十六进制散列
+ *
+ * @param text 文本
+ * @return 去除十六进制散列后的文本
+ */
+ private static String removeHashMatcher (String text) {
+ Matcher hashMatcher = HASH_PATTERN.matcher(text);
+ text = hashMatcher.replaceAll("");
+ return text;
+ }
+
+ // 查找下一个要替换的数字的起始索引
+ private static int findNextNumberToReplace (String text) {
+ // 这里可以添加更复杂的逻辑来定位要替换的数字,但为了简化,我们假设数字以空格或非数字字符分隔
+ for (int i = 0; i < text.length(); i++) {
+ char c = text.charAt(i);
+ if (Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
+ // 找到数字的起始位置
+ while (i < text.length() && (Character.isDigit(text.charAt(i)) ||
+ (text.charAt(i) >= 'a' && text.charAt(i) <= 'f') ||
+ (text.charAt(i) >= 'A' && text.charAt(i) <= 'F'))) {
+ i++;
+ }
+ // 返回数字的起始索引(减1,因为我们要在循环外部处理i的递增)
+ return i - 1 > 0 ? i - 1 : 0;
+ }
+ }
+ return -1; // 没有找到要替换的数字
+ }
+
+ // 找到数字的结束索引
+ private static int findEndOfNumber (String text, int startIndex) {
+ // 从startIndex开始向后查找,直到遇到非数字字符
+ for (int i = startIndex; i < text.length(); i++) {
+ if (!(Character.isDigit(text.charAt(i)) ||
+ (text.charAt(i) >= 'a' && text.charAt(i) <= 'f') ||
+ (text.charAt(i) >= 'A' && text.charAt(i) <= 'F'))) {
+ return i;
+ }
+ }
+ return text.length(); // 如果字符串以数字结束,则返回字符串的长度
+ }
+
+ // 检查一个字符串是否是年份
+ private static boolean isYear (String str) {
+ try {
+ int year = Integer.parseInt(str);
+ Year y = Year.parse(str, YEAR_FORMAT);
+ return YEARS_TO_SKIP.contains(str);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ // 检查一个字符串是否是简单数字(这里假设不超过六位的连续数字)
+ private static boolean isSimpleNumber (String str) {
+ try {
+ int number = Integer.parseInt(str);
+ return String.valueOf(number).equals(str) && number >= 0 && number < 1000000;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ public static void main (String[] args) {
+ String textWithIdentifiers = "Here are some identifiers: 123-456-7890, 1234567812345678, a1b2c3d4e5f6a1b2c3d4e5f6, 2023, and 987654.";
+ // 去除标识符
+ String textWithoutIdentifiers = removeIdentifiers(textWithIdentifiers);
+ // 打印结果
+ log.info(textWithoutIdentifiers);
+
+ // String traditionalText = "不經意,妳的笑容";
+ // String simplifiedText = traditionalToSimplified(traditionalText);
+ //
+ // log.info("繁体文本: [" + traditionalText + "]");
+ // log.info("简体文本: [" + simplifiedText + "]");
+ //String dirtyString="?��简体文���f?�G��?��??�G�G��پ?�l?,,,杩欐槸涓€涓\\uE043贡鐮";
+ // // 先进行编码转换
+ // dirtyString = convertEncoding(dirtyString);
+ // // 再进行乱码和无意义 Unicode 字符的清理
+ // String cleanString = clean(dirtyString);
+ //// String s1 = removeNonPrintableUnicodeChars(s);
+ // log.info("去除乱码:[{}]", cleanString);
+ }
+
+ public static String clean (String input) {
+ // 更广泛的乱码字符范围,包括一些扩展的不可打印字符
+ String cleanString = input.replaceAll("[\\x00-\\x1F\\x7F-\\x9F\\uFFFD]", "");
+ // 去除无意义的 Unicode 字符,这里范围可根据实际情况修改
+ cleanString = cleanString.replaceAll("[\\uE000-\\uF8FF]", "");
+ return cleanString;
+ }
+
+ public static String convertEncoding (String input) {
+ // 尝试多种编码转换,找到正确的编码
+ String[] encodings = {"UTF-8", "GBK", "Big5", "ISO-8859-1"};
+ for (String encoding : encodings) {
+ try {
+ byte[] bytes = input.getBytes(encoding);
+ String result = new String(bytes, StandardCharsets.UTF_8);
+ return result;
+ } catch (Exception e) {
+ // 编码转换失败,继续尝试下一个编码
+ continue;
+ }
+ }
+ return input;
+ }
+ }
+
diff --git a/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/HammingUtils.java b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/HammingUtils.java
new file mode 100644
index 000000000..2a744a3ca
--- /dev/null
+++ b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/HammingUtils.java
@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.mdpf.util;
+
+import com.hankcs.hanlp.HanLP;
+import lombok.extern.slf4j.Slf4j;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+public class HammingUtils {
+
+ // ======================== 新增方法 ========================
+ /**
+ * 短文本处理逻辑(按字符拆分)
+ */
+ private static List handleShortText(String str) {
+ List result = new ArrayList<>();
+ for (char c : str.toCharArray()) {
+ result.add(String.valueOf(c));
+ }
+ return result;
+ }
+
+ // ======================== 原始方法(优化后) ========================
+ public static String getHash(String str) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] hash = md.digest(str.getBytes(StandardCharsets.UTF_8));
+ return new BigInteger(1, hash).toString(2);
+ } catch (Exception e) {
+ log.error("Hash计算失败: {}", e.getMessage());
+ return str; // 降级处理
+ }
+ }
+
+ public static String getSimHash(String str) {
+ int[] v = new int[128];
+ // 修复点:调用已定义的handleShortText方法
+ List keywords = str.length() < 200 ?
+ handleShortText(str) :
+ HanLP.extractKeyword(str, str.length());
+
+ for (int i = 0; i < keywords.size(); i++) {
+ String keywordHash = getHash(keywords.get(i));
+ // 补全128位
+ keywordHash = String.format("%128s", keywordHash)
+ .replace(' ', '0')
+ .substring(0, 128);
+
+ int weight = 10 - (i / (keywords.size() / 10));
+ for (int j = 0; j < 128; j++) {
+ v[j] += (keywordHash.charAt(j) == '1') ? weight : -weight;
+ }
+ }
+
+ StringBuilder simHash = new StringBuilder();
+ for (int bit : v) {
+ simHash.append(bit > 0 ? "1" : "0");
+ }
+ return simHash.toString();
+ }
+
+ public static int getHammingDistance(String hash1, String hash2) {
+ if (hash1.length() != hash2.length()) {
+ return -1;
+ }
+ int distance = 0;
+ for (int i = 0; i < hash1.length(); i++) {
+ if (hash1.charAt(i) != hash2.charAt(i)) {
+ distance++;
+ }
+ }
+ return distance;
+ }
+
+ public static double getSimilarity(String hash1, String hash2) {
+ int distance = getHammingDistance(hash1, hash2);
+ return 1.0 - (double) distance / 128; // 标准化到[0,1]
+ }
+}
diff --git a/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/HttpURLConnectionUtil.java b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/HttpURLConnectionUtil.java
new file mode 100644
index 000000000..4ecaf3b0f
--- /dev/null
+++ b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/HttpURLConnectionUtil.java
@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.mdpf.util;
+
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class HttpURLConnectionUtil {
+ public static HttpURLConnection readFile (String filePath) {
+ try {
+ URL url = new URL(filePath);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+
+ if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ return connection;
+ } else {
+ System.out.println("Failed to fetch file. Server returned HTTP code: " + connection.getResponseCode());
+ }
+ connection.disconnect();
+ } catch (Exception e) {
+ System.out.println("Error fetching file from URL: " + e.getMessage());
+ }
+ return null;
+ }
+}
diff --git a/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/ParserUtils.java b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/ParserUtils.java
new file mode 100644
index 000000000..241b836cb
--- /dev/null
+++ b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/ParserUtils.java
@@ -0,0 +1,44 @@
+package cn.iocoder.yudao.module.mdpf.util;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 文件解析通用辅助工具类,提供创建文本片段Map等功能。
+ */
+@Component
+@Slf4j
+public class ParserUtils {
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ /**
+ * 创建一个包含原始文本和元数据的 Map。
+ * 这个 Map 将作为中间数据结构,传递给 TextProcessor。
+ */
+ public Map createSegmentMap(String datasetMetaId, String originalMinioPath,
+ String fileExtension, String extractedText,
+ Map sourceSpecificMetadata, LocalDateTime processTime,
+ String segmentType) {
+ Map segmentMap = new HashMap<>();
+ segmentMap.put("id", UUID.randomUUID().toString()); // 临时ID,便于在内存中追踪或作为MySQL的rawTextSegmentMongoId字段
+ segmentMap.put("datasetMetaId", datasetMetaId);
+ segmentMap.put("originalMinioPath", originalMinioPath);
+ segmentMap.put("sourceFileExtension", fileExtension);
+ segmentMap.put("extractedText", extractedText != null ? extractedText : "");
+ segmentMap.put("sourceSpecificMetadata", sourceSpecificMetadata != null ? sourceSpecificMetadata : Collections.emptyMap());
+ segmentMap.put("extractTime", processTime);
+ segmentMap.put("segmentType", segmentType);
+ return segmentMap;
+ }
+}
\ No newline at end of file
diff --git a/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/TextCleaningUtil.java b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/TextCleaningUtil.java
new file mode 100644
index 000000000..5d96c0721
--- /dev/null
+++ b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/TextCleaningUtil.java
@@ -0,0 +1,158 @@
+package cn.iocoder.yudao.module.mdpf.util;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 文本清洗辅助工具类
+ */
+public class TextCleaningUtil {
+
+ // 简单HTML标签去除
+ private static final Pattern HTML_TAG_PATTERN = Pattern.compile("<[^>]*>");
+ // 简单Markdown格式去除 (粗体、斜体、链接、图片等)
+ private static final Pattern MARKDOWN_PATTERN = Pattern.compile("(\\*\\*|__)(.*?)\\1|(\\*|_)(.*?)\\3|\\[(.*?)\\]\\((.*?)\\)|!\\((.*?)\\)\\[(.*?)\\]");
+ // 简单的邮箱和电话号码识别 (用于PII匿名化)
+ private static final Pattern EMAIL_PII_PATTERN = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}");
+ private static final Pattern PHONE_PII_PATTERN = Pattern.compile("\\d{3}[-\\s]?\\d{3}[-\\s]?\\d{4}|\\(\\d{3}\\)[-\\s]?\\d{3}[-\\s]?\\d{4}");
+
+
+ /**
+ * 规范化空白字符:将多个空格、制表符、换行符替换为单个空格,并去除首尾空白。
+ */
+ public static String normalizeWhitespace(String text) {
+ if (text == null) return null;
+ return text.replaceAll("\\s+", " ").trim();
+ }
+
+ /**
+ * 规范化标点符号:将全角标点转半角,统一常见标点,去除重复标点等。
+ * 这是一个简化版,实际可能需要更复杂的规则或第三方库。
+ */
+ public static String normalizePunctuation(String text) {
+ if (text == null) return null;
+ // 修复:移除Java中不支持的命名参数 (target: replacement:)
+ String result = text.replace(",", ",")
+ .replace("。", ".")
+ .replace("!", "!")
+ .replace("?", "?");
+
+ // 修复:String.replaceAll 不支持 lambda 表达式作为替换字符串。
+ // 需要使用 Pattern 和 Matcher 显式处理来保留重复标点的第一个字符。
+ Pattern p = Pattern.compile("[\\.,!?;]{2,}"); // 匹配两个或更多连续的 .,!?; 标点符号
+ Matcher m = p.matcher(result);
+ StringBuffer sb = new StringBuffer(); // 用于构建替换后的字符串
+ while (m.find()) {
+ // 对于每个匹配项,替换为该匹配项的第一个字符
+ m.appendReplacement(sb, Matcher.quoteReplacement(m.group().substring(0, 1)));
+ }
+ m.appendTail(sb); // 将匹配后的剩余部分追加到StringBuffer
+ return sb.toString();
+ }
+
+ /**
+ * 去除HTML标签。
+ */
+ public static String removeHtmlTags(String text) {
+ if (text == null) return null;
+ return HTML_TAG_PATTERN.matcher(text).replaceAll("");
+ }
+
+ /**
+ * 去除常见的Markdown格式。
+ */
+ public static String removeMarkdownFormatting(String text) {
+ if (text == null) return null;
+ return MARKDOWN_PATTERN.matcher(text).replaceAll("$2$4$5$7"); // 替换为捕获组中的内容
+ }
+
+ /**
+ * 简单匿名化 PII (个人身份信息),例如邮箱和电话号码。
+ * 返回一个包含清洗后文本和是否包含 PII 的 Map。
+ */
+ public static Map anonymizePii(String text) {
+ HashMap map = new HashMap<>();
+ boolean hasPii = false;
+ if (text == null) {
+ // 修正:移除 'new *' 冗余行
+ return new HashMap() {{
+ put("text", null);
+ put("has_pii", false);
+ }};
+ }
+
+ Matcher emailMatcher = EMAIL_PII_PATTERN.matcher(text);
+ // 修正:移除命名参数 'replacement:'
+ if (emailMatcher.find()) {
+ text = emailMatcher.replaceAll("[EMAIL_REDACTED]");
+ hasPii = true;
+ }
+
+ Matcher phoneMatcher = PHONE_PII_PATTERN.matcher(text);
+ // 修正:移除命名参数 'replacement:'
+ if (phoneMatcher.find()) {
+ text = phoneMatcher.replaceAll("[PHONE_REDACTED]");
+ hasPii = true;
+ }
+
+ map.put("text",text);
+ map.put("has_pii",hasPii);
+ // 修正:移除 'new *' 冗余行
+ return map;
+ }
+
+ /**
+ * 检查文本是否包含敏感词。
+ */
+ public static boolean containsSensitiveWords(String text, List sensitiveWords) {
+ if (text == null || sensitiveWords == null || sensitiveWords.isEmpty()) {
+ return false;
+ }
+ String lowerText = text.toLowerCase();
+ for (String word : sensitiveWords) {
+ if (lowerText.contains(word.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 简单计算文本质量得分 (占位符)。
+ * 实际可能基于:可读性指数(Flesch-Kincaid),语法正确性API,内容相关性等。
+ */
+ public static Double calculateQualityScore(String text) {
+ if (text == null || text.trim().isEmpty()) {
+ return 0.0;
+ }
+ // 示例:基于文本长度和非标点字符比例的简单评分
+ int length = text.length();
+ long alphaNumericCount = text.chars().filter(Character::isLetterOrDigit).count();
+ if (length == 0) return 0.0;
+ // 假设长度越长,字母数字占比越高,质量越高
+ return Math.min(1.0, (double) alphaNumericCount / length + (double) length / 500.0); // 简单示例
+ }
+
+ /**
+ * 简单计算文本的Token数量 (占位符)。
+ * 实际可能需要调用大模型分词器,如 SentencePiece, BPE 等。
+ * 这里用空格分割词语来粗略估计。
+ */
+ public static Integer countTokens(String text) {
+ if (text == null || text.trim().isEmpty()) {
+ return 0;
+ }
+ // 简单的空格分词
+ return text.split("\\s+").length;
+ }
+
+ // TODO: 实现更复杂的文本处理功能,例如:
+ // - 语言检测 (使用 Apache Tika, Lingua 等库)
+ // - 关键词提取
+ // - 实体识别
+ // - 文本摘要
+}
\ No newline at end of file
diff --git a/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/TextProcessor.java b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/TextProcessor.java
new file mode 100644
index 000000000..600723271
--- /dev/null
+++ b/yudao-module-mdpf/yudao-module-mdpf-biz/src/main/java/cn/iocoder/yudao/module/mdpf/util/TextProcessor.java
@@ -0,0 +1,127 @@
+package cn.iocoder.yudao.module.mdpf.util;
+
+import cn.iocoder.yudao.module.mdpf.dal.dataobject.dataset.DataSetFileMiddleDO;
+import cn.iocoder.yudao.module.mdpf.util.TextCleaningUtil; // 此导入现在相对于新包是正确的
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.DigestUtils;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 文本处理器,负责对原始文本片段进行深度清洗、质量评估和格式转换。
+ */
+@Component
+@Slf4j
+public class TextProcessor {
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ /**
+ * 清洗单个原始文本片段并评估其质量。
+ *
+ * @param rawSegmentMap 原始文本片段信息 (来自解析策略的 Map)
+ * @param datasetId 关联的 DataSetMiddleDO 的 ID (MySQL 主表 ID)
+ * @param sourceFileId 关联的 DataSetMiddleMongoDO 的 ID (MongoDB 元数据 ID)
+ * @return 清洗后的 DataSetFileMiddleDO 实体,如果文本质量过低或被过滤则返回 null
+ */
+ public DataSetFileMiddleDO cleanAndEvaluate(Map rawSegmentMap, Long datasetId, Long sourceFileId) {
+ // 从 Map 中提取原始文本片段的各项信息
+ String originalMinioPath = (String) rawSegmentMap.get("originalMinioPath");
+ String fileExtension = (String) rawSegmentMap.get("sourceFileExtension");
+ String extractedText = (String) rawSegmentMap.get("extractedText");
+ Map sourceSpecificMetadata = (Map) rawSegmentMap.get("sourceSpecificMetadata");
+
+ // 从 additionalMetadata (现在是 sourceSpecificMetadata 的一部分) 中获取新字段
+ String dataSetFileUrl = (String) sourceSpecificMetadata.get("dataSetFileUrl");
+ String dataSetFileType = (String) sourceSpecificMetadata.get("dataSetFileType");
+ String datasetFileName = (String) sourceSpecificMetadata.get("datasetFileName");
+ String sourceFileName = (String) sourceSpecificMetadata.get("sourceFileName");
+
+
+ if (!StringUtils.hasText(extractedText)) {
+ log.warn("Skipping null or empty extracted text for datasetId: {}, Source File ID: {}", datasetId, sourceFileId);
+ return null;
+ }
+
+ String cleanedText = extractedText;
+ StringBuilder remarks = new StringBuilder(); // 仅用于日志,不存入 DB
+
+ // --- 深度清洗步骤 (使用 TextCleaningUtil) ---
+ cleanedText = TextCleaningUtil.normalizeWhitespace(cleanedText);
+ cleanedText = TextCleaningUtil.normalizePunctuation(cleanedText);
+ cleanedText = TextCleaningUtil.removeHtmlTags(cleanedText);
+ cleanedText = TextCleaningUtil.removeMarkdownFormatting(cleanedText);
+ Map piiResult = TextCleaningUtil.anonymizePii(cleanedText);
+ cleanedText = (String) piiResult.get("text");
+ if ((Boolean) piiResult.get("has_pii")) {
+ remarks.append("包含PII,已匿名化; ");
+ }
+// if (TextCleaningUtil.containsSensitiveWords(cleanedText,null)) { // <-- 这里的调用现在应该能正确解析了
+// cleanedText = TextCleaningUtil.filterSensitiveWords(cleanedText); // <-- 这里的调用现在应该能正确解析了
+// remarks.append("包含敏感词,已过滤; ");
+// }
+
+ // --- 质量评估与过滤 ---
+ Double qualityScoreDouble = TextCleaningUtil.calculateQualityScore(cleanedText);
+ BigDecimal qualityScore = BigDecimal.valueOf(qualityScoreDouble); // 转换为 BigDecimal
+ Integer tokenCount = TextCleaningUtil.countTokens(cleanedText);
+
+ DataSetFileMiddleDO cleanedFileDO = new DataSetFileMiddleDO();
+
+ // 填充来自 DataSetMiddleServiceImpl 传递的 ID
+ cleanedFileDO.setDataSetId(datasetId);
+ cleanedFileDO.setSourceFileId(sourceFileId); // 对应 MongoDB 元数据 ID
+
+ // 填充来自文件元数据的字段
+ cleanedFileDO.setDataSetFileUrl(dataSetFileUrl);
+ cleanedFileDO.setDataSetFileType(dataSetFileType);
+ cleanedFileDO.setDatasetFileName(datasetFileName);
+ cleanedFileDO.setSourceFileUrl(originalMinioPath); // originalMinioPath 对应 source_file_url
+ cleanedFileDO.setSourceFileName(sourceFileName);
+ cleanedFileDO.setSourceFileExtension(fileExtension);
+
+ // 填充清洗后的文本和相关度量
+ cleanedFileDO.setCleanedText(cleanedText);
+ cleanedFileDO.setCleanedTextHash(DigestUtils.md5DigestAsHex(cleanedText.getBytes(StandardCharsets.UTF_8)));
+ cleanedFileDO.setQualityScore(qualityScore);
+ cleanedFileDO.setTokenCount(tokenCount);
+ cleanedFileDO.setCleanTime(LocalDateTime.now()); // 清洗时间是当前时间
+
+ // 最终过滤逻辑:文本太短或质量分过低
+// if (!StringUtils.hasText(cleanedText) || tokenCount <= 10 || qualityScore.compareTo(BigDecimal.valueOf(0.2)) < 0) {
+// log.warn("Filtered out text segment due to final quality check (datasetId: {}, sourceFileId: {}), remarks: {}", datasetId, sourceFileId, remarks.toString());
+// return null; // 返回 null 表示该片段被过滤
+// }
+
+ return cleanedFileDO;
+ }
+
+ /**
+ * 批量清洗原始文本片段列表。
+ *
+ * @param rawSegments 原始文本片段信息列表
+ * @param datasetId 关联的 DataSetMiddleDO 的 ID (MySQL 主表 ID)
+ * @param sourceFileId 关联的 DataSetMiddleMongoDO 的 ID (MongoDB 元数据 ID)
+ * @return 清洗后的 DataSetFileMiddleDO 实体列表
+ */
+ public List cleanAndEvaluateList(List