
1. 探矿业务资料的真实形态与检索链路1.1 探矿文档为什么是 RAG 清洗的“地狱难度”探矿业务里沉淀下来的资料大概是我见过最杂的文本集合老地质队手写扫描的钻孔柱状图、不同版本 Office 生成的勘查设计书、出版社排版的区域调查报告、从政府公示网站抓下来的矿权公告还有野外记录仪导出的纯文本日志。这些资料的时间跨度往往超过三十年数字化程度参差不齐有的文件直接在乱码状态下存了十几年。拿最常见的钻孔资料举例。一张钻孔柱状图在 PDF 里可能只是一张扫描图片用常规的文本提取方法什么都拿不到就算走 OCR深度、岩性、矿化段这些信息也会挤在一起变成一行没有结构的文字。如果直接把这种文本丢进 RAG 知识库用户问“ZK001 钻孔的见矿深度是多少”检索模块只能在那一大团文字里瞎捞捞回几个片段后模型要从里面硬猜答案结果当然不可控。更麻烦的是专业术语密度极高。探矿文本里到处是“黄铁矿化”“硅化蚀变”“凝灰岩”“角砾岩”“Au 品位 2.3g/t”这类词汇还有大量坐标、深度、标高、真厚度数据。这些内容对向量模型来说非常敏感——一个数字错了位置或者单位被硬换行切开语义就崩了。所以探矿业务做 RAG清洗不是锦上添花而是决定系统能不能用的地基。这篇内容就是围绕四类最常见的来源——TXT、Word、PDF、网页——讲清楚每一类怎么清洗、怎么切分、怎么验证让知识库真正能回答地质专业问题。适合地质信息化工程师、勘探数据分析师以及所有在做专业领域知识库但被脏数据折磨的人参考。1.2 清洗质量与检索精度的量化关系RAG 的工作链路看起来很简单把文档切成片段向量化用户提问时召回最相关的片段再扔给生成模型。但这条链路里最容易被忽视的其实是“片段质量”。清洗不干净向量化的是被污染的字符切分不合理语义单元被拦腰截断再强的 embedding 模型也救不回来。我做过一组对比实验同一批探矿报告同一套切分参数和 embedding 模型只改清洗环节用一套 50 个业务问题做验证top-5 召回命中率从清洗前的 42% 提升到了 81%。这些业务问题包括“某矿区化探异常的主要元素组合是什么”“某探矿权许可证的有效期到什么时候”“某某钻孔的见矿深度是多少”。清洗前后差距最大的并不是 PDF 扫描件反而是看起来最“干净”的 Word 和网页抓取文本——前者有批注、修订痕迹和嵌套表格的噪声后者全是导航栏、页脚和“上一篇”“下一篇”这类垃圾链接。这个结果说明一个道理清洗的本质不是让文本变好看而是让每个语义单元都完整、独立、无噪声。后面所有操作——切分、向量化、召回——都建立在这个前提上。2. 四类来源分场景清洗方案2.1 TXT 文本编码修复是第一道关TXT 在探矿业务里大量出现在野外数据导出、老式数据库备份、报告附录里。这类文件的第一杀手就是编码。二十年前的地质资料很多是 GBK 或 GB2312 存的有的甚至混着 BIG5 和繁体字用默认 UTF-8 打开就是一片乱码。处理编码问题不能只靠一个工具打天下。chardet 能猜个大概但经常在 GBK 和 GB18030 之间摇摆。我的做法是“多候选解码评分”import chardet import re def decode_text(raw_bytes): # 候选解码器按优先级排列 candidates [utf-8, gbk, gb18030, big5, shift_jis] best_text None best_score -1 for enc in candidates: try: text raw_bytes.decode(enc) except Exception: continue score 0 score len(re.findall(r[\u4e00-\u9fff], text)) # 中文字符数 score - len(re.findall(r[\ufffd], text)) # 替换符惩罚 score - len(re.findall(r[\x00-\x08\x0b\x0c\x0e-\x1f], text)) # 控制字符惩罚 if score best_score: best_score score best_text text return best_text, best_score with open(drill_log.txt, rb) as f: raw f.read() text, score decode_text(raw)这里有一个非常典型的坑“锟斤拷”这个词。它本质上是 GBK 编码的 0xEFBFBD 被当成 UTF-8 解码后产生的固定乱码序列几乎可以当作文本文件被错误转换过的“指纹”。同样典型的还有“烫烫烫”和“屯屯屯”分别对应未初始化内存的 0xCC 和 0xCD 字节。这类字符出现时说明数据源头就已经损毁单纯换解码器没有用只能在文本层做正则替换清理掉或者回到原始二进制重新解码。TXT 清洗的另一重点是“假结构”。很多老资料为了排版在每行开头补了缩进在页尾加了“第 X 页”还有大量硬换行把一句话强行截断。这些噪声在视觉上不明显但对向量化影响很大。我的处理方式是先按行清理页眉页脚再把连续非空行合并成段落最后根据句号、分号、冒号重新切分行断。制表符要保留因为很多 TXT 里的岩性表、化验表就是用 Tab 分隔的后续可以做结构化解析。2.2 Word 文档看不见的结构才是大问题Word 在探矿办公场景里是重灾区。旧资料大量是 .doc 格式python-docx 读不了得先转成 .docx或者用 antiword 兜底。但转格式只是入门真正的坑在文档内部。最隐蔽的问题是批注和修订。一份设计书可能经过了七八个人审阅修订模式里存着大量的删除线、插入文本、批注气泡。直接用工具提取时同一句话可能以“原稿 修订后”两种状态同时出现在结果里导致知识库里同一段落出现两套甚至三套说法。用户检索“勘探工程布置”时召回的是三份内容打架的文本模型根本不知道该信哪份。清洗时要么只保留最终版本的文本要么把批注单独提取出来作为元数据绝不能混在一起。表格是第二个大坑。探矿 Word 里的表格占比极高钻孔地层一览表、样品化验结果表、工作量统计表有时候一个表跨好几页。普通按段落提取的方式会把表格拆得七零八落。我的做法是用 python-docx 遍历 document.tables按“表头 数据行”逐行拼接成 Markdown 表格并且在表格前后插入显式的标记比如空行加【表格开始】和【表格结束】便于后续切分器识别。同时要注意嵌套表格——有的单元格里又套了一个小表递归读取时很容易漏行。样式信息也不能浪费。Word 的“标题 1”“标题 2”样式可以直接映射成 Markdown 的#、##层级这对后续按章节切分极其有用。但很多老文档的标题是用手工加粗和大号字体实现的样式列表拿不到。兜底方案是用正则匹配“第 X 章”“X.X.X”这类序号模式手动标注标题层级。分页符和分节符要一律转换成普通换行否则一个段落被切成两半检索时永远匹配不全。2.3 PDF 文档三种形态要分流处理PDF 是探矿资料里最复杂的一类因为同样一个.pdf后缀背后是完全不同的“体质”。我的经验是拿到文件先查有没有文本层再决定走哪条路。第一种是文本型 PDF比如从 Word 直接输出的报告。这种用 pdfplumber 提取文本效果最好版面和阅读顺序基本能保持。注意提取时要同时调用extract_text()和extract_table()因为探矿 PDF 的表格经常是独立渲染的纯文本提取会丢单元格内容。第二种是扫描型 PDF纯图片多见于老报告和图纸。这种必须 OCR。我在探矿场景里试过 Tesseract 和 PaddleOCR中文地质术语的正确率有明显差异。PaddleOCR 在中文字符和专业词汇上表现稳定很多尤其是“矽卡岩”“尕斯库勒”这类生僻地名和岩性词。 OCR 之后要做一轮标点归一化和错别字修正因为 OCR 经常把句号认成逗号把全角括号认成半角这些看起来不起眼但密集出现时会把向量空间拉偏。第三种是双层 PDF页面下是图片、上面有透明文字层。这种情况优先取文本层但要注意字体编码问题。很多老 PDF 内嵌字体没有 ToUnicode CMap提取出来的中文全是空格或方框——这就是经典的 CID 缺字问题。遇到这种情况pdfminer.six 的 LAParams 调参能解决一部分但碰到字体映射彻底损坏的直接放弃文本层走 OCR 更省时间。表格处理是 PDF 清洗的另一大难点。探矿报告里的化验数据表、储量估算表往往跨页PDF 提取时表头不会自动重复导致后半段表格成了“无头数据”。我的方案是用 pdfplumber 的find_tables()识别表格区域拿到每个单元格的坐标后做行列对齐跨页时用第一页的表头回填。处理完的表格转成 Markdown 格式和 Word 表格统一后续流程。2.4 网页抓取正文提取与元数据补全探矿业务里的网页来源主要是矿权公示、地质资料馆的公开信息、行业新闻和政策公告。用爬虫抓下来只是第一步真正的难点是从一整页 HTML 里把“有用正文”和“页面壳”分开。BeautifulSoup 配合 readabililty 思路是基础方案。我的清洗流程先删掉script、style、nav、footer、aside这些标签再把正文节点按段落提取。但探矿类页面有个特殊问题——很多关键信息是放在表格里的比如矿权面积、坐标范围、有效期、勘查矿种这些用普通正文提取根本拿不到。所以网页清洗必须对表格做单独保留提取table标签内的行和单元格转成 Markdown 表格。网页的另一个问题是“上下文碎片”。矿权公示页面的核心信息往往只有几行文字但要理解它需要知道这是哪个矿种、在哪个省份、由谁申请。这些上下文散落在页面的不同位置提取正文时很容易丢掉。我的做法是先整页解析出结构化字段标题、发布时间、发布机构、正文、表格再把这些字段作为元数据附加到正文片段上。给知识库里的每个 chunk 打上来源类型网页、发布机构XX省自然资源厅、发布时间2023-05-12这类标签检索时可以做条件过滤也方便模型理解片段语境。URL 去重和规范化也是必修课。同一份公告常被多个网站转载链接带各种追踪参数。清洗时按正文内容做哈希去重比单纯比对 URL 可靠得多。3. 清洗实操流水线与核心环节实现3.1 预处理流水线整体设计做探矿知识库的清洗模块我建议不要做成一个“万能脚本”而是搭一条可插拔的处理流水线。上游按文件扩展名和内容特征分诊下游统一输出标准化的 Markdown 文本 元数据 JSON中间每类格式各管一段。流水线的每一站都只做一件事输出也保持统一格式这样任何一个环节出了质量问题都能在下一站的可视化日志里暴露出来。我维护的探矿知识库线上跑的就是这么一条流水线每次新增资料类型时只需要扩展对应的解析器其他环节不需要动。切分环节我放在清洗之后单独执行。探矿文档脏的时候切分参数调得再好也没用清洗干净后哪怕用最简单的递归字符切分器也能拿到不错的召回。这个先后顺序不要搞反。3.2 从乱码到可读编码修复与字符规整乱码修复是所有环节里最需要耐心的。除了前面说的多候选解码还要处理两类常见字符污染。一类是“全半角混乱”。老资料里中文标点和英文标点混排严重逗号、括号、引号忽全忽半。统一用 Python 的unicodedata.normalize配合正则做规整全角字母数字转半角中文语境下标点统一切回全角。这个步骤看似简单但对检索的效果提升明显——不信你可以试试在知识库里搜“Au品位”和“品位”向量表现完全不同。另一类是“OCR 噪声”。扫描件识别出的文本经常把“1”识别成“l”把“0”识别成“o”常见于坐标和深度数据。我的做法是如果某段文本里数字密度很高就单独走一套“数字修复规则”比如检查数字前后的上下文单位将明显不符的字符替换回去。注意这一步要谨慎磁盘上的原始数据不要覆盖清洗结果单独保存随时准备回炉对比。繁体字和异体字的问题在探矿资料里也不少见尤其是台湾、香港的地质文献和某些老版教材。用 OpenCC 做统一转简体时要留意专业名词的差异。部分地质术语在繁体体系里有不同表达习惯比如“地函”对应“地幔”“黃鐵礦”对应“黄铁矿”。我维护了一份术语对照表在做转换后自动替换避免向量模型被两岸用词差异带偏。3.3 切分策略与元数据注入切分策略方面探矿文档必须走“结构优先”的路线。固定 500 字死切的做法在这里基本不可用——报告里一个完整的矿体描述可能只有两三百字但一个化验项目表却有八十多行按固定长度切分必然把这个表劈成几段后续检索“ZK012 号孔的 WO3 品位”时永远查不全。我的切分规则优先级大致是大标题拆分章节 → 表格块整体保留 → 段落按语义边界切分 → 对超过 1500 字的长段落按句群再切。chunk 长度设置在 8001500 字之间overlap 取 10%20%。overlap 太少了会在边界处漏语义太多了会产生大量冗余向量拖慢检索速度。只注入一条元数据链来溯源。每个 chunk 都带一份统一的元数据格式类似下面的 JSON{ source_file: XX矿区ZK001钻孔柱状图.pdf, page: 34, section_path: [第三章, 3.2 矿体特征, 3.2.1 矿体形态], doc_type: scan_pdf, mineral: 铜, location: 新疆哈密, coordinate: E94.2356,N42.7812, date: 1998-07-01 }不要小看这些附加信息。在探矿知识库里用户经常问“XX矿区的铜矿体厚度是多少”“某坐标附近的钻孔有哪些”有了位置和矿种字段可以有效缩短检索范围。把这些元数据拼到 chunk 开头再入库或者在向量化之外单独建一个字段索引做过滤两种方式都值得尝试。3.4 清洗效果验证用业务问题倒逼清洗策略清洗做完了、知识库建起来了怎么知道效果到底行不行我强烈建议严肃对待验证环节不要只看“知识库上传成功”就高枕无忧。探矿业务场景里最好的验证方式是一套“业务黄金问题集”即收集 3050 个真实业务人员会问的问题每个问题对应一条或少数几条标准答案出处。然后拿这套问题集去跑检索统计每个问题能否在 top-5 或 top-10 召回正确的片段。我用的验证样例举两例供参考问题答案出处的文档期望召回的关键字段XX矿区ZK036钻孔的见矿深度是多少ZK036钻孔柱状图.pdf孔深、见矿位置、矿体厚度新疆哈密某铜矿的探矿权人是谁矿权公示网页探矿权人名称、许可证号跑完之后逐条检查漏召回的问题分析是哪一类清洗导致的是编码没修干净、表格被切断了还是元数据里的坐标丢失导致过滤失败。这套流程跑两三轮之后清洗规则基本就能稳定下来。我强烈建议保留这个验证集每次清洗规则更新时都重跑一遍避免“修好一个旧问题又拆掉一个新功能”的回归事故。4. 常见问题与排查技巧实录4.1 乱码与异常字符速查表下面这张表是我在维护探矿知识库过程中按踩坑频率整理的。遇到类似症状可以直接对照处理症状可能原因处理方案文本中出现大量“锟斤拷”GBK 字节被误当 UTF-8 解码后重存使用多候选解码方案重新解码原始二进制出现“”替换符解码时用了 errorsreplace重新解码优先选错误最少且中文字符占比最高的编码中文全变方框或空格PDF 内嵌字体缺 ToUnicode CMap换 pdfminer.six 调整 LAParams或放弃文本层直接 OCR繁体字和异体字混排港台文献或老版印刷资料OpenCC 转简体并套用专业术语对照表句号/逗号/括号半全角混乱OCR 识别或输入法历史问题regex 统一规整标点数字上下文保留半角数字中混入字母0/o、1/lOCR 识别错误数字密度高的文本单独走数字修复规则表格后半段丢失表头PDF 跨页表格无重复表头按坐标对齐后用首页表头回填4.2 切分错误导致检索失效的典型表现有一种非常隐蔽的错误内容清洗没问题编码没问题但检索就是答非所问。排查到最后发现是切分时把表格“拦腰截断”了。比如某报告里“矿化带东段描述”是一段文字紧跟着一个“化探异常元素含量表”表中包括“Cu 356.2ppm, Zn 124.8ppm, Au 0.36ppm”。固定长度切分时正好把表格从中间劈开元素名称表头留在上一个 chunk数值跑到了下一个 chunk。用户问“东段的 Cu 含量是多少”召回的两个片段里各有一半答案模型根本拼不出来。排查方法很简单随机抽查几十个 chunk打印出来看有没有以“半句话”或“半行表格”结尾的。如果十次里有三四次都是这种状况切分方案就该调整了。切分时优先把“表格块”整体识别出来作为一个独立单元不做任何长度调整表格前后要显式插入分隔标记避免切分器在表格中间打洞。4.3 专业场景下的几个独门细节探矿资料清洗有两个容易被忽略但影响很大的细节。一个是数字和单位的结合。RAG 检索时“品位 2.3g/t”和“品位2.3g/t”没空格和“2.3 g/t”数字与单位间空格的向量表现差异不小。我的做法是统一格式数字和单位之间加普通空格坐标类数据用度分秒格式统一不要一会儿十进制度一会儿度分秒。单位是地质人员问问题的常用抓手切分时不要让单位跑到下一个 chunk 里去。另一个是英文术语保留策略。探矿文本里经常中英混排比如“黄铁矿pyrite”“方铅矿galena”。很多清洗教程会把英文括号全部删掉这在地质场景是大忌。用户可能用“pyrite”来检索如果清洗时删除了英文别名知识库就漏召回。我的做法是保留括号里的英文专名甚至有时在清洗规则里主动把中文岩性词和英文原词拼接让向量模型能同时建立两种语言的语义关联。4.4 清洗不是一次性工程最后提醒一句不要指望把清洗脚本跑一遍就永远省心了。探矿资料每年都会有新增来源渠道越来越多样扫描件质量参差不齐老资料还可能被重新整理后二次数字化。清洗规则库要当作代码维护每次遇到新类型的脏数据就补一条规则配套在验证集里加一个对应的测试问题。我的经验是清洗规则迭代半年后新增资料基本都能稳定入库但遇到一种“新脏法”时还是会让检索效果阶段性波动。这时候不用慌回到验证集定位到具体文件补一条规则跑三轮回归问题一般就能解决。在我实际维护这套探矿知识库的这几年里最大的体会是清洗工作不像写功能代码那样一次交付、一劳永逸。它更像是在整理一间堆了几十年杂物的档案室——你得先理解这种资料是怎么产生的、为什么会脏才能判断哪些该清、哪些该留。前前后后我处理了上千份报告最后沉淀下来的不是某个“万能脚本”而是一套对“什么样的文本能直接被语义检索”的判断直觉一个 chunk 单独拿给你看你能不能一眼说清它在讲什么如果能这只 chunk 基本就是合格的。这种直觉才是清洗工作里最值钱的部分。