我们做探矿业务的知识库,和网上那些示例项目最大的区别是:数据来源根本不是整齐划一的MD文件。TXT、Word、PDF、网页这四类来源,每一类都有自己的脾气。TXT可能是1998年用GBK编码存的钻孔数据,打开直接是乱码;Word报告里表格列宽发疯似地错位,公式还是微软公式编辑器时代的老物件;PDF一半是扫描的纸质报告,字是图片像素;网页资料就更别提了,正文里夹着几十个广告和无关链接。走完一遍清洗流程之后我得说,"清洗"这两个字才是RAG落地最硬核的环节,检索精度高不高,八成由清洗质量决定。
这篇东西,就围绕探矿业务场景,把TXT、Word、PDF和网页四类源文件的清洗思路、实操手法和翻车记录整理出来。如果你是做地质、矿业、勘探系统的研发,或者正在搭建行业RAG知识库,这篇东西可以直接当个参照系用。
1. 探矿领域的文档"底子"为什么这么难啃
1.1 四种源文件的真实生态
先说TXT。探矿行业资历深的从业者应该深有体会:大量实测数据以纯文本格式躺在老服务器里,编码千奇百怪。很多是GBK/GB2312,还有一批是早期繁体系统的Big5,甚至有一些是程序导出时编码混乱但尚未报错的"半乱码"文件。这些TXT往往没有统一分隔符,有的用制表符,有的用空格,有的干脆固定列宽。字段名可能是英文缩写,也可能是当年老专家的拼音简写。
Word的情况是另一个极端——格式丰富到让人头疼。探矿成果报告、储量估算说明、岩芯编录表,动辄上百页,里面有大量多级标题、嵌套表格、页眉页脚、域代码,还有插入的OLE公式对象。最麻烦的是表格:很多人用Word画表格根本不规范,单元格合并、拆分、空行都任着性子来,导致python-docx按行读出来的内容直接错位。
PDF在这个领域更野。二十世纪九十年代到二十世纪初的地质报告,很多是纸质扫描件,一本几百页,没做过OCR。就算后来转成电子版,也是扫描图片套了个PDF壳。另外还有一种"半文本"PDF:文字层存在但排版混乱,表格数据被切成碎片,专业术语里还夹着各种特殊字符(品位值符号、希腊字母等)。
网页来源同样不省心。政府公示的地质勘查成果、地调局的新闻公告、行业论坛的讨论帖、矿权交易信息,内容里夹杂大量导航、广告、推荐位、版权声明,抓下来不清理根本没法喂给分块器。更麻烦的是很多网页是动态渲染的,静态抓取只能拿到一堆脚本标签。
一句话概括:探矿业务资料不是"文档",是"地质历史遗产"。清洗它们,本质上是给RAG擦屁股。
1.2 清洗在RAG链路中的位置
我做一个不太严谨但很形象的类比:向量检索像"查字典",你要查一个词,得先把整本书的页码写对;清洗就是"编页码"这一步。原始文档里错位的表格、乱码的字段、扫描件里藏在图片里的文字,如果不清干净,切块器会把垃圾切进去,embedding模型会把垃圾内容编码进向量,检索引擎拿到的就是一堆语义噪音。
很多人一上来就调chunk_size、top_k、embedding模型,折腾半天效果不稳,其实根子在于上游没做清洗。我自己的经验是:清洗后的文档和原始文档,在同一个RAG链路下的检索命中率差距,可以用"数量级"来形容。清洗不是选做题,是必做题。
为了说明问题,我把清洗目标拆成三个层次:
- 第一层"去噪":去掉页眉页脚、导航链接、OCR识别噪声、表格错位数据,让文本内容干净。
- 第二层"结构化":给文本建立可解析的层次,给表格建立行列关系,给公式建立可索引的文本表达。这一步是为了让切块器知道"哪些信息应该被切进同一个块"。
- 第三层"规范化":统一编码、统一术语、统一格式(数字、单位、经纬度表达),让同一含义的实体在向量空间里距离更近。
后面每一个章节,我都会围绕这三个层次展开。
2. TXT清洗:编码是头一道生死关
2.1 乱码先从"猜编码"开始
TXT文件清洗的第一步永远不是清洗本身,而是猜测编码。探矿资料年代跨度太大,编码情况基本是"一锅乱炖":最常见的GBK,偶尔碰到UTF-8无BOM,如果文件来源于老款国产仪器软件,还可能是GB2312的变体。千万别凭扩展名或读前几个字节就下结论,工具上我一般用chardet做初判,再用iconv或者Python的open()做实测回读。
到这里必须补一个很多教程不会讲的细节:chardet对中文短文本的检测准确率并不理想,尤其是文件只有几百字节时,很容易把GBK误判成ISO-8859-1或者Windows-1252。所以稳妥做法是大文件抽样检测、小文件对明显中文特征做二次校验。比如GBK编码的汉字,字节范围一般落在0xB0-0xF7这个区间,这个规律用正则就能快速排查。
检测完编码,直接用下面的模板做转换:
import charset_normalizer from pathlib import Path def detect_and_decode(raw: bytes): guess = charset_normalizer.from_bytes(raw[:20000]).best() for enc in [guess.encoding, "utf-8", "gbk", "gb18030", "big5", "latin1"]: try: return raw.decode(enc), enc except (UnicodeDecodeError, LookupError): continue return raw.decode("gbk", errors="replace"), "gbk_fallback"这里我推荐charset_normalizer而不是chardet,它在中文场景下更稳一点。解码后再统一转成UTF-8写回,后续所有清洗流程都以UTF-8为中间格式。
2.2 探矿TXT数据的内容规整
编码问题解决后,真正的清洗才开始。探矿TXT里最常见的内容形态是三类:钻孔编录数据、化探元素分析结果、物探测量点位数据。这些TXT的水平高低,取决于当年导出程序写得规不规矩。
有的表头是中文,字段名是"孔号""样号""Cu""Pb""Zn""深度自""深度至",分隔符是空格;有的表头是拼音缩写,字段名是"KH""YH""Cu""Pb""Zn""SDZ""SDZ",分隔符是制表符;还有更狠的,一个固定列宽的文本文件,列与列之间全是空格,但不同文件空格数量不一致。
规整思路是:先把分隔符统一,再用pandas做表格化。常见套路是在read_csv时指定sep=None让pandas用python引擎自动推断分隔符,随后用str.split()和正则把脏列拆开。
实操里我习惯先写一个"字段感知清洗函数":
import pandas as pd import re def clean_geo_txt(path): with open(path, "r", encoding="utf-8", errors="replace") as f: lines = f.readlines() # 删除空行、行首行尾的杂志字符 lines = [re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", ln.strip()) for ln in lines] # 尝试自动识别分隔符 sample = "".join(lines[:50]) delim = "\t" if sample.count("\t") > sample.count(" ") else None df = pd.read_csv(path, sep=delim, engine="python", skipinitialspace=True) # 列名清理 df.columns = [re.sub(r"\s+", "_", c.strip()) for c in df.columns] return df这段代码不保证一次就对,特别是遇到以空格为分隔符但值本身有空格的情况,需要用正则按"多个空格"来切:
rows = [re.split(r"\s{2,}", ln) for ln in lines if ln.strip()]在这个场景里有个关键判断:字段之间到底是"一个或多个空格"还是"固定宽度"。前者用正则切割,后者用切片。拿不准的时候,统计前五行每行字符位置上的空格分布,基本能看出来规律。
2.3 乱码TXT的一种"抢救"思路
有些TXT损坏严重,编解码都救不回来。比如当年从软盘拷出来时字节就丢了,或者被某个程序以错误编码重写过一遍。这时候用再好的解码器都是徒劳。
我的经验做法是"局部抢救":如果文件里数字和英文字母可读,只是中文乱码,那么可以只保留ASCII可见字符和数字,把乱码区替换成占位符。探矿数据里数值才是核心,品位值、深度、坐标都是数字,中文标签丢了后面通过上下文还能推断。反过来,如果数字乱了但中文完整,说明编码错位了,尝试用错误编码的"重映射"技巧反向还原。
3. Word清洗:格式掩护下的信息丢失
3.1 用python-docx还是pandoc,这是个取舍
Word清洗的技术选择,我纠结了很久。python-docx能读到段落、表格、样式,但它对复杂公式、文本框、OLE对象基本无能为力;pandoc能把Word转成markdown,转换速度快、正文结构保真好,但表格和公式一样会丢;还有人用LibreOffice做转换,处理大文件经常卡死。
实际项目里我的策略是分场景:
- 纯文本型Word报告(储量计算说明、年度勘查总结):直接用pandoc转markdown,干净利落。转换后检查一遍图片引用是否变成附件路径,如果是就说明正文结构没问题。
- 表格密集的Word(钻孔编录表、样品分析表):必须用python-docx精细读取表格,每一步都校验行列数。
- 带公式的Word(物化探数据处理报告):公式对象转出来大概率是图片或空对象,得用公式识别管线单独处理。
3.2 表格、公式、页眉脚和不可见内容
Word表格是清洗的深水区。python-docx读表格时,如果表格里有合并单元格,你会在循环行时发现某些行比列数短或者多出空单元格。我在探矿报告里就遇到过"一行本来4列,因为竖向合并变成了3列+2个空单元格"的情况,直接按列取值会错位。
处理办法:读取表格前先检测每行的单元格数量,对不齐的用空字符串补齐,或者根据行列坐标进行"对齐重建"。列宽的问题也要注意,很多Word表格的列宽是显式指定的,但转换时会被忽略,导致内容顺序错乱。用python-docx读table.rows[i].cells时,列宽信息要另外取,不能依赖pandas自动对齐。
公式这块,探矿文档里最常出现的是地层对比公式、岩石化学参数计算公式和物探反演公式。Microsoft公式编辑器(OLE对象)在python-docx里只能看到一个w:object节点,里面基本是空的。我的做法是先用Word内置脚本或LibreOffice宏把公式转成MathML或LaTeX,再用解析器转成文本。如果公式是以图片形式插入的,那就得走OCR管线,用公式识别模型(比如Mathpix之类的API)转成LaTeX。探矿文档里公式不多,做得粗一点也够用。
页眉页脚和隐藏文字也千万别放过。很多Word报告把"编制单位""提交时间""密级"放在页眉里,把辅助说明放在批注里,这些信息切块时如果不清理,要么污染正文,要么被切块器当正文切进去造成重复。
一个很实用的土办法:转好的Markdown文件里搜索出现频率特别高的短语,比如"第X页""共X页""勘查单位",如果这些词频繁出现,说明页眉页脚混入正文了,得回到Word源文件把页眉页脚区域单独剥离,再重新转换。
3.3 宏、域代码和程序生成文档
探矿系统的Word报告很多不是人工敲的,是程序生成的。比如用C#和VB写的自动出报告工具,往Word模板里填数据。这种文档有一个特点:所有内容被放在各种域代码、表格嵌套和书签里,转出的Markdown经常是空壳。我在清洗时就遇到过:一个40页的报告转出来只有3页,原因就是正文全在文本框里。
另外Word宏也是个安全隐患。有的老报告模板带宏,转换工具打开时弹安全警告,一不留意就中断了。清洗管线里我一般直接禁用宏、只读模式打开,避免宏代码被触发。如果要用LibreOffice转换,加一个--no-macro参数更稳妥。
4. PDF清洗:扫描件和复杂版面的破解
4.1 先分清楚文本PDF还是扫描PDF
PDF清洗第一步永远是"分类型"。方法很简单:用PyMuPDF(fitz)抽文本,如果第1页到第3页能抽出的有效文本超过50个字符,那大概率是文本型PDF;否则很可能是扫描件。
文本型PDF也有自己的坑。很多地质报告是从Word转成PDF,再用虚拟打印机打印的,文字层是有了,但版式信息丢了。表格线还在,可表格内容已经被切成一行行散落的文字,读出来完全看不出表格结构。还有一些是早期用方正书版排版的PDF,字符映射混乱,直接抽取出来的文字是乱码,或者字序颠倒。这种情况我会在抽取之后做一次"文本重排序",用版面坐标信息把文字按阅读顺序组装回去。
扫描PDF就得上OCR。选型上我有几个工具可以给参考:PaddleOCR中文识别质量高、对小字号和生僻字(比如矿物的"铌钽铁矿"这类汉字)支持不错;Tesseract老牌但中文精度差点,适合英文缩写较多的物化探数据;商业API识别率最高,但涉及业务数据外发时要评估合规性。探矿数据很多涉密,OCR工具选型要先过信息安全这关,不能图方便直接全量上传云端。
4.2 版面分析、OCR和表格重建
OCR扫描PDF的完整流程是:扫描图→图像预处理→版面分析→文字识别→结构重建。直接整本OCR是下策,先做版面分析能大幅提升表格识别效果。版面分析说白了就是把页面分成"标题区""正文区""表格区""图片区"几个区域,对表格区单独走表格识别,对正文区走普通识别。
我用PaddleOCR的版面分析模型做区域检测,再用PaddleOCR的表格识别模型做表格还原。这个模型的输出可以转成HTML表格格式,再通过HTML转Markdown的管线落成结构文本。速度方面一台普通办公电脑大概每秒能处理3-5页A4扫描件,一本300页的报告处理完要一个多小时,中间要留意内存占用,建议分章节跑。
预处理这一步很关键,尤其处理老报告影印件时。这些扫描件噪声大、底色黄、有斑点、字迹断断续续,直接识别效果很差。我一般先做灰度化、自适应二值化、去噪,再用形态学操作把断裂笔画补上。写参数时要参照扫描件的DPI,400 DPI以上识别效果比较好,低于200 DPI的扫描件建议重扫,别硬靠算法补。
这里提一个特别值得注意的点:日本银行和很多老地质报告是双栏排版,中英文混排,页脚还有页码和页码说明。版面分析后要按阅读顺序重组栏目,不然OCR识别的文本顺序是乱的,切块质量会很差。
4.3 图件说明和表格数据的单独处理
探矿PDF还有一个特殊的东西:地质图件、剖面图、等值线图旁边的说明文字。这些图件的说明文字往往是核心数据(图例、比例尺、坐标系统),但因为它们在图框外或者图内,OCR时经常被漏掉或者错位。
我的处理方法是:版面分析时专门划出一个"图件注释"类别区域,识别后单独存放。如果这些注释含有坐标或比例尺信息,可以额外提取出来,在后面切块时作为图件元数据添加进去,而不是硬塞进正文。
表格数据的重建在探矿PDF里很容易出问题。比如化探分析结果表,可能有几十行,每行包括"样号、Cu、Pb、Zn、Au、Ag"等十几个元素含量值,PDF转文本时这些值经常错位,行列对不上。针对这种表格我建议优先用表格识别模型,别用手写正则去逐行逐列拼,否则一个细胞跨页就全乱了。识别完成后的表格,比对所有列的数值范围做合理性校验,发现异常(比如品位值超过地质学常识范围几十倍)就标记为"待人工审核"。
5. 网页采集与正文抽取
5.1 探矿网页资料的采集特点
探矿领域的网页数据主要来自政府公示、行业资讯、矿权交易公告、地勘单位官网。这些页面的采集难度不大,但清洗难度很扎心:一个页面里正文可能只有两三千字,导航、广告、相关推荐、版权声明加起来的噪音是正文的三四倍。而且很多政府网站使用GB2312编码,抓取时直接按UTF-8解码就是乱码。习惯了requests加beautifulsoup四件套的人,一上来容易栽在编码上。
采集时务必要做三件事:尊重目标站点robots协议、控制请求频率、保持用户代理的合理性。探矿行业站点很多是老旧的ASP或者Java系统,扛不住高频请求,被对方拉黑是小,给对方服务器造成压力和后续访问困难是大。
5.2 正文抽取工具的选择
网页正文抽取,我首推trafilatura,其次是readability-lxml,再其次才是自己写正则。trafilatura对各种门户网站的正文抽取效果很好,能自动识别标题、作者、发布时间和正文,去除导航信息,还自带一个简单的爬虫接口。用它的好处是省时间,坏处是在小众站点上偶尔抽不到正文,这时我再退回到BeautifulSoup配合"去掉script/style/nav/footer,按段落密度提取正文"的手工策略。
有一段我常用的后备代码:
from bs4 import BeautifulSoup import re def extract_readable(html: str) -> str: soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "nav", "footer", "aside", "header"]): tag.decompose() body = soup.body or soup # 按区块内链接密度打分,低链接密度的才是正文 for div in body.find_all(["div", "section", "article"]): text_len = len(div.get_text(separator="\n", strip=True)) link_count = len(div.find_all("a")) if text_len > 200 and link_count / max(text_len, 1) < 0.01: return div.get_text(separator="\n", strip=True) return soup.get_text("\n", strip=True)这个方案不完美,但作为兜底足够。另外网页里的表格数据也建议单独抽取,而不是直接取纯文本,否则表格的行列信息又丢了。政府公示网站经常有附件PDF或Word下载,链接也要一并记录下来,后续走PDF或Word清洗管线。
6. 清洗完成之后,检索精度怎么验证
6.1 切块策略与清洗质量的联动
清洗做得再漂亮,切块策略不对,检索效果一样翻车。探矿文档的切块有个天然难点:信息密度高、术语密集,一个200字的段落里可能包含好几个独立知识点(岩石类型、构造特征、矿化蚀变),如果切块太大,检索出来一堆无关信息;切块太小,又容易把"岩石类型"和"构造特征"的共同背景切断。
我的经验是清洗完成后再做切块试验,别拿原始文档的切块参数套清洗后的文档。清洗后文档的段落更规整、表格更清晰,之前为屏蔽噪音被迫调大的chunk_size可以适当缩小。跑一个对比实验,在小规模向量库上,把chunk_size设为256、384、512三种,top_k设为3、5、10三种,用领域问题集做检索,看哪种组合的命中效果最好。清洗质量越高,最优chunk_size往往可以选得越小。
探矿业务还有一个特点:大量内容是表格。如果表格清洗成了结构化Markdown表格,切块器最好能保留表格边界,不要把行数据切裂。我用LangChain的TextSplitter时,会先对文本做"表格保护"预处理,把表格替换成占位符,切完再恢复,这样能最大限度避免表格被拦腰切断。
6.2 高精度检索不能只靠向量
探矿业务的检索需求很具体,比如"查某矿区辉钼矿的Re-Os同位素年龄","查某地区二叠纪地层厚度数据",这种问题光靠向量相似度基本不够。我的配置是"向量检索+关键词检索+重排序"三件套。清洗质量高,关键词检索的命中率就会跟着提高,因为术语被统一了、乱码被修复了。
重排序我用的是cross-encoder模型,精排一次查询和候选文档的相关性,效果比直接按向量相似度排序好很多。但精排对算力有要求,探矿知识库如果做到几十万级别段落,重排序要控制候选集数量,一般把向量检索返回的top_k从50压缩到20再进精排,速度和质量比较平衡。
检索评估方面,我建了一个约200条的领域问题集,每条问题标注正确答案来源段落。评估指标用Recall@k和MRR。清洗前后对比时,我看到最明显的提升出现在"包含数字/代码/特殊符号"的问题上,比如矿体编号、分析报告编号,这类问题原始文档里的乱码和歪斜字符是命中率杀手,清洗后直接解决。
7. 常见问题排查与心得
7.1 问题速查表
我把这几类源文件在清洗过程中的高频问题整理成一个表格,方便现场排查:
| 文件类型 | 常见问题 | 原因 | 处理方向 |
|---|---|---|---|
| TXT | 整篇乱码 | 编码误判 | 先chardet/charset_normalizer检测,再实测回读 |
| TXT | 列错位 | 分隔符不一致 | 自动识别分隔符,正则切割固定多空格 |
| Word | 表格行列错位 | 合并单元格 | 检测行单元格数,按坐标对齐 |
| Word | 正文丢失只有图片 | 内容在文本框/域代码 | 宏转换,或用Word另存HTML |
| Word | 公式变成图片 | OLE对象无法解析 | 公式转LaTeX,或OCR识别公式区域 |
| 文字能复制但乱序 | 版面复杂/双栏 | 用坐标重排文本 | |
| 扫描件识别率低 | 预处理不足 | 灰度化、二值化、去噪后重试 | |
| 表格数值错位 | 纯文字抽取顺序混乱 | 专用表格识别模型,跨页单独处理 | |
| 网页 | 正文抽不到 | js动态渲染 | 用Playwright渲染后抽正文 |
| 网页 | 编码乱码 | 站点用了GB2312 | 按响应头编码解码,再统一UTF-8 |
7.2 几条我自己的经验
最后分享几条实操心得,都是踩过坑换回来的。
第一,清洗流程必须是"可重入"的。清洗脚本输入和输出都应该是文件,中间步骤可以断点续跑。探矿报告动辄几百页,OCR跑一半崩了非常常见,没有断点机制会让人崩溃。我在脚本里对每个文件输出一个清洗状态文件,跑完一个标记一个,下次启动直接跳过已完成的。
第二,人工抽查的比例别低于2%。清洗管线再完善,总会有特殊文件漏网。我在每个批次清洗完之后,随机抽文件检查,重点看表格是否错位、页眉页脚是否混入正文、专业术语是否被错误改写。有一次抽查发现,一个Word清洗后"铅锌矿"被转成了"铅侔矿",就是在识别特殊字符时替换错了,这种错误如果不抽查根本发现不了。
第三,清洗日志和元数据留档。每个源文件清洗后,我都有一个JSON元数据文件,记录它的来源、原始编码、清洗脚本版本、清洗耗时、错误标记等。这个元数据在后面做知识库溯源时很有用。检索结果如果不理想,可以按元数据追溯这个chunk出自哪个原始文件,直接检查清洗环节在哪一步丢信息了。
第四,不要过度清洗。探矿报告里有些"不整洁"的信息其实承载着上下文。比如老报告里"矿化富集于构造破碎带上下盘围岩中"这种句子,虽然语法松散,但检索"构造破碎带"和"矿化富集"时,它反而是高质量命中片段,不要因为句式不工整就强行重写它。清洗的原则是"去噪、结构化、规范化",不是"文学润色"。
再补充一个小技巧:探矿文档里经常出现坐标信息(经纬度、北京54、西安80、CGCS2000坐标系)。清洗时可以对坐标做一次统一的规范化处理,比如把度分秒格式统一转成十进制度浮点数。这样做检索时,"东经120度30分"和"120.5度"就能在向量空间里对齐,对地理信息检索的帮助非常明显。这个操作不复杂,但收益立竿见影。
如果你也在做行业RAG,尤其是数据来自老文档体系,建议别急着调embedding模型,先花精力把清洗管线打磨好。清洗这一步扎实了,后面的一切才站得住。