做探矿业务的RAG应用时,我第一个崩溃的瞬间不是模型效果差,而是明明数据都在,检索却像在乱码大海里捞针。探矿行业这些年攒下了海量的TXT、Word、PDF和网页快照,里面有钻孔编录、岩矿鉴定报告、化探数据、储量估算表,甚至还有扫描的老地质图说明。可这些文档一进知识库,问题就全来了:TXT打开全是乱码,Word里公式和表格散架,PDF有扫描件无法复制文字,网页抓下来满屏导航和广告。不把这些源头文档洗干净,后面分块、向量化、检索全部白搭。
这篇内容我基于实际做探矿知识库的教训整理,适合正在搭RAG知识库的地质数据工程师、做行业大模型应用的开发者,以及被“文档很全但检索很废”折磨的同行。我会从乱码根源讲起,拆解TXT、Word、PDF、网页四类文档的清洗思路,再给出从清洗到高精度检索的完整落地路径,最后附上踩坑实录和问题排查速查表。
1. 探矿业务文档的“乱”从何而来:清洗是RAG精度的地基
1.1 探矿文档的典型类型与检索痛点
探矿业务不是单一文档格式,而是几十年前到现在的格式大杂烩。早年野外记录用纯文本,随后过渡到Word文档,同时大量成果以PDF形式归档,而近年来部分资料以网页形式发布。这种历史沉积造成四个典型痛点。
TXT文件是最容易被低估的坑。很多探矿TXT并非标准UTF-8编码,而是GBK、GB2312甚至更古老的编码。用默认UTF-8方式读取时,中文直接变成“锟斤拷”“烫烫烫”这类经典乱码。更麻烦的是,野外采集设备导出的TXT常夹杂着制表符、分节符和未知字符控制码,这些数据进到RAG里,分块器会切得乱七八糟。
Word文档的问题是格式嵌套。早期的.doc版本和现在的.docx混在一起,python-docx对新版能读,对老版只能靠额外工具。矿区报告里大量使用公式、表格和嵌入图片,直接提取文本时,公式会变成一段乱码,表格行被生硬拼接,图片内容完全丢失。如果文档还经过多人修改,修订标记没清干净,提取出的正文里会混入“作者:xxx”之类的元信息。
PDF是探矿业务的重灾区。一部分是从Word导出的文字版PDF,提取还顺利;另一部分是扫描老报告的PDF,纯粹是一张张图片,不跑OCR就没法拿到文字。更尴尬的是,很多探矿PDF是双栏排版,直接按页提取会把左栏的尾句和右栏的首句拼接成一句毫无逻辑的话。表格在PDF里也经常被抽成乱序文本,表头和单元格内容分离。
网页资料在探矿业务里近年来越来越多,比如公开的地质调查成果、政策通知、矿业权公示。这类页面最普遍的问题是“正文藏在一堆导航和脚本里”。直接抓全文,RAG索引里全是“点击下载”“相关推荐”“版权声明”,真正有效的地质描述被淹没。加上部分页面是JavaScript动态渲染,普通请求拿到的HTML里根本没有正文。
1.2 RAG检索精度与文档质量的因果关系
RAG的检索精度高度依赖知识库中文本的语义完整性。如果原始文档乱码、错字、格式断裂,即使切分再合理、向量模型再强,得到的embedding也是从垃圾输入中提取的。我用一个很直观的类比来解释这个因果:你把一本书复印给一个人,要求他从中找到某个观点。如果复印件的某些页变成黑白条纹,某些段落缺页,某些句子被切成了两截,这个人能在指定时间内找到完整而准确的答案,只能靠运气。
清洗的目的不是简单“把乱码变中文”,而是恢复文本的结构和语义。你要让后续的文本切分器能识别自然段落,让表格保留行列关系,让数字和专有名词不被截断,让公式有机会转成可检索的文本。这是从“能读”到“能检索”的跳跃。探矿文档里的关键信息高度依赖精确术语和数值:钻孔深度、矿层厚度、品位、坐标、岩石命名,这些一旦在清洗阶段损坏,RAG检索再多轮也找不回。
我在实践中总结出一个判断标准:清洗后的文档经过单字错误率检测后,1000字里乱码或错字不应超过5个;而结构上,段落边界清晰、表格行不被跨界拼接、公式至少能转成可读的LaTeX字符串。达不到这个标准就不要进向量库,否则后面所有调优都是白费。
2. 四类核心文档的清洗全拆解:TXT、Word、PDF、网页
2.1 TXT乱码的编码识别与修复
处理TXT,第一件事不是转码,而是准确识别原编码。我写过最蠢的代码:把所有TXT硬编码为GBK去解码,结果一批UTF-8文件被解成更严重的乱码。后来老老实实先用chardet检测编码,再解码。
import chardet def detect_and_read(path): raw = open(path, "rb").read() result = chardet.detect(raw) encoding = result["encoding"] or "utf-8" try: return raw.decode(encoding) except UnicodeDecodeError: return raw.decode(encoding, errors="replace")这个方案能覆盖绝大多数情况,但要注意chardet对短文本的检测准确率很低。探矿TXT里很多是几百字节的野外记录,此时我会用“编码探测+内容校验”的双保险:先看探测结果,再统计解码后的字符是否包含常见中文高频字,比如“矿”“层”“深”“度”。如果一段文本里全是低频怪字,大概率解码错了。
修复乱码有时不是简单解码能解决的。早年某些采集程序会把UTF-8数据以字节流形式写进GBK文件,形成所谓的“双重编码错误”。此时需要先把文本编码为Latin-1恢复原始字节,再用正确编码重新解码。这类操作不能靠猜,我会先检查十六进制字节模式:当乱码字符集中在“锟斤拷”“烫烫烫”这类典型序列时,“锟斤拷”是UTF-8被GBK解错的标准产物。遇到它,直接按“先编码成GBK字节再按UTF-8解码”的路线处理。
清理TXT还需要删除控制字符。探矿TXT里常见ASCII控制码、零宽字符、表情符号残留。正则表达式可以一次性滤掉大多数非内容字符:
import re def clean_text(text): text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]", "", text) text = re.sub(r"[\ufeff\u200b-\u200f\u202a-\u202e\u2060]", "", text) text = re.sub(r"[ \t]+", " ", text) return text.strip()这段操作看似基础,但对检索精度的提升立竿见影。零宽字符看起来无害,却会让向量模型把一个完整词拆成两个残缺token,导致语义偏移。
2.2 Word文档解析与内容归一化
Word清洗不能只盯着正文文本。探矿报告里最有价值的往往是表格和公式,而这两者恰好是纯文本提取最容易丢失的部分。
对于.docx,我优先用python-docx解析文档主体,但需要注意表格的读取方式。表格若直接遍历cell.text拼接到段落列表里,会丢失表头与数据行之间的关系。正确做法是把每个表格结构化为二维数组,再用统一的表格序列化格式保存。我一般将其转成Markdown表格,这样后续切分器可以识别出表格边界。
from docx import Document def extract_docx(path): doc = Document(path) blocks = [] for para in doc.paragraphs: if para.text.strip(): blocks.append(para.text.strip()) for table in doc.tables: rows = [[cell.text.strip() for cell in row.cells] for row in table.rows] header = rows[0] if rows else [] md = "| " + " | ".join(header) + " |\n" md += "| " + " | ".join(["---"] * len(header)) + " |\n" for row in rows[1:]: md += "| " + " | ".join(row) + " |\n" blocks.append(md) return "\n\n".join(blocks)关于Word公式,提取纯文本会得到乱码,但探矿报告里的公式很多是储量计算、化验分析的表达式,语义价值很高。我会检测文档中的OMML公式节点,使用latex2mathml之类工具将其转换成LaTeX字符串。转换后的公式虽然不适合直接录入向量库,但经过文本化处理后,检索时能够匹配到“品位计算公式”这类关键描述。
.doc老格式模块没有的坑。python-docx无法直接处理.doc文件,我的做法是先用LibreOffice命令行批量转成.docx。探矿机房的电脑未必装了LibreOffice,但用soffice --headless --convert-to docx转一次后,整个清洗管道就统一了。这里有个性能提醒:百份Word文档批量转换可能耗时十几分钟,最好做成离线预处理任务,不要放进在线检索链路。
2.3 PDF解析:文字版与扫描版的两条路线
PDF清洗必须区分“文字版”和“扫描版”。我用PyMuPDF快速检测页面中是否有可提取的文本层,如果一页的文本长度低于阈值,就判定为扫描页,进入OCR分支。
文字版PDF提取时,探矿报告的双栏排版是最大的坑。直接用page.get_text("text")会把双栏的阅读顺序打乱。我的做法是使用page.get_text("blocks")获取文本块的坐标信息,然后按x坐标将页面分成左右两区,再分别按y坐标排序。
import fitz def extract_pdf_onepage(page): blocks = page.get_text("blocks") left, right = [], [] mid = page.rect.width / 2 for b in blocks: x0, y0, x1, y1, text, *_ = b if text.strip(): (left if (x0 + x1) / 2 < mid else right).append((y0, text.strip())) left.sort(key=lambda t: t[0]) right.sort(key=lambda t: t[0]) return "\n".join(t[1] for t in left) + "\n\n" + "\n".join(t[1] for t in right)这个方案能解决90%的双栏提取问题。但是遇到三栏或者页眉页脚乱入,还需要再加规则:过滤掉字体明显小于正文、坐标贴近页面边缘的文本框。
扫描版PDF是探矿老资料的常态,必须OCR。我团队实际使用PaddleOCR,它对中文地质术语的识别率远高于Tesseract。跑OCR前需要先对图片做预处理:灰度化、去除噪点、增强对比度。扫描老报告的背景常有黄色底纹和污渍,不经处理直接OCR,识别率掉得很快。这里有几个关键参数可以调:
from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch", show_log=False) result = ocr.ocr(img, cls=True)use_angle_cls参数对旋转页面很有效,老报告扫描时经常歪斜,不启用方向分类,整段文字识别可能全错。OCR出的文本经过后处理时,我发现探矿地名人名容易把“垭”识别成“娅”、“熔”识别成“溶”,需要维护一份专业术语纠错词典,用字符串替换来修正。
PDF表格提取比文本更麻烦。扫描版表格我没找到完美方案,目前是用OCR单元格+人工抽检。文字版PDF表格可以用pdfplumber按表线识别,但探矿表格的边框经常残缺,我会退一步,用get_text("words")按坐标聚类成行,再按行还原表格。即使如此,复杂合并单元格仍然可能错位。经验是:表格清洗不必强求100%还原结构,如果行与行之间能保留明确的语义边界,后续分块器就不会把不同钻井的数据混合。
2.4 网页抓取与正文清洗
网页清洗的目标是“去掉所有人不想看的,只留正文”。探矿业务相关的网页,我用BeautifulSoup配合lxml解析,优先命中文章主体标签,比如<article>、<div class="content">,但不硬编码,因为不同网站结构差异巨大。更可靠的做法是使用一套启发式规则:删除script、style、nav、footer、aside;过滤文本长度小于10字节的元素;统计页面内文本密集度,把文本密度最高的区域视为正文。
from bs4 import BeautifulSoup def extract_main_html(html): soup = BeautifulSoup(html, "lxml") for tag in soup(["script", "style", "nav", "footer", "aside", "form", "iframe"]): tag.decompose() body = soup.body or soup best_node = None best_score = 0 for p in body.find_all(["p", "div", "section"]): text = p.get_text(" ", strip=True) if len(text) < 50: continue score = len(text) if score > best_score: best_score = score best_node = p return best_node.get_text("\n", strip=True) if best_node else ""对付动态渲染页面(比如用React、Vue搭建的矿业信息平台),普通requests拿到的是空壳HTML。我的方案是先尝试用requests-html或Selenium渲染,但这两者都比较笨重。实际项目里,我更倾向直接用浏览器开发者工具找到数据的真实API接口,很多网页的正文其实是加载完JS后从JSON接口拿到的,直接请求那个JSON能省掉所有渲染成本。这一步算是个逆向技巧,对技术能力要求稍高,但清洗速度提升非常明显。
网页抓下来的正文还要做基础归一化:把连续多个换行压缩为一个,去掉“免责声明”“点击展开”“上一篇下一篇”等噪音段落。我在规则维护上会建立一份“废弃短语黑名单”,针对探矿信息网站常见的“版权所有”“本页地址”做正则删除。
3. 结构化提取、元数据保留与清洗质量评估
3.1 探矿关键字段的抽取与结构化
清洗不只是恢复文本,还要让RAG能够精准检索到关键信息。探矿报告中反复出现的关键字段有矿区名称、地理位置、钻孔编号、孔深、矿体类型、品位值、岩性描述。如果能把这些字段从全文里抽出来并作为文档元数据保存,检索时可以前置过滤,大幅提升精度。
我用两种方式做字段抽取。一是正则+规则引擎,适合结构相对固定的文本。比如钻孔编号常见为“ZK01”“ZK-2023-018”这种模式,坐标常用“东经117°15′30″”这种表达。二是基于实体识别的方式,如果探矿领域有自己的NER模型,可以直接跑一批标注数据训练,但小团队很难有足够标注样本。更务实的方案是维护一套“领域词典+正则”的轻量抽取器,把匹配到的字段填入文档头部。
清洗管道里,我会为每份文档生成类似下面的元信息:
矿区: 洛南某多金属矿区 资料来源: 2008年勘探报告.pdf 钻孔编号: ZK-12 孔深: 320.5m 矿体类型: 铜锌矿这段元信息单独存储在向量库的metadata字段中,检索时用filter控制。这样一来,用户问“ZK-12孔的铜品位”,知识库可以先按钻孔编号过滤,再在片段级做语义匹配,不会捞出一堆其他钻孔的无关内容。
3.2 清洗后的质量评估方法
清洗管道跑完,不能直接说“干净了”,要用可量化指标评估。我常用的三个指标:字符错误率、段落完整度、检索命中率。
字符错误率很好算,找一份人工校对过的标准文档作为Ground Truth,用编辑距离计算清洗结果与标准文本的差异,除以总字符数。探矿报告里的数字尤其敏感:孔深320.5m如果清洗中把小数点丢掉,检索到“3205m”就完全失真。所以我评估时会给数字字符更高权重。
段落完整度是指清洗后文档中段落边界与原文段落的对齐程度。如果一个自然段被拆成十个小碎片,或者两段被拼成一段,都会影响后面的语义切分质量。我写过一个简单的统计器:计算清洗结果的换行符与断言式段落标记(例如“第 段”“岩矿鉴定结果”)的匹配数,但这个只能作为辅助参考,最终还是要抽检。
检索命中率是最贴近业务效果的评估方式。我会准备20条探矿领域的典型问题,比如“某矿区铅锌矿的平均品位是多少”“ZK-05钻孔的终孔深度和岩性变化”,然后分别用清洗前和清洗后的文档建立向量库,看top5检索结果的准确率。污染严重的TXT文件清洗后,检索命中率往往能从不足40%拉升到75%以上。
3.3 清洗管道的设计与自动化
清洗不是一次性脚本,而是需要持续运行的管道。探矿资料持续新增,每次导出一批新文件夹,都要跑一遍相同流程。我会用Python把各个子步骤封装成函数,然后用Prefect或Airflow编排依赖。小项目不想上重型调度工具,直接写一个pipeline.py按顺序调用也可以。
管道节点包括:文件加载、格式检测、编码修复、解析提取、结构归一化、字段抽取、质量评估、输出向量化JSON。中间任何一步失败,都应该单独生成日志文件,而不是让整个任务中断。我踩过的最深的坑是某批次PDF里混了一个密码保护的加密文件,OCR程序在这页卡死了半小时。后来给解析函数加了单文件超时和异常捕获,确保一个坏文件不影响整批处理。
4. 从清洗到高精度RAG检索的衔接实践
4.1 清洗后的分块策略
清洗完成只是第一步,分块策略决定检索粒度。探矿文档有着丰富的层级结构,我在分块时优先利用这些自然边界:先按章节分割,再按段落分割,最后把相邻段落按窗口合并成块。这样比单纯按固定字符长度切分要精准得多。
针对表格,我不把整个表格塞进一个块,而是按表头和若干数据行生成多个小块。比如一个钻孔综合记录表有几十行,每行代表不同深度的岩性,如果把整表嵌入,检索“孔深200米处的岩性”时,向量会被其他深度行干扰。拆行之后,每一行自带表头语义,检索准确率明显提升。
分块还应该保留重叠。我用滑动窗口,窗口大小根据文档类型动态调整:普通叙述文本用512字符,表格内容用256字符,重叠比例设为20%。这样的好处是,跨越两个块的查询词不会因为分块边界被切断。
4.2 清洗前后检索效果对比
我拿一个真实案例做对比:某矿区20份Word报告加30份PDF扫描件,直接使用默认PDF解析器提取文字后建知识库,再走一遍完整清洗管道后重建知识库。
清洗前,检索“西北角矿体的品位变化趋势”,返回的前5个结果有3个来自页眉页脚和无关表格;清洗后,同样的查询,前3个结果分别是矿体的化探数据表、文字报告中的矿体描述段、储量估算表格。实际效果验证了清洗的价值不只在消除乱码,更在释放结构信息。
这个对比不是停留在“看起来变好了”的主观感受,我用top5准确率计算:清洗前为42%,清洗后为78%。如果继续加入元数据过滤和同义词词典,可以提升至85%以上。RAG应用上线前,建议先做一次这样的清洗前后对比,既能验证管道质量,也能给团队一个明确的性能基线。
4.3 检索增强层面的进一步优化
清洗之后的RAG想做到“高精度”,除了文本干净,还要在检索策略上做文章。我通常用混合检索:关键词稀疏检索加向量稠密检索。探矿领域的专业术语很多是低频词,比如“矽卡岩”“辉绿岩脉”,向量模型未必把它们表征得足够好,加上BM25关键词匹配能覆盖这些长尾术语。
同时要建立同义词扩展。探矿报告里的“铅锌矿”和“铅锌多金属矿”在语义上等价,但在字符串层面完全不同。我维护了一个领域同义词表,在查询时先做同义词扩展再生成向量,能有效提升召回。这些优化建立在清洗之后的干净文本上,文档杂乱时再好的检索算法也会被无效内容稀释。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| TXT打开后是“锟斤拷” | UTF-8字节被GBK解码 | 先重新编码为GBK字节,再按UTF-8解码 |
| Word提取缺表格内容 | 表格未写入段落块 | 单独遍历table并格式化为Markdown |
| PDF提取顺序错乱 | 双栏未按坐标分隔 | 按文本块x坐标分左右两区排序 |
| 扫描PDF识别率极低 | 图片未预处理 | 灰度化、降噪、增加对比度后再OCR |
| 网页正文全是无关导航 | 抓取了全HTML | 用文本密度启发式提取主区域 |
| 检索结果包含乱码片段 | 清洗管道有漏网文件 | 检查异常日志,对可疑文件重新处理 |
| 动态网页请求不到内容 | JS渲染后才生成正文 | 寻找页面真实API或使用浏览器渲染 |
| PDF有加密文件卡住管道 | 密码保护导致解析无响应 | 为解析函数增加超时和异常跳过机制 |
5.2 实操中踩过的坑
清洗管道里最坑的不是单类文件的解析,而是“混合格式文件夹”。我处理过一个项目,一个文件夹里同时有TXT、doc、docx、PDF、扫描版PDF、HTML文件,很多文件后缀名与实际格式不符。比如文件名是.txt,打开二进制流却发现是Word文档。后来我在管道起点增加“格式嗅探”环节,读取文件头部字节判断真实格式,而不是信任后缀名。这一步至少避免了30%以上误解析。
OCR后处理必须做术语纠错。探矿行业专有名词非常多,OCR模型很容易把“锰矿”识别成“锰扩”,把“石英脉”识别成“石英服”。我维护了一个面向矿种名、岩石名的纠错词典,用简单的字符串替换就能显著提升检索命中率。需要注意替换顺序,长词优先后,防止“铅锌矿”被“铅锌”部分替换造成语义缺失。
还有一个小技巧:清洗后的文档不要直接丢弃原始文件。我会把清洗结果与原始文件一一映射,保存一份manifest.json,记录来源文件、清洗版本、检测结果。这样检索结果出现问题时,可以快速定位是清洗阶段的哪一个环节出错,而不是像考古一样重新翻原始资料。
5.3 一条可复制的清洗迭代路径
如果你刚开始做探矿RAG,我的建议是先挑10份典型文档,覆盖四种格式,跑通最小清洗管道,再扩展到全部资料。不要一上来就想处理完所有历史文件,那样只会被无数边角问题淹没。
先把TXT编码、Word段落表格、PDF文字版双栏、网页正文提取这四个主干建好,再逐步加入扫描版OCR、公式转换、元数据抽取、同义词扩展。每一步都跑清洗前后检索对比,效果有用就保留,没用就砍掉。清洗管道不是越复杂越好,很多看起来高大上的步骤(比如深度学习OCR纠错)在实际数据量不大时,收益可能不如一个专科名词词典。
我个人在实际操作中的体会是,探矿业务的RAG清洗,本质上是把“几十年前混乱资料”翻译成“模型能理解的结构化文本”。这个过程不酷,但非常踏实。每修好一个乱码文件,检索结果就多一分可信度。如果你也被乱码搞到怀疑人生,先从最简单的一份TXT开始,把它读到屏幕上正常显示,再谈高精度检索。