1. 探矿业务里的数据泥潭:为什么通用 RAG 方案一上就废
做过矿业数字化的人都有一个共同体会:这个行业的数据,脏得非常有“个性”。地质报告是扫描件,钻孔柱状图是 CAD 导出的 PDF,化验数据是老师傅用 Excel 攒了十几年的 TXT,矿权公示信息散落在各种网页里,还有一堆 Word 格式的内部评审意见。你把这些东西一股脑丢给一个开箱即用的 RAG 框架,检索出来的结果基本没法看——要么是乱码,要么是表格错位,要么是公式变成一串问号。
我去年接手的一个探矿知识库项目,前期用通用方案跑了两周,召回率惨不忍睹。后来花了大概一个月时间专门做数据清洗和分块策略,才把检索精度拉到可用水平。这篇文章就把这套清洗流程完整拆开讲,涉及 TXT、Word、PDF、网页四种来源的处理思路,以及探矿业务特有的坐标、品位、地层代号这些结构化信息的保留方法。
先说清楚这套方案适合谁:如果你手上有大量地质报告、化验数据、矿权资料需要做成可检索的知识库,不管是用现成的 RAG 框架还是自建向量库,这篇文章里的清洗逻辑都能直接参考。不需要你精通深度学习,但得懂基本的 Python 和正则表达式。
核心思路其实就一句话:先按数据来源分而治之,再按业务语义重新组装。通用方案失败的根本原因,是它假设所有文档都是“干净的连续文本”,而探矿数据恰恰相反——它的价值藏在表格、图件标注、坐标数字和地层代号里,这些恰恰是通用解析器最容易丢掉的部分。
2. 四类数据源的清洗策略拆解
2.1 TXT 化验数据:看似最简单,坑最深
TXT 在探矿业务里通常有两种形态:一种是化验室导出的原始数据,用制表符或逗号分隔;另一种是老师傅手工整理的台账,格式全靠自觉。前者好办,后者才是噩梦。
我遇到过一个典型情况:某矿区从 2008 年到 2020 年的铜品位数据,前五年用空格分隔,中间三年用制表符,最后几年又变成了逗号。更麻烦的是,有些行中间夹杂着“以下为补测数据”这样的中文说明,还有的行因为当年录入时手抖,多了一个小数点。
处理这类数据的核心原则是:先做结构探测,再做字段映射,最后做数值校验。不要一上来就写死分隔符。
import re import pandas as pd def detect_delimiter(line): """探测单行最可能的分隔符""" candidates = ['\t', ',', ';', '|', r'\s{2,}'] for d in candidates: parts = re.split(d, line.strip()) if len(parts) >= 3: return d return None def clean_assay_txt(filepath): with open(filepath, 'r', encoding='utf-8', errors='ignore') as f: lines = [l for l in f.readlines() if l.strip()] # 过滤说明性文字行 data_lines = [l for l in lines if not re.search(r'[以下补测说明备注]', l)] delimiter = detect_delimiter(data_lines[0]) records = [] for line in data_lines: parts = re.split(delimiter, line.strip()) records.append(parts) df = pd.DataFrame(records) return df这里有个经验:编码问题一定要用 errors='ignore' 兜底。地质行业的老文件很多是 GBK 甚至 GB2312 编码,直接按 UTF-8 读会直接抛异常中断整个流程。忽略错误字符虽然会丢一点信息,但比整个文件读不进来强得多。
数值校验环节,我通常会加一个合理性检查:铜品位超过 30% 的基本可以判定为录入错误(自然界极少有这种富矿),这时候要么标记出来人工复核,要么按前后行的均值修正。这一步看起来多余,但实测能拦掉大概 3% 到 5% 的脏数据,对后续检索的可信度提升很明显。
2.2 Word 文档:表格和公式是重灾区
Word 格式的地质报告,难点集中在三块:嵌套表格、MathType 公式、以及跨页的合并单元格。python-docx 这个库能处理大部分情况,但遇到合并单元格会直接返回 None,需要自己写逻辑补全。
表格处理的关键是保留行列的语义关系。举个例子,一个钻孔的测斜数据表,表头是“孔深、倾角、方位角”,如果解析后变成三列孤立的数字,检索时就完全丢失了“这是哪个钻孔的哪个深度”这个上下文。我的做法是把表头信息拼接到每一行的文本里:
from docx import Document def extract_word_tables(doc_path): doc = Document(doc_path) chunks = [] for table in doc.tables: # 提取表头 headers = [cell.text.strip() for cell in table.rows[0].cells] for row in table.rows[1:]: cells = [cell.text.strip() for cell in row.cells] # 跳过全空行 if not any(cells): continue # 将表头与数据拼接成语义完整的句子 row_text = ';'.join( f"{h}为{c}" for h, c in zip(headers, cells) if c ) chunks.append(row_text) return chunks这样处理之后,原本一行“120.5 | 75 | 180”会变成“孔深为120.5;倾角为75;方位角为180”,检索“倾角75度的测点”时就能命中。
公式处理是另一个大坑。MathType 插入的公式在 python-docx 里读出来是空的,因为它是 OLE 对象。可行的方案有两种:一是用 Word 宏批量把公式转成 LaTeX 再导出,二是用 LibreOffice 的无头模式转换。我实测下来,如果公式量不大(几百个以内),用宏转换更可控;量大的话建议走 LibreOffice 命令行,虽然慢但稳定。
注意:Word 宏转换公式前一定要先备份原文件。我踩过一次坑,宏跑到一半卡死,原文档的公式对象全部损坏,只能从备份恢复。
2.3 PDF 文档:扫描件和矢量图要分开对待
PDF 在探矿资料里占比最大,也最复杂。必须先把 PDF 分成两类:文本型 PDF和扫描型 PDF。前者可以直接抽文字,后者必须先做 OCR。
判断方法很简单,用 pdfplumber 试着抽第一页的文字,如果字符数少于 50,基本可以判定是扫描件。
import pdfplumber def is_scanned_pdf(pdf_path): with pdfplumber.open(pdf_path) as pdf: first_page = pdf.pages[0] text = first_page.extract_text() or '' return len(text.strip()) < 50文本型 PDF 的表格提取,pdfplumber 的 extract_tables 已经够用,但要注意它的默认策略对无边框表格识别很差。地质报告里的化验表经常没有完整边框,这时候需要调整 table_settings,把 vertical_strategy 和 horizontal_strategy 改成 text 模式,靠文字对齐来推断表格结构。
扫描型 PDF 走 OCR 路线,我推荐 PaddleOCR,对中文和数字的识别率比 Tesseract 高不少,尤其是表格里的数字。但 OCR 之后一定要做后处理:把“O”和“0”、“l”和“1”这类常见混淆字符按上下文修正。地质数据里数字密度高,这个修正步骤能显著提升后续检索的准确率。
还有一个容易被忽略的点:PDF 里的图件标注。地质图上的地层代号、断层编号、产状符号,这些信息在纯文本抽取时全部丢失。如果业务上需要检索这些内容,得单独走图件解析流程,把标注文字和坐标一起抽出来存成结构化数据。这块工作量不小,建议按需处理,不要一开始就全量做。
2.4 网页数据:动态渲染和反爬的平衡
矿权公示、地质资料馆的公开信息,很多只在网页上。静态页面用 requests 加 BeautifulSoup 就能搞定,但现在的政务类网站大量使用动态渲染,必须上 Playwright 或 Selenium。
我的选择是 Playwright,原因是它的等待机制更智能,不用写一堆 sleep。抓取时重点处理三件事:正文提取、表格保留、附件链接记录。
正文提取不要用简单的 div 选择器,网站改版就失效。用 readability 类的算法(比如 python-readability)自动识别正文区域更稳。表格直接转成和 Word 一样的“表头+数据”拼接格式。附件链接单独存一个字段,因为很多矿权信息的核心数据在附件 PDF 里,正文只是索引。
from playwright.sync_api import sync_playwright from readability import Document def scrape_page(url): with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto(url, wait_until='networkidle') html = page.content() browser.close() doc = Document(html) main_content = doc.summary() # 后续用 BeautifulSoup 清理标签、提取表格 return main_content抓取频率要控制好,政务网站经不起高频请求。我一般设置每请求间隔 3 到 5 秒,并且严格遵守网站的 robots.txt。这不是技术问题,是基本的职业操守。
3. 从清洗到入库:分块策略与元数据设计
3.1 探矿业务的分块逻辑
通用 RAG 方案默认按固定字符数分块,比如每 500 字一块。这在探矿场景下是灾难——一个钻孔的描述可能跨了三个块,检索时只能召回片段,上下文全丢了。
我的分块策略是按业务实体分块:一个钻孔一个块,一个化验样品一个块,一个矿权一个块。块内保留完整的属性信息,块与块之间通过元数据关联。
具体实现上,先用规则识别实体边界。钻孔描述通常以“ZK”加编号开头,化验数据以样品编号开头,矿权信息以许可证号开头。识别到边界就切块,块大小不固定,从几百字到几千字都有可能。
这样做的好处是检索粒度匹配业务粒度。用户问“ZK1201 的见矿深度是多少”,直接命中那个钻孔的完整块,不会出现只召回半句话的情况。
3.2 元数据字段设计
元数据是探矿 RAG 的灵魂。我设计的字段包括:数据来源类型(TXT/Word/PDF/网页)、矿区名称、钻孔编号、样品编号、坐标范围、数据年份、置信度等级。
置信度等级这个字段特别有用。原始化验数据标为“高”,OCR 识别的标为“中”,人工整理台账标为“低”。检索时可以按置信度过滤,避免把不可靠的数据混进结果。
坐标范围用 GeoHash 存储,检索“某区域内的所有矿点”时可以直接做空间过滤,比全文检索快得多。
3.3 向量化前的文本规范化
入库前还有一步不能省:术语统一。探矿行业里同一个东西有多种叫法,“品位”和“含量”、“倾角”和“倾伏角”、“断层”和“断裂”,如果不统一,检索时召回率会大打折扣。
我的做法是维护一个同义词表,在向量化之前做替换。这个表不用一开始就建全,跑一段时间检索日志,把用户实际用的词和文档里的词对不上的情况收集起来,逐步补充。
数字格式也要统一。有的文档写“1.5%”,有的写“1.5个百分点”,有的写“15000ppm”。统一转成标准格式再入库,检索“品位大于1%的样品”时才能准确命中。
4. 实操中踩过的坑与排查手册
4.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 检索结果全是乱码 | 编码识别错误 | 用 chardet 检测文件编码 | 按检测结果指定编码读取,加 errors='ignore' |
| 表格数据错位 | 合并单元格未处理 | 打印解析后的行列结构 | 补全合并单元格的空白值 |
| 公式变成空字符串 | MathType OLE 对象 | 检查 docx 里的 XML 结构 | 用宏或 LibreOffice 预转换 |
| 扫描件 OCR 数字错误 | 字体或分辨率问题 | 抽样对比原图 | 提高 DPI 到 300,做字符混淆修正 |
| 网页抓取为空 | 动态渲染未等待 | 检查 networkidle 是否触发 | 增加显式等待或改用 API |
| 检索召回不相关 | 分块粒度过细 | 查看召回块的上下文 | 改为按业务实体分块 |
4.2 三个独家避坑技巧
第一个:PDF 解析一定要做页面级容错。我遇到过一份 200 页的地质报告,第 87 页损坏,导致整个文件解析失败。后来改成逐页 try-except,坏页跳过并记录,其余 199 页正常处理。这个改动让整体成功率从 60% 提升到 98%。
第二个:OCR 结果要保留原图坐标。PaddleOCR 返回的每个文本框都有坐标信息,不要只取文字。保留坐标后,可以把 OCR 结果按位置重新组装成表格,比纯文本顺序准确得多。这个技巧在处理化验单时特别管用。
第三个:建立清洗日志和回滚机制。每次清洗都记录输入文件、输出结果、处理参数。发现某批数据检索效果差时,能快速定位是哪个环节出的问题。我吃过一次亏,清洗脚本改了一行正则,导致三千多条数据被误删,没有日志只能全部重跑。
4.3 性能优化的实际数据
清洗流程的性能瓶颈通常在 OCR 和 PDF 解析。我的实测数据是:纯文本 TXT 每秒处理约 5000 行,Word 每秒约 20 页,文本型 PDF 每秒约 10 页,扫描型 PDF 走 OCR 每秒只有 0.5 到 1 页。
优化手段主要是并行化。OCR 用多进程,按 CPU 核心数开进程池,8 核机器能跑到每秒 4 到 6 页。PDF 解析用多线程,因为主要是 IO 等待。这样一套组合下来,一个十万页级别的资料库,全量清洗大概需要两到三天,可以接受。
内存控制也要注意。不要一次性把所有文档加载到内存,用生成器逐文件处理。我见过有人写清洗脚本把 50GB 的 PDF 全部读进内存,机器直接卡死。
5. 检索效果验证与持续迭代
5.1 怎么判断清洗做得好不好
清洗质量最终要落到检索指标上。我用的评估方法是:人工构造 100 个典型查询,看 Top5 召回里有多少是相关的。查询要覆盖不同数据源和不同业务场景,比如“某矿区 2015 年的平均铜品位”、“ZK1201 的终孔深度”、“某矿权的有效期限”。
基线是通用方案,召回率大概 40% 左右。经过上面这套清洗流程后,召回率能到 85% 以上。剩下的 15% 主要是图件标注和手写体识别的问题,属于已知的难点。
5.2 迭代方向
清洗不是一次性的工作。新数据进来要按同样的流程处理,同时把检索日志里暴露的问题反馈到清洗规则里。我现在的做法是每月做一次检索质量复盘,把低分查询挑出来,分析是分块问题、术语问题还是解析问题,然后针对性优化。
还有一个值得投入的方向是结构化知识库与向量库的混合检索。品位、坐标、深度这类数值型查询,走结构化数据库比向量检索准得多。把两者结合,先做结构化过滤缩小范围,再做向量检索排序,效果比纯向量方案好不少。这块我还在摸索,等跑出稳定数据再单独写一篇。
这套流程跑下来,最大的体会是:探矿数据的 RAG,清洗占七分,模型占三分。与其花时间调 embedding 模型,不如把清洗做扎实。数据干净了,用最基础的向量模型也能跑出不错的效果。