Files
ai-agent-admin/backend-fastapi/ai_platform/knowledge/services/cleaning_service.py
T
2026-06-08 18:14:59 +08:00

160 lines
5.2 KiB
Python

"""
文档预处理/清洗服务
参考 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'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL | re.IGNORECASE)
# 移除所有 HTML 标签
text = re.sub(r'<[^>]+>', '', text)
# 解码常见 HTML 实体
text = text.replace('&nbsp;', ' ')
text = text.replace('&lt;', '<')
text = text.replace('&gt;', '>')
text = text.replace('&amp;', '&')
text = text.replace('&quot;', '"')
text = text.replace('&#39;', "'")
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": "去除行尾空白"},
]