avatar

一条在知识海洋的咸鱼

这个家伙很懒,啥也没有留下😋

  • 首页
  • Linux
  • OCJP
  • Java核心技术卷
  • J2EE相关标准
  • 深入理解Java虚拟机
  • NIO与SOcket编程技术指南
  • Java多线程编程核心技术
  • Redis开发与运维
  • Spring Cloud Alibaba 微服务原理与实践
  • DevOps
  • Docker
  • MySQL必知必会
  • AI自学路线
  • Spring Boot 编程思想(核心篇)
主页 19 文本清洗
文章

19 文本清洗

发表于 2026-06-13 更新于 2026-06- 13
作者 Administrator
32~41 分钟 阅读

文本清洗

文本清洗的核心是在不破坏原文结构的前提下,去掉明显噪声,保留标题、段落、页码和来源信息,让后面的切片和检索更稳定。

[TOC]

1. 学完本节要达到什么程度

前面已经把文档解析成文本。

但解析出来的文本通常不干净。

常见问题:

空行太多  
页眉页脚重复  
页码混在正文里  
PDF 换行很乱  
中英文空格异常  
连字符断词  
标题层级丢失  
来源信息丢失  

本节的目标是:

raw_text  
 -> cleaned_text  

不要追求一次清洗完美。

先做低风险、可解释、可回退的清洗。

2. 清洗原则

清洗最怕误删。

原则:

少做高风险改写  
优先处理明显噪声  
保留标题结构  
保留页码线索  
保留代码块  
保留原始 raw_text清洗过程可重复执行  

不要把清洗写成:

一堆看不懂的正则  

每个规则都应该能解释。

3. raw_text 和 cleaned_text 都要保存

数据库里建议保留:

raw_text:解析原文  
cleaned_text:清洗结果  

原因:

清洗错了可以回退  
能比较前后效果  
方便排查 RAG 质量问题  
不同策略可以重新清洗  

不要覆盖原始解析文本。

4. 清洗流程

建议顺序:

统一换行  
去除不可见字符  
压缩连续空行  
修复 PDF 断行  
去除明显页码  
去除重复页眉页脚  
保留标题结构  
生成清洗报告  

清洗流水线:

graph LR;  
 A["raw_text<br/>解析原文"] --> B["统一换行"];  
 B --> C["去不可见字符"];  
 C --> D["压缩连续空行"];  
 D --> E["修复 PDF 断行"];  
 E --> F["去明显页码"];  
 F --> G["去重复页眉页脚"];  
 G --> H["保留标题结构"];  
 H --> I["cleaned_text<br/>清洗结果"];  
 I --> J["清洗报告<br/>记录修改次数和策略"];  

其中“修复断行”和“去页眉页脚”风险稍高。

第一版可以做得保守一点。

5. 统一换行

不同系统换行不同:

\r\n  
\r  
\n  

先统一成 \n:

def normalize_newlines(text: str) -> str:  
 return text.replace("\r\n", "\n").replace("\r", "\n")```  
  
这是低风险规则。  
  
几乎所有文本都可以做。  
  
## 6. 去不可见字符  
  
有些 PDF 会解析出奇怪字符。  
  
可以先去掉部分控制字符:  
  
```python  
import re  
  
  
def remove_control_chars(text: str) -> str:  
 return re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)```  
  
注意不要删 `\n` 和 `\t`。  
  
换行和缩进可能有结构意义。  
  
## 7. 压缩空行  
  
空行太多会影响切片。  
  
可以把 3 个以上连续换行压成 2 个:  
  
```python  
import re  
  
  
def collapse_blank_lines(text: str) -> str:  
 return re.sub(r"\n{3,}", "\n\n", text)```  
  
保留两个换行,是为了保留段落边界。  
  
不要压成一个换行。  
  
## 8. 去除行首行尾空格  
  
```python  
def strip_lines(text: str) -> str:  
 lines = [line.strip() for line in text.split("\n")] return "\n".join(lines)```  
  
这条规则简单,但要注意代码块。  
  
如果文档里有大量代码,缩进可能有意义。  
  
Markdown 文档可以选择保守处理:  
  
```text  
代码块内不 strip代码块外 strip  

9. 保留 Markdown 代码块

清洗 Markdown 时,代码块要小心。

可以先识别代码块状态:

def strip_lines_keep_code(text: str) -> str:  
 result = [] in_code_block = False  
 for line in text.split("\n"): if line.strip().startswith("```"): in_code_block = not in_code_block result.append(line.rstrip()) continue  
 if in_code_block: result.append(line.rstrip()) else: result.append(line.strip())  
 return "\n".join(result)```  
  
代码块里不做激进清洗。  
  
否则示例代码可能被破坏。  
  
## 10. 修复 PDF 断行  
  
PDF 经常把一段话解析成多行:  
  
```text  
RAG 的核心是先检索相关内容  
再把检索结果作为上下文交给模型  
生成最终回答  

有时可以合并成:

RAG 的核心是先检索相关内容再把检索结果作为上下文交给模型生成最终回答  

但合并断行有风险。

标题、列表、表格不应该乱合并。

保守策略:

def merge_soft_line_breaks(text: str) -> str:  
 lines = text.split("\n") result = [] buffer = ""  
 for line in lines: stripped = line.strip()  
 if not stripped: if buffer: result.append(buffer) buffer = "" result.append("") continue  
 if stripped.startswith(("#", "-", "*", "1.", "[PAGE")): if buffer: result.append(buffer) buffer = "" result.append(stripped) continue  
 if buffer: buffer += stripped else: buffer = stripped  
 if stripped.endswith(("。", "!", "?", ".", "!", "?", ":", ":")):  
 result.append(buffer) buffer = ""  
 if buffer: result.append(buffer)  
 return "\n".join(result)```  
  
这不是完美算法。  
  
它只是一个学习阶段的保守起点。  
  
## 11. 去页码  
  
解析 PDF 后可能出现:  
  
```text  
1  
2  
第 3 页  
Page 4  

可以删除单独成行的页码:

import re  
  
  
def remove_page_numbers(text: str) -> str:  
 lines = []  
 for line in text.split("\n"): stripped = line.strip()  
 if re.fullmatch(r"\d{1,4}", stripped): continue  
 if re.fullmatch(r"第\s*\d{1,4}\s*页", stripped):  
 continue  
 if re.fullmatch(r"Page\s+\d{1,4}", stripped, flags=re.IGNORECASE): continue  
 lines.append(line)  
 return "\n".join(lines)```  
  
注意:  
  
```text  
不要删除正文里的数字  
只删除单独成行的页码  

12. 去重复页眉页脚

页眉页脚常常每页重复。

比如:

AI Agent 学习手册  

每页都出现。

可以统计重复行:

from collections import Counter  
  
  
def remove_repeated_lines(text: str, min_count: int = 3) -> str:  
 lines = text.split("\n") counter = Counter(line.strip() for line in lines if line.strip())  
 repeated = { line for line, count in counter.items() if count >= min_count and len(line) <= 80 }  
 result = [ line for line in lines if line.strip() not in repeated ]  
 return "\n".join(result)```  
  
这条规则有风险。  
  
如果某句话在正文里重复多次,也可能被删。  
  
所以第一版可以只对 PDF 使用,并且把删除的行记录到 cleaning report。  
  
## 13. 保留标题  
  
标题对 RAG 很重要。  
  
它能帮助后面按结构切片。  
  
Markdown 标题:  
  
```text  
# 一级标题  
## 二级标题  
### 三级标题  

不要清洗掉 #。

DOCX 如果能识别标题样式,可以在解析阶段转成:

# 标题 1## 标题 2  

PDF 如果标题结构不好识别,第一版先不强求。

后面可以通过字体大小、目录、规则等方式增强。

14. 清洗报告

建议每次清洗返回一个报告:

class CleaningReport(BaseModel):  
 original_length: int cleaned_length: int removed_blank_lines: int removed_repeated_lines: list[str] rules: list[str]```  
  
报告的作用:  
  
```text  
知道清洗做了什么  
方便调试  
避免误删难以发现  

metadata 里也可以记录:

{  
 "cleaner": "basic-cleaner-v1", "rules": [ "normalize_newlines", "remove_control_chars", "collapse_blank_lines" ]}  

15. 完整清洗函数

示例:

def clean_text(raw_text: str, file_type: str) -> tuple[str, dict]:  
 text = raw_text rules = []  
 text = normalize_newlines(text) rules.append("normalize_newlines")  
 text = remove_control_chars(text) rules.append("remove_control_chars")  
 if file_type.lower() == "md": text = strip_lines_keep_code(text) rules.append("strip_lines_keep_code") else: text = strip_lines(text) rules.append("strip_lines")  
 if file_type.lower() == "pdf": text = remove_page_numbers(text) rules.append("remove_page_numbers")  
 text = collapse_blank_lines(text) rules.append("collapse_blank_lines")  
 report = { "cleaner": "basic-cleaner-v1", "original_length": len(raw_text), "cleaned_length": len(text), "rules": rules, }  
 return text, report```  
  
第一版先不要默认启用“去重复页眉页脚”和“合并软换行”。  
  
可以作为可选规则。  
  
## 16. Java / Python 协作  
  
清洗也可以放 Python 服务。  
  
接口:  
  
```text  
POST /api/rag/clean  

请求:

{  
 "documentId": "xxx", "rawText": "...", "fileType": "pdf"}  

响应:

{  
 "documentId": "xxx", "cleanedText": "...", "report": { "cleaner": "basic-cleaner-v1", "rules": [] }}  

Java 收到后写入:

rag_document_text.cleaned_text  
rag_document_text.metadata.cleaning  
rag_document.status = CLEANED  

17. 清洗前后对比

建议保存一个调试接口:

GET /api/documents/{documentId}/text  

返回:

{  
 "rawTextPreview": "...", "cleanedTextPreview": "...", "metadata": {}}  

不要一次返回超大全文给前端。

可以只返回前 2000 字。

调试时很有用。

18. 清洗策略的工程化管理

文本清洗会直接改变后续切片和检索结果,因此它应当像代码和数据库结构一样有版本,而不是散落在多个临时正则表达式里。

18.1 规则版本和幂等性

建议为每套清洗规则分配 cleanerVersion。同一份 raw_text 使用相同版本重复执行时,应得到相同结果;已经清洗过的文本再次清洗,也不应持续丢失内容。

修改断行、页眉页脚或空白规则后,不要直接覆盖而不留记录。应先用固定样本比较新旧版本,再决定是否批量重新清洗、切片和向量化。

18.2 可解释的清洗报告

清洗报告不仅记录“成功”,还应说明做了什么。建议统计原始字符数、清洗后字符数、删除行数、合并断行数、移除页眉页脚次数和异常字符数量。

如果文本缩短比例突然过高,应触发告警或人工复核。对于合同、制度和技术手册,误删一个编号、表格行或否定词,影响往往比保留少量噪声更严重。

18.3 回归样本

为 PDF、DOCX、Markdown 和 TXT 各准备一批代表性样本,覆盖中文标点、代码块、表格、页码、重复页眉和跨页断句。每次修改规则后比较关键段落是否仍然存在。

清洗质量的最低标准不是“看起来整齐”,而是信息不丢失、结构可识别、结果可复现,并且能支持下一步稳定切片。

19. 常见问题

19.1 清洗是不是越干净越好

不是。

清洗的目标是减少噪声。

不是重写原文。

过度清洗会破坏语义和结构。

19.2 页眉页脚一定要删吗

不一定。

如果它只是文档标题,保留也没问题。

如果它每页重复很多次,才会影响检索。

19.3 是否要用大模型清洗

第一版不建议。

用规则清洗更可控、成本更低、可重复。

复杂表格、扫描件、版面恢复可以后面再考虑模型辅助。

19.4 清洗失败怎么办

保留 raw_text。

把文档状态改为:

FAILED  

并记录 error_message。

不要让失败文本继续进入切片。

20. 练习清单

完成几件事情:

实现 normalize_newlines实现 remove_control_chars实现 collapse_blank_lines实现 strip_lines_keep_code实现 remove_page_numbers实现 clean_text返回 cleaning report把 cleaned_text 写入 rag_document_text更新 rag_document.status = CLEANED保留 raw_text 不覆盖  
能查看清洗前后预览  

建议目录:

ai-agent-study  
├── python  
│   └── rag-service  
│       ├── cleaners.py  
│       ├── schemas.py  
│       └── main.py  
├── java  
│   └── rag-api  
│       └── PythonCleanClient.java  
└── docs  
 └── text-cleaning.md  

21. 小结

本节的结论:

文本清洗要保守、可解释、可回退,重点是去掉明显噪声并保留结构。

最小链路:

raw_text  
 -> 基础规则清洗  
 -> cleaned_text -> cleaning report -> status=CLEANED  

清洗质量会直接影响后续切片质量,也会影响后面 RAG 检索效果。

参考资料

  • LangChain Text Splitters
  • LangChain Recursive Text Splitter
AI自学路线
AI
许可协议: 
分享

相关文章

7月 13, 2026

55 异常兜底:工具失败、非法输出、空检索与超时

异常兜底:工具失败、非法输出、空检索与超时 Agent 的可靠性主要体现在失败时。工ࠥ

7月 13, 2026

54 Agent Memory:短期记忆、用户上下文与摘要

Agent Memory:短期记忆、用户上下文与摘要 Memory 的目标是保持任务连续性,不是永久保存所有聊天。记忆Š

7月 13, 2026

53 Human-in-the-loop:让关键动作暂停、确认与恢复

Human-in-the-loop:让关键动作暂停、确认与恢复 HITL 不是弹一个“确定吗”,而是让工作流在关键动作前持久

下一篇

20 文档切片

上一篇

18 文档解析

最近更新

  • 55 异常兜底:工具失败、非法输出、空检索与超时
  • 54 Agent Memory:短期记忆、用户上下文与摘要
  • 53 Human-in-the-loop:让关键动作暂停、确认与恢复
  • 52 工单 Tool:字段校验、创建结果与失败补偿
  • 51 订单 Tool:Mock、鉴权、状态解析与脱敏

热门标签

java基础 微服务 maven Spring Tomcat DDD Linux Linux基础 SQL基础 数据结构算法

目录

©2026 一条在知识海洋的咸鱼. 保留部分权利。

使用 Halo 主题 Chirpy