☰
矿山地质资料RAG清洗实战:从乱码到高精度检索
2026/10/8 14:31:39 网站建设 项目流程

进了矿山地质资料那批老文档之后,我第一反应是头痛。几百份 TXT、Word、PDF 混在一起,还有从网页上抓下来的公告和矿权信息,字符一会儿是乱码,一会儿是半截表格,标题里写“从乱码到高精度检索”,这句话基本就是我接手 RAG 清洗工作的真实写照。这里面的 RAG,指的就是检索增强生成那套技术路线,但很多人一上来就追着模型和向量库调参,忽略了最前面的清洗环节。探矿业务的特点是资料杂、专业词多、格式老,如果不先把 TXT、Word、PDF 和网页四类来源的乱码和结构噪声处理干净,后面做高精度检索就是空中楼阁。

这篇文章不会写成教科书式的流程文档。我只讲我在实际项目中拆解乱码、整理文本、构建检索链路的完整思路,每一步都对应可复现的操作方案。适合正在做 RAG 落地、尤其是地质矿产类文档处理的技术人员参考,也适合刚接触数据清洗、想搞懂为什么解析之后文本还是乱的初级工程师。

1. 探矿资料入RAG前,乱码与错字如何拖垮检索精度

先说一个反直觉的结论:多数 RAG 项目准确率上不去,问题不出在模型,出在文档进入向量库之前的那一步。Embedding 模型见过大量正常文本,但它不会知道你这份文档里藏着多少个解码错误字符,也不会自动帮你把错位的表格行重新拼回去。乱码一旦进入向量化环节,就会被当作正常语义进行编码,检索时自然就南辕北辙。

1.1 乱码不是小概率问题,而是数据现状

探矿业务的文本来源极其特殊。很多老矿山资料是当年在 DOS 环境下录入的,保存为 GBK 编码的 TXT;后来从档案系统导出的文件也可能是 GB2312 或 GB18030。还有一部分 Word 文档经过了多次转存,某些段落用了旧版公式编辑器,页面里还嵌着扫描图。PDF 的情况更复杂,有的有文本层,有的干脆就是扫描件整页变成了图片。再加上从政府公示、行业网站抓取的网页,动态渲染、编码声明与实际内容不符的情况比比皆是。

这些资料混在一起,如果不做针对性清洗,你看到的现象就是:检索“XX矿区ZK0301钻孔见矿深度”,返回结果里出现大量“�”字符,或者把第二页页眉当成正文切片。更隐蔽的问题是中文标点被转成英文标点后没影响肉眼阅读,但会让模型语义向量出现偏移,专业术语“g/t”与“克/吨”之间明明等价,检索系统却认为它们是两个完全不同的概念。

1.2 乱码对四种检索能力的伤害程度不同

我一般把探矿 RAG 的检索分四类来评估:全文本语义检索、关键词精确匹配、表格数据查询、坐标和数值范围查询。乱码对这四种模式的影响并不一样。

语义检索最怕“字符层面残缺”。一句“Cu平均品位1.24%”,如果变成“Cu平均品��1.24%”,Embedding 模型会把“品”和“1.24”之间的距离拉远,可能最终召回的段落是“该矿区地下水流向为北东”,因为这句话里同样有“Cu”和“1.24”相关的地球化学背景词,语义空间错配了。

关键词精确匹配则被“全角半角”和“编码替代字符”直接击穿。“ZK0301”写成“ZK0301”,关键词就检索不到。坐标格式也是重灾区:“40°30′30″”和“40.5083°”表达的是同一个位置,但字符串完全不同。探矿检索场景里,这些数字不是普通的数字,是判断矿体走向、孔深、品位的关键索引,一旦解析出错,后面所有分析都建立在错数据上。

表格数据查询主要依赖结构完整性。PDF 里的化验结果表如果提取出来变成一长串文本,行和列全部错位,表面上看数据都在,实际上字段名和数值根本对不上。RAG 的检索粒度是“语义相近的句子”,它无法根据“Cu”这个表头自动知道下一列是“Au”,所以你必须提前把表格转化为有明确列分隔的文本,甚至直接走结构化存储。

数值范围查询更直接,它要求数字必须保持数字形态。清洗过程中一旦用 errors='ignore' 这种参数把非法字符静默丢掉,百分号、小数点、负号就可能一起消失。这造成的后果比乱码还严重,因为至少乱码还能提醒你信息丢了,静默丢弃是无声的。

2. 四大来源逐个拆:TXT编码、Word对象、PDF扫描、网页动态内容

乱码的成因不同,清洗办法就不能一套打天下。我按来源分开处理,每一类都先判断它最容易坏在哪个环节,再决定用什么工具链。

2.1 TXT 老档案:编码是原罪,先做编码识别再统一

老探矿档案里的 TXT 大多数是 GBK 编码,但也存在少量 UTF-16 和 GB18030。最保险的做法不是让 Python 猜一次编码就结束,而是把几种常见编码都尝试一遍,再用汉字比例、连续可读长度、特殊符号密度做一轮质量打分。

import chardet raw = open("zk0301_old.txt", "rb").read() guess = chardet.detect(raw) print(guess) # 常见结果:{'encoding': 'GB2312', 'confidence': 0.99} try: text = raw.decode("gbk") except UnicodeDecodeError: text = raw.decode("utf-8", errors="replace")

这里有个关键细节:不要用 errors='ignore'。一旦忽略非法字符,整个数字串可能断成两截,而你根本不知道断在哪里。我建议统一用 errors='replace',把无法解码的字符替换成特殊占位符,比如“�”,然后在清洗阶段用正则把连续的占位符行挑出来,交给人工或 OCR 流程二次确认。

编码统一之后,还要处理 TXT 里常见的“旧式表格”。这类文本一般用空格或制表符对齐列内容,肉眼看着是整齐的表格,但输入到模型里只是一堆空白字符。我的做法是把连续两个以上空白字符统一替换为制表符,再用 pandas 按行拆分,保留分隔符结构,而不是让所有内容糊成一段话。

2.2 Word 文档:公式、图片、批注和宏都可能偷走内容

很多人以为 Word 转 txt 是最简单的,实际上 Word 在探矿业务里最容易丢内容。地质详查报告经常包含大量公式,比如储量计算的加权平均公式、抗压强度表达式,这些内容如果是以旧版公式对象或 MathType 公式形式存在的,python-docx 默认是读不到文本的。你取到的段落可能是空的,或者只有公式前后的说明文字。

比较可靠的路线分两步。第一步,用 LibreOffice 命令行把 docx 转成 txt 或纯文本,期间关闭宏执行,避免来自外部文档的宏在转换过程中触发意外操作。第二步,对仍无法解析的公式对象做专项提取。公式在 docx 内部其实是一段 OMML 或嵌在 document.xml 里的对象引用,你可以用 python-docx 遍历所有的 oMath 节点,把它转成 UnicodeMath 或 LaTeX 文本,再把 LaTeX 字符串作为该段的附加内容接回去。

soffice --headless --convert-to txt:Text --outdir ./clean_txt ZK0301_详查报告.docx

这里强调一下安全问题。Word 宏在纯文本提取阶段是可以躲开的,但如果你用 Windows COM 的方式调用 Word 打开文档再另存为纯文本,宏可能被执行。地质资料经常在多个单位之间互相传输,你不知道这份 docx 里的宏到底写了什么。稳妥起见,所有 Word 转换一律放在禁用宏的模式下,或者直接放到 Linux 环境里跑 LibreOffice。

表格是另一个坑。探矿报告中的钻孔岩芯采样表、水文观测表往往嵌在文档的文本框里,python-docx 默认遍历不到文本框内部。你要做的是写一个递归函数,把 document.element.body 下的 w:txbxContent 全部找出来,再继续解析其中的段落和表格。我第一次做的时候没处理文本框,结果一份报告少了三分之一的数据,检索精度直接崩到没法看。

2.3 PDF 扫描件与文本层混合体:提取密度阈值决定走OCR还是直接取

PDF 在探矿资料里严重两极分化。新版电子勘查报告有完整文本层,老扫描图件则完全没有。前端时间我看到有人推荐用 PyMuPDF 一步搞定,但拿到扫描件立刻翻车,因为根本没有文本层。我的判断方式很简单:对每一页先提取文本,计算非空格、非换行字符的数量,如果低于该页面积的某个阈值,就判定为扫描页,交给 OCR。

Python 里我习惯用 PyMuPDF 快速抽文本层,用 pdfplumber 处理表格边界。PyMuPDF 的 text 提取速度快,适合大规模过滤;pdfplumber 读取的表格结构更接近视觉上的行列,适合处理化验结果表。

import fitz doc = fitz.open("2023_ZK0301_岩芯化验.pdf") for page_no in range(len(doc)): page = doc[page_no] txt = page.get_text("text") txt_stripped = "".join(txt.split()) if len(txt_stripped) < 20: print(f"page {page_no+1} 疑似扫描页")

扫描页 OCR 并不复杂,探矿资料里的正文多为印刷宋体字,PaddleOCR 中文识别效果已经足够。难点在图表注释和小字号数字上,地图、剖面图、柱状图上的钻孔编号和标高数字非常容易被认错。我通常先把页面图像放大到 300 DPI,再转灰度、做一次去噪,然后才交给 OCR 识别。识别之后的文本还带着坐标位置,可以用坐标信息把同一表格框内的文字重新拼接,避免表格内容被 OCR 输出成多列混排。

最后还要处理 PDF 中“嵌入式网页打印版”。有些来源是网页转成的 PDF,页眉页脚、目录、链接全部带进来,提取之后满页面都是网址和导航文字。这种 PDF 清洗时要把页眉、页脚按页面位置过滤,只保留中间正文区域,同时对连续的页码行做正则删除。

2.4 网页内容:动态渲染和编码跳变是主要来源

探矿 RAG 的语料库不可能只用本地文件,矿权公示、行业新闻、招投标信息都要从网页抓取。网页清洗的核心问题是“抓到的东西和浏览器看到的不一样”。静态 HTML 明明没有数据,新闻正文是通过 JavaScript 异步加载出来的,如果你用 requests 直接拿 HTML,得到的是一堆 script 标签和毫无意义的外壳。

正确做法是分两层抓取。第一层先用 trafilatura 或 readability-lxml 抽取正文候选区,把 script、style、nav、footer 全部剥掉。第二层如果正文区为空或异常短,再用 Playwright 或 Selenium 无头浏览器加载一次,等网络请求完成后再提取。探矿公示类页面的表格通常是服务端渲染,用 requests 就能拿到;新闻类和行情类页面则多半是前端渲染,必须走无头浏览器。

import trafilatura downloaded = trafilatura.fetch_url("https://example-mining-page/notice") text = trafilatura.extract(downloaded, include_comments=False, include_tables=True, favor_recall=True)

编码方面网页最坑的是“声明编码与实际编码不一致”。HTML 的 meta 标签写的是 UTF-8,实际内容却是 GBK,这种文件直接喂给解析器,前几百字节正常,后面的中文全变乱码。我在抓取时会把响应头里的 charset、HTML meta 声明的 charset、字节层面的实际编码结果三个信号放在一起判断,以 chardet 给出的置信度为主,如果 chardet 说 GB18030 而非页面声称的 UTF-8,就强制用 GB18030 解码。

网页里的图片注释要单独处理,尤其是工程图纸扫描件转成的网页图片。这时候必须走 OCR 流程,把图片里矿区边界线上的地名、坐标点识别成文本,再把 OCR 结果作为该网页的额外文本字段,挂在同一份文档 ID 下。这样检索时既能看到网页正文,也能检索到图片内部的地名信息。

3. 从清洗到入库:我用pandas与SQL做的三层数据治理

文本解析完成只是清洗的第一步,后续还需要把清洗后的内容组织成适合入库的结构。我把这一步拆成三层:去重、规范化、低质段落标记。三层做完,文本才能干净利落地进入向量化和检索环节。

3.1 去重不只看标题,指纹碰撞才可靠

探矿资料特别喜欢重复。同一份报告可能同时存在 Word 版和 PDF 版,内容一字不差,但文件头、页眉、版本编号略有不同。如果只按整个文件 MD5 去重,这两份会同时进入知识库,检索时重复段落占用大量召回窗口,明明只有一个答案,却频繁返回两段相似文本,徒增误导。

我的做法是用规范化哈希。先把整篇文本做一层标准化:全角转半角、转换成统一换行符、去掉所有空白字符、过滤掉页眉页脚,然后计算 SHA256。Word 和 PDF 经过不同解析器提取出来的文本,在标点、空格上会有一点差异,但经过规范化之后,两者应该能撞上同一个指纹。

对于部分逻辑复制导致的内容性重复,比如 A 章节从旧报告中复制了整段,但段落开头改了两个字,哈希指纹会失败,这时再用 SimHash 或 MinHash 计算局部相似度。在 pandas 里可以先把每篇文档的行拆成短词窗口,计算每行的 simhash,再两两比较。这样能揪出 90% 以上的整段复制文本。

3.2 规范化和元数据注入:矿区编号、坐标格式、单位统一

清洗到这一步,文本内容基本是正常人了,但“正常”不等于“可检索”。探矿文本有很强的约定俗成特征,比如品位单位“g/t”在不同报告里可能写成“克/吨”“克每吨”“g·t⁻¹”,经纬度有度分秒和十进制度的差异,甚至钻孔号“ZK0301”在表格里被写成“ZK-03-01”。这些表达差异会直接拉低关键词检索的精度。

我在 pandas 里建了一张映射表,用正则把常见的异体写法统一。单位统一放在文本清洗之后、切块之前,因为切块之后再做替换容易破坏分块边界。

import pandas as pd df = pd.read_csv("parsed_docs.csv") df["content"] = df["content"].str.replace(r"[gG]\s*[·/tT]\s*", "g/t", regex=True) df["content"] = df["content"].str.replace(r"克/吨|克每吨|克吨", "g/t", regex=True) df["content"] = df["content"].str.replace(r"[((]?ZK[--]?0*([0-9]+)[))]?", r"ZK\1", regex=True)

这种替换必须基于对探矿业务的理解,不能只看通用文本清洗规则。如果不知道“ZK0301”和“ZK-03-01”在矿区台账里可能指同一个孔号,这条规则就不会被写出来。所以 RAG 清洗不是纯数据处理任务,还要配合业务人员梳理一份“专业术语归一化对照表”,至少覆盖品位单位、坐标格式、钻孔编号、地层代号和主要矿物名称。

3.3 低质量段落标记:让后续切块绕开噪声区

不是所有文本都值得进入向量库。探矿文档里大量存在页眉页脚、表格序号行、纯页码、录入时遗留的空行和背景说明文字。与其在切块阶段依赖模板去猜,不如在入库前就把低质量段落标记出来。

我用一个简单的打分函数:统计每段文本的汉字占比、数字占比、符号占比、是否只包含数字与空白、是否包含页眉特征词。得分低于阈值的文本段落打上 low_quality 标签,放进单独字段。切块器读取到该字段时直接跳过,或只在整篇缺少正文时才考虑使用这些段落。

这个步骤对检索精度的影响非常明显。我测试过一个包含大量 OCR 误识别的扫描版报告,把低质量段过滤后,相同问句的检索命中率提升了 21%。原因很简单,乱码和噪声段落占据向量空间的区域太广,把真正的矿石特征词淹没在无关向量里了。

4. 切块与检索精度:清洗后的文本在RAG管线的最后100米

很多人以为文本清洗完了就大功告成,其实切块和检索之间的配合同样决定最终精度。探矿文本的切块不能用“固定 512 字一刀切”,因为地质报告中章节语义长度差异巨大,一句话可能就十几个字但信息密度极高,一个章节表格可能连续上百行却只围绕同一钻孔。

4.1 切块策略:按文档结构分层,再按长度微调

我的优先策略是“标题优先切块”。先把清洗后的文档按标题层级拆成一棵目录树,标题在探矿报告里就相当于勘探阶段、矿区范围、地质构造、矿体特征、储量计算这些语义边界。每一个二级标题下的内容作为一个候选块,如果长度超过模型上限,再按段落向前滑动,滑动步长控制在文本窗口的三分之一左右。

这样切出来的块有一个好处:每一块的语义是完整的,不会把一个钻孔柱状图从中间拦腰截断。钻孔柱状图的数据表经常在“ZK0301”和“ZK0302”之间有明显标题分隔,标题优先切块能天然保留这种边界。

表格在切块前需要特殊处理。我用 pdfplumber 或 pandas 把表格转成 tab 分隔的纯文本后,会显式加入列头重复逻辑:如果表格超过一定行数,切块时每隔若干行重复一次表头。否则模型在检索“品位”时看到的可能只是中间行数据,却不知道这一列对应的是哪个元素。

4.2 元数据注入:把矿区、孔号、坐标、文件类型塞进检索过滤条件

高精度检索不能完全依赖语义相似度,元数据过滤可以帮你把检索范围从“整个知识库”缩小到“某个矿区、某份报告、某个钻孔”。比如用户问“ZK0301 的见矿深度”,如果元数据里已经拆出 doc_name=台帐.xlsx、drill_id=ZK0301、file_type=excel,检索系统就能先过滤出相关候选集,再让向量模型做精排。

我把元数据字段设计成两套。一套是文件级元数据,包括文档来源、文件格式、入库时间、矿区编号、版本号;另一套是块级元数据,包括所在二级标题、页码、表格是否存在、是否含坐标。块级元数据里的“是否含坐标”特别有用,因为探矿问题的坐标相关性很强,用户问“钻孔孔位坐标”时,某些干巴巴的岩石描述文本虽然包含经纬度,但根本不是答案来源。

4.3 评估闭环:用30个业务问句定期回测清洗质量

清洗做得好不好,最终要靠业务问题来验证。我每个项目都会建立一个包含 30 个左右真实问题的评估集,覆盖矿区查询、钻孔参数、品位、坐标、储量、矿石类型这几个维度。每次清洗管线改动之后,就把这些问题跑一遍,对比检索命中的 top5 中有多少比例是正确段落。

例如:“XX矿区ZK0301钻孔见矿深度多少米”“辉绿岩地层厚度数据在哪份报告里”“Mo元素平均品位超过 0.05% 的样品集中在哪个孔”“某矿区的边界坐标是多少”。这些问题如果命中率在清洗前后没有显著变化,说明清洗策略可能还没打中要害。我把前后对比记录成一张简单表格。

问句类型清洗前top5命中率清洗后top5命中率主要改善原因
钻孔参数58%82%Word文本框内容补全 + 去重
品位与元素44%79%单位归一化 + 表格结构还原
坐标范围36%71%OCR修复 + 坐标格式统一
储量计算51%86%PDF文本层提取 + 低质段过滤

这个评估集建议沉淀成独立脚本,每次抓完新网页或转完新 PDF,先跑一遍,稳了再增量入库。否则今天刚跑的准确率是 80%,明天灌入一批没有清洗的网页又跌回 60%,你都不知道是哪一批数据惹的祸。

5. 可以直接抄的坑位清单与配置

踩坑记录比原理更有价值,这一节把我在探矿业务 RAG 清洗过程里遇到的高频问题压缩成清单,按环境、工具、数据三类分开。

5.1 环境坑:Windows、Linux、Mac 的转换差异要提前锁定

探矿单位的技术环境非常杂,有人用 Windows 服务器,有人用 Mac 笔记本,有人用 Linux 容器做定时任务。同一个 LibreOffice 转换命令在 Windows 上需要注意文档被 Word 进程占用的问题,Mac 上则是字体路径和中文文件名的问题,Linux 容器最常缺中文字体,导致 PDF 提取出的空格异常多还是其次,OCR 出来的汉字全是方块更致命。

我建议把清洗这条链路的运行环境在项目一开始就固定下来。如果你要在 Mac 或 Windows 上做调试,最后部署到 Linux,至少要提前安装好中文字体,比如 Noto Sans CJK、文泉驿正黑,并且把字体缓存重建一次。不要等到跑完 5000 份 PDF 才想起来看 OCR 效果。

5.2 数据坑:页眉页脚、目录、修订标记都会伪装成正文

有几类文本特别容易逃过清洗。第一是目录页,Word 自动生成的目录在转纯文本后保留了大量点线和页码,看起来像正文,实际全是重复信息;第二是修订和批注,DOCX 解析时如果没排除 w:ins、w:del 节点,会把修改前后的重复内容全部带进纯文本;第三是 PDF 的链接注释文本,部分 PDF 提取后会把超链接地址作为正文追加到段落里。

针对目录,我一般按“目录”标题位置定位,把目录区直接截断;针对修订和批注,python-docx 读取时要用解析器区分插入和删除节点,通常只保留删除前的原文或删除后的最终内容,具体看业务文档基线;针对 PDF 链接,提取用 PyMuPDF 的 page.get_text() 时关闭 links=True 选项,或者提取后正则过滤掉以 http 开头的孤儿文本。

5.3 清洗前置检查单:每批新文档上线前过一遍

我最后保留的检查单只有十项,但每项都能避免一次线上翻车。每批新文档入库前,我都会跑一遍这个检查单。

  1. 编码统一为 UTF-8,原始编码信息记录在源的属性字段里。
  2. 全角标点全部转为半角,数字与单位之间的空格规则固定。
  3. 用替换符替代 ignore 式错误处理,乱码行标记不删除。
  4. PDF 按页提取文本密度评分,扫描页走 OCR 流程。
  5. Word 转换时禁止宏执行,文本框内容递归提取。
  6. 表格统一转成 tab 分隔文本,长表格明确保留列头。
  7. 网页抓取先过滤 script、style、nav,再抽正文。
  8. 指纹去重与 SimHash 局部相似度双跑。
  9. 坐标、品位、钻孔号等专业术语统一为对照表形式。
  10. 跑 30 个核心问句评估集,top5 命中率达标后再增量入库。

这套检查单看起来繁琐,但第一次跑通之后基本就是自动化脚本的事。真正花时间的反而是“业务对照表”的维护,需要和总工办、地质组不断地核对单位、编码、术语版本。清洗不是一次性的活,它更像是一套随语料库扩大的持续过程。

我现在的习惯是,每收到一批新资料,先拿检查单过一遍再入库。只要这一步稳住了,后面的切块、向量化、重排和 RAG 生成才能站得住脚。从乱码到高精度检索的距离,往往不是模型的参数量,而是你在清洗阶段有没有舍得花时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询