""" 文档预处理/清洗服务 参考 Dify 的 DatasetProcessRule,支持可配置的文本清洗规则。 在文本提取之后、分块之前执行。 """ import logging import re from typing import Dict, Any, List, Optional logger = logging.getLogger(__name__) # 默认预处理规则 DEFAULT_PROCESS_RULES: Dict[str, Any] = { "pre_processing_rules": [ {"id": "remove_extra_spaces", "enabled": True}, {"id": "remove_urls_emails", "enabled": False}, {"id": "remove_html_tags", "enabled": False}, {"id": "remove_consecutive_newlines", "enabled": True}, {"id": "remove_trailing_whitespace", "enabled": True}, ], } class CleaningService: """ 文本清洗服务 支持的清洗规则: - remove_extra_spaces: 合并连续空格为单个空格 - remove_urls_emails: 移除 URL 和邮箱地址 - remove_html_tags: 移除 HTML 标签 - remove_consecutive_newlines: 合并连续空行(3+)为双空行 - remove_trailing_whitespace: 去除行尾空白 """ # 规则处理器映射 RULE_PROCESSORS = { "remove_extra_spaces": "_remove_extra_spaces", "remove_urls_emails": "_remove_urls_emails", "remove_html_tags": "_remove_html_tags", "remove_consecutive_newlines": "_remove_consecutive_newlines", "remove_trailing_whitespace": "_remove_trailing_whitespace", } @classmethod def clean(cls, text: str, process_rules: Optional[Dict[str, Any]] = None) -> str: """ 根据预处理规则清洗文本 Args: text: 原始文本 process_rules: 预处理规则配置,为 None 则使用默认规则 Returns: 清洗后的文本 """ if not text: return text rules = process_rules or DEFAULT_PROCESS_RULES pre_rules = rules.get("pre_processing_rules", []) original_length = len(text) for rule in pre_rules: rule_id = rule.get("id", "") enabled = rule.get("enabled", False) if not enabled: continue processor_name = cls.RULE_PROCESSORS.get(rule_id) if not processor_name: logger.warning(f"未知的预处理规则: {rule_id}") continue processor = getattr(cls, processor_name, None) if processor: text = processor(text) cleaned_length = len(text) if original_length != cleaned_length: logger.info( f"文本清洗完成: {original_length} -> {cleaned_length} 字符 " f"(减少 {original_length - cleaned_length})" ) return text.strip() @staticmethod def _remove_extra_spaces(text: str) -> str: """合并连续空格为单个空格(保留换行符)""" # 只处理同一行内的连续空格,不影响换行 lines = text.split('\n') cleaned_lines = [] for line in lines: cleaned_lines.append(re.sub(r'[ \t]+', ' ', line)) return '\n'.join(cleaned_lines) @staticmethod def _remove_urls_emails(text: str) -> str: """移除 URL 和邮箱地址""" # 移除 URL text = re.sub( r'https?://[^\s<>"{}|\\^`\[\]]+', '', text, ) # 移除邮箱 text = re.sub( r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', '', text, ) return text @staticmethod def _remove_html_tags(text: str) -> str: """移除 HTML 标签,保留文本内容""" # 移除 script 和 style 标签及其内容 text = re.sub(r']*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) text = re.sub(r']*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) # 移除所有 HTML 标签 text = re.sub(r'<[^>]+>', '', text) # 解码常见 HTML 实体 text = text.replace(' ', ' ') text = text.replace('<', '<') text = text.replace('>', '>') text = text.replace('&', '&') text = text.replace('"', '"') text = text.replace(''', "'") return text @staticmethod def _remove_consecutive_newlines(text: str) -> str: """合并连续空行(3个以上换行)为双换行""" return re.sub(r'\n{3,}', '\n\n', text) @staticmethod def _remove_trailing_whitespace(text: str) -> str: """去除每行行尾空白""" return '\n'.join(line.rstrip() for line in text.split('\n')) @classmethod def get_default_rules(cls) -> Dict[str, Any]: """获取默认预处理规则""" return DEFAULT_PROCESS_RULES.copy() @classmethod def get_available_rules(cls) -> List[Dict[str, str]]: """获取所有可用的预处理规则""" return [ {"id": "remove_extra_spaces", "label": "合并连续空格"}, {"id": "remove_urls_emails", "label": "移除 URL 和邮箱"}, {"id": "remove_html_tags", "label": "移除 HTML 标签"}, {"id": "remove_consecutive_newlines", "label": "合并连续空行"}, {"id": "remove_trailing_whitespace", "label": "去除行尾空白"}, ]