直接开聊。这几年给矿区做数据整理,最崩溃的往往不是勘探取样那套流程,而是你攒了一堆钻孔编录、化验报告、储量估算说明,高高兴兴把它们塞进RAG知识库,结果检索一问三不知。不是模型不行,是你喂进去的文本本身就是一堆找不到北的“字符尸体”——乱码、错位、表格碎块、扫描图糊成一团,模型拿这些玩意儿做向量化,能检索出什么才怪。
这篇文章我想把探矿业务里TXT、Word、PDF和网页抓取这四类文本的RAG清洗思路完整捋一遍。核心就一句话:清洗不是把乱码改对,而是让文本变成RAG能吃、能切、能检索的结构化知识。
1. 乱码只是表面症状:脏文本是怎么一步步拖垮RAG检索的
很多朋友一听到“清洗”,第一反应就是编码转换。觉得把GB2312的TXT转成UTF-8、把PDF里的乱码重新解析一遍就完事了。真不是这么简单。乱码只是最表层的问题,更致命的问题藏在后面。
1.1 探矿报告里的“字符尸体”长什么样
先说说我实际见过的场景。某矿区的地质队发过来一批老钻孔资料,是当年用老式系统导出的一堆TXT。用文本编辑器打开看着很正常,但丢给Python一读,三分之一的内容显示成“锟斤拷”“烫烫烫”这种经典乱码。这里面有两种原因:一是文件本身是GB2312编码,被按UTF-8解析了;二是文件在传输或导出过程中字节流已经受损,不是简单的编码标记错误能解决的。
更隐蔽的是那种“半乱码”状态——大部分文字能读,但某些符号彻底错位。比如旧格式的矿体类别编号、样品号里的横杠“-”变成了“鈥”,小数点“.”变成乱码或丢失。这类文本你肉眼看不出大问题,但Embedding之后向量已经完全不准了,检索自然一塌糊涂。
1.2 乱码对切分、向量化、召回的三重打击
脏文本对RAG的破坏是链条式的:
- 切分阶段被误导:大多数切分器按段落、标点、长度来断句。乱码字符通常没有合理标点,会造成大段内容被错误合并,或者在一个词中间硬生生断开。比如“矿体厚3.5 m,倾向145°”被切成了“矿体厚3.5”和“m倾向145”,语义全都是碎渣。
- 向量化阶段被打乱:Embedding模型靠token之间的上下文关系来编码。一个正常的句子里混入了“锟斤拷”这种错误token,模型会花一部分注意力去处理这些噪声,导致原本重要的语义特征被稀释。更麻烦的是,同一份报告里同一术语可能因为乱码出现多种变形,比如“ZK301”在不同段落里被打成“ZK30I”“ZK30l”,向量距离被拉远,检索召回直接失败。
- 召回阶段找不准:用户问“ZK301钻孔见矿深度”,你的知识库里存的是“ZK30I钻孔见矿深度”,字面不匹配、语义特征又被稀释,大概率命中的是别的钻孔。这种错误在勘探档案里非常致命。
1.3 认知升级:清洗是“结构化知识”的过程,不只是字符修复
所以我现在做探矿RAG的数据准备,脑子里想的已经不是“把字改对”,而是“把一份报告变成模型能理解的可检索单元”。清洗要从三层同时着手:
- 字符层:编码统一、乱码修复、非法字符剔除。
- 结构层:识别标题、目录、段落、表格、图注,把半结构化的文档内容转成有序文本。
- 语义层:把专业实体、数值单位、坐标关系规范化,保持文档本身的逻辑上下文。
这三层做完了,后面的切分、Embedding、召回才有意义。
2. 按来源各打各的仗:TXT、Word、PDF、网页清洗策略拆解
不同来源的文本,脏的方式完全不同。TXT十有八九是编码问题;Word的问题是版式和内嵌对象;PDF最复杂,可能同时具备文本错位、扫描图像、表格断裂三种病;网页则是一堆HTML标签噪音和重复导航。下面按来源拆开说。
2.1 TXT文件:编码探测比盲目转换重要
探矿业务里的TXT来源通常是老式录井仪的导出数据、GIS属性表的文本输出、早期的地质报告文本备份。它们的共同特点是编码不确定,可能是GB2312、GBK、UTF-8、UTF-16,甚至还有带BOM和不带BOM的区别。
我现在的处理顺序是:
- 先探测,不猜测。用
chardet或charset-normalizer做置信度检测,但要注意,短文本的检测结果有时会误导。正确做法是先读文件的前几十KB做探测,再拿文档中间和末尾的部分交叉验证。 - 统一转UTF-8。探测出原编码之后,用
iconv或Python的codecs做转换,并记录“原始编码→目标编码”的映射关系,方便后续溯源。 - 损毁字节兜底。探测不出编码的文件,直接按二进制流看字节分布,常见模式是高位字节大量堆积同时夹杂ASCII控制符。这种文件我会用容错转换,把无法映射的字节替换成占位符,再用正则清理掉。
- 末尾和头部检查。老式系统导出的TXT经常在文件头带一段设备参数,文件尾带一堆无意义的结束符,这些都要在清洗阶段剥离。
注意:很多老系统的TXT并不是纯文本,而是“类CSV”的定宽文件,只是没有扩展名。清洗时如果能识别出定宽结构,应该先做字段拆分,而不是当成普通段落文本处理。比如钻孔测斜数据,每行是一个测点,深度、倾角、方位角各占固定宽度,这种文件直接切段落进RAG会彻底失去语义。
2.2 Word:吃透docx结构,别停留在文本提取
Word在探矿业务里是报告主力,尤其那些几十页的储量核实报告、矿业权评估说明,经常是Word格式。但直接把docx当纯文本提取,是典型的浪费行为。
docx本质上是一个Zip包,里面装着word/document.xml、word/header.xml、word/footer.xml等一堆XML。用python-docx或直接操作XML,可以拿到远比纯文本丰富的信息:
- 标题层级:Word里的Heading 1、Heading 2对应报告的一级、二级目录。这些结构信息直接决定了后面切块时哪些内容该合在一起、哪些该分开。比如“3 矿区地质”“3.1 地层”“3.2 构造”,这个层级关系是RAG切分的重要锚点。
- 表格结构:探矿报告里满是钻孔一览表、样品分析结果表、资源量表。直接提取文字会把表头和数值混成一行。正确做法是按行读取,把表头拼到每一行前面,生成“字段名=值”的键值对文本,这样模型才能理解“品位=0.35%”是这条记录的一部分。
- 内嵌对象:Word里经常嵌Excel图表、Visio示意图、公式。文本提取拿不到这些,但如果只是把图片转注释文字,至少能提醒RAG“此处有图”。至于公式,能用OMML转文本的最好,转不了就把公式编号保留下来,配合图注说明让模型知道它的存在。
- 批注和修订:老报告带了一堆审阅批注、修订痕迹。这些内容会混进文本提取结果,严重影响语义。清洗时必须显式排除。
我一般会自己写一个docx_extractor模块,先用python-docx遍历document.xml里的段落和表格,把它们按顺序组织成一个“文档对象树”,再做切分前的结构化导出。顺序很重要——Word文档里表格和段落是交错排列的,打乱顺序会导致上下文丢失。
2.3 PDF:文本型、扫描型、图表型要分流
PDF是探矿行业最顽固的障碍,没有之一。储量报告最终版通常只出PDF,而且很多是扫描件,连文本层都没有。
我的分流策略是:
第一步:检测PDF类型
用PyMuPDF读取每一页,看是否包含文本层,也就是能否提取出非空字符串。如果全页无文本,标记为扫描页;如果有文本但文本量和页面面积明显不对,说明可能是“半扫描”状态——背景是图像、上面叠了一层识别文本。
第二步:分类处理
- 文本型PDF:用
pdfplumber或PyMuPDF提取文本。这里有个关键点:很多探矿报告是双栏排版,直接按默认顺序提取会把左右两栏的内容交叉在一起。需要用坐标信息(每个字符块的bbox)来判断栏位,按“从上到下、从左到右”重排。 - 扫描型PDF:走OCR。非中文文档我会用
Tesseract加中文语言包,中文地质文档更推荐PaddleOCR,表格识别能力明显更强。OCR之后不光是文字,最好输出带坐标的识别结果,便于后续把表格结构的横竖线关系还原出来。 - 图表型PDF:勘探剖面图、钻孔柱状图、等值线图,这类页面文本很少但信息量巨大。我的做法是:在清洗阶段识别这是图,提取图标题、图例、坐标轴标注生成一段描述性元数据,放进RAG里让检索至少知道“某页是某矿段的纵剖面图”。
第三步:版面和表格的坐标级处理
PDF表格是公认的重灾区。文本坐标分布看起来很整齐,但提取出来后完全是散的。我会用pdfplumber的extract_table()接口拿表格,但默认接口在合并单元格和跨页表格上经常失灵。替代方案是设定“文字区块规则”:纵向对齐的行聚合成一列,横向连续的文字聚合在一个单元格,再按“表头: 内容”的方式拼接成一行纯文本。
实战提醒:跨页表格是个大坑。一个样品分析表可能从第5页延续到第7页,单独切分会导致每段文本都是“表头+断掉的半截数据”。处理时我宁可把跨页表格整个合并成一个块,也不要让它被切分器拦腰截断。检索时命中率才是第一位的。
2.4 网页:正文识别与HTML噪音剥离
探矿业务里的网页来源,一般是下属单位官网发布的地质调查报告、矿业权公示信息、行业规范文件。抓下来的HTML如果直接去标签,你会得到导航栏、侧边栏、页脚版权声明、广告、相关推荐……一堆和正文无关的垃圾。
我常用的工具是trafilatura,它和readability-lxml、BeautifulSoup的差别在于:
trafilatura专门为学术正文提取设计,能自动识别标题、段落、列表、表格,并且过滤页眉页脚。readability-lxml更适合新闻类页面,对表格支持一般。- 纯BeautifulSoup需要自己手写大量规则,适合页面结构极其固定的场景。
还有一个细节很多人忽略:网页里的“目录跳转锚点”和“页内链接文字”经常和正文混在一起。提取正文后必须把<a>标签里的超链接去掉,但保留链接文字。如果链接文字本身就是术语,比如“详见《固体矿产地质勘查规范总则》”,这个引用是语义的一部分,不能丢。
网页正文提取完之后,我还会做一步“去重”。同一篇行业规范可能被多个网站转载,抓回来之后内容完全相同,只是标题前面加了不同网站名称。清洗阶段要用MinHash或简单的文本相似度计算做去重,避免知识库里同一份规范被嵌入5遍,检索时返回一堆冗余结果。
3. 探矿领域专属清洗细节:让拆出来的文本真正可检索
这可能是这篇文章里“含金量”最高的一节。通用清洗只能解决“乱码”问题,但探矿文本还有一些独有的特征,不好好处理,检索精度上不去。
3.1 地层符号、钻孔编号与坐标刻度的规范化
探矿文本里写满各种神秘符号:地层单位用的“D2”“T3”“C2h”,钻孔编号“ZK1206-1”“CK301”,坐标点用的高斯-克吕格投影带号“42N 560832 4512392”……这些不是普通英文单词,而是有严格格式的专业标识符。
清洗阶段要做几件事:
- 符号统一大小写和上下标。Word和PDF里的地层符号经常带上下标,提取出来后变成“D2”和“D2”混用,甚至数字变成特殊字符。要在清洗阶段统一成“D_2”或“D2”的固定写法,否则同一个地层的向量表示会分裂成好几种。
- 钻孔编号的连字符规范。ZK1206-1和ZK1206_1和ZK1206―1在文档里可能指同一个孔,但它们会被Embedding成完全不同的token序列。清洗时必须用一个归一化函数处理掉连字符变体。
- 坐标格式的结构化。有些报告里把坐标写成“X:4512.392; Y:560.832”,有些写成“(4512392, 560832)”,还有一些是带度分秒的经纬度。如果希望用户能按坐标检索,就要把这些格式统一成标准度或标准米制表达,同时保留原始文本作为别名。
这三项看着不起眼,但直接决定了“用户搜ZK301能不能搜到ZK301-1的相关描述”这种命中问题。
3.2 数值单位与品位表达的处理
化验报告里到处都是“Cu 0.35%”“Au 2.15g/t”“品位3.5×10⁻⁶”这类数值。清洗阶段对数值的处理有三个层次:
- 基础层:统一数值格式。全角数字变半角、千分位逗号统一去掉、百分比表达式固定成“0.35%”。
- 语义层:把数值和单位当作一个整体处理,不要拆分。因为“2.15g/t”是一个完整语义单元,检索时用户说“两克每吨的金品位”,Embedding模型需要把这个文本和“2.15g/t”关联起来。
- 边界层:用正则把“品位”“厚度”“深度”这类量词和后面的数值绑定。比如“矿体平均厚度4.8m,倾向145°,倾角65°”,清洗后要确保“4.8m”“145°”“65°”没有被切分器拆开。
提示:这里有一个资料上不常写的经验——探矿数据里经常出现“<0.1g/t”这种小于符号、以及“—”“~”表示区间的写法。区间表达的检索价值极高,用户问“品位在2到3克每吨之间的钻孔”,模型需要在文本里数出“2~3g/t”这种模式。清洗时我会把区间表达统一成“2~3 g/t”的固定模板,并且把数值范围单独抽出来做索引,辅助后续的数值过滤型检索。
3.3 表格、图注与“半结构化”内容的扁平化
探矿报告里有大量半结构化内容:钻孔柱状图、样品分析表、资源量估算表。这些内容在原始文档里是表格,提取出来后却是碎片。
我的做法是三步:
- 表格转键值对文本。把“样品号 | Cu(%) | Pb(%) | Zn(%) | 深度(m)”转成“样品号…的铜品位为…,铅品位为…,锌品位为…,深度为…米”。这比逗号分隔更利于模型理解列语义。
- 图注生成上下文。剖面图、平面图、柱状图在文档里通常只有一个图题,比如“图4-3 矿区地质剖面图”。清洗时我会结合页面前后文本生成一段补充描述,告知模型“该图显示地层展布和矿体位置,详情见PDF第47页”。虽然RAG不能直接看图,但至少把这个引用关系留存在知识库里。
- 深度的层级关系保留。钻孔编录是按深度逐行记录的,清洗时必须保留“深度间隔”和“岩性描述”的对齐关系。我最常犯的错误是切分器把“0-2m:第四系覆盖”和“2-8m:灰岩”切开成两个块,导致用户问“浅表层是什么”时抓不到完整上下文。解决方法是按“钻孔单位”把连续深度区间合并成一个文本块,或者用深度段作为元数据附加到每一段上。
4. 清洗前后对比:高精度检索不是玄学,是可量化的
前面说了这么多理论,这部分我把一次实际前后的检索对比放出来。前阵子帮某矿区整理一批旧地质档案,正好用同一个知识库、同一组问题,分别跑清洗前和清洗后的检索,结果挺能说明问题。
4.1 清洗前的“高召回、低精度”假象
先看清洗前的情况。当时的语料库直接喂进去的是:
- 约120个TXT格式的钻孔编录,存在部分乱码;
- 约40份Word版的年度勘查工作总结,带一堆修订批注和表格;
- 约30份PDF扫描版的化探报告,几乎没有文本层;
- 网页抓取的地质规范文件,带了大量导航菜单和页脚。
用默认的RecursiveCharacterTextSplitter按512字符切分,Embedding用常见的bge-large-zh,检索TopK设为5。然后我提了一个很业务化的问题:“ZK1206钻孔在180米到200米之间的岩芯描述和见矿情况”。
结果是:Top5返回的结果里,有两个来自“ZK1206”但内容是其他深度段的描述,两个来自其他钻孔的相似岩性描述,还有一条是网页抓来的规范正文,整段和问题没关系。表面上看“搜到了”,实际上没有一个块真正回答到点子上。这就是典型的召回率高但精度低——向量空间里“ZK1206”“岩芯描述”这些特征被乱码和噪声冲淡了,模型只能模糊匹配到一些轮廓相近的块。
4.2 清洗后,检索结果发生了三个明显变化
清洗完之后(具体管道下文会讲),同一个问题再跑一次,结果:
- 命中位置更精准。返回的第一个块就是“ZK1206钻孔编录,深度165-210m段,岩性为黄铁矿化花岗闪长岩,见矿厚度4.2m,Cu品位0.31%”。这个块不仅在深度的区间内,而且把岩性、矿化、品位全部包在一个上下文里。
- 跨源干扰基本消失。因为网页正文做了去重和标签过滤,那些“规范正文”不会再出现在检索结果的前排。
- 表格数据能用了。用户问“各钻孔金的平均品位”,清洗前的表格碎片根本组不成一条有效记录,清洗后“孔号—金平均品位—取样数量”键值对文本直接被召回。
4.3 一个小范围可复现的评估方案
如果你也想量化清洗的效果,不用整得太复杂。我自己的做法是:
- 挑30~50个业务问题,覆盖岩性、构造、矿体、品位、坐标、储量这几类。
- 对每个问题标注标准答案所在的文档和段落位置。
- 分别跑清洗前和清洗后两条管道,记录每个问题在Top5、Top10里是否命中正确答案。
- 对比Hit@5和Hit@10,再用
nDCG@10给排序质量打个分。
这么一轮跑下来,清洗的价值立刻现形。我这儿一个典型的数字是:Hit@5从0.36提升到0.78,Hit@10从0.52提升到0.91。别看这数字小,放到几十万字的探矿报告库里,意味着现场工程师不用再翻半个小时的PDF找数据,RAG能直接把那段岩芯描述调出来。
5. 工程落地的取舍与自动化流水线设计
最后一part讲落地。清洗不能靠手工,必须做成一个能自动跑的管道。管线的好处不只是省人力,更是保证“每一次新增文档都经过同样的清洗标准”,不至于不同批次质量飘来飘去。
5.1 按文件类型路由:谁是主力、谁是兜底
我的管道流程大致是:
- 文件进入队列,先做类型识别。后缀名只作参考,真正的判断靠读取文件头(magic bytes)和内部结构。
- TXT走编码探测→归一化;Word走docx结构解析→去批注→表格转键值对;PDF走类型检测→文本型直接提取/扫描型OCR/图表型生成描述;网页走正文提取→去重→转化文本。
- 所有输出统一为一个中间JSONL格式,每行记录包含:原始路径、来源类型、章节层级、清洗后文本、提取的元数据(坐标、深度、品位值等)。
- 最后把它交给切分器和Embedding管道。
这样设计的好处是每类文件可以用它最擅长的清洗器,而不是一把万能钥匙硬开。比如PDF扫描件,用OCR跑一遍要十几分钟,但这是必要的;如果是纯文本型PDF,就没必要浪费算力走OCR。
5.2 缓存、增量与质量监控
清洗阶段的缓存特别值得做。同一个大PDF可能因为有几十页扫描件,跑一次OCR非常慢。产品迭代期间,文档没变就不应该重跑清洗。我用的是一个简单的“文件hash—产物”缓存:
- 每次进入管道的文件先算SHA256;
- 如果hash和缓存的记录匹配,直接复用清洗结果;
- 不匹配才重新清洗,完成后写回缓存。
增量更新也依赖这个机制——老矿区每周可能来一批新编录的TXT和Word,管道只需要处理新增的部分,不用全量重跑。
质量监控我提一个必须盯的指标:清洗后的有效文本率。也就是“清洗后非空字符数”除以“清洗前原始内容字符数”。如果这个比例突然异常低,说明可能误伤了正文,或者OCR效果崩了。我通常设两个阈值:一个报警阈值,一个熔断阈值。报警触发时人工抽查,熔断触发时自动暂停该来源类型的清洗任务。
5.3 现场部署时的三个提醒
最后说几个探矿现场特有的坑,都是踩过才长出来的经验:
提醒一:时间跨度大的文档,别用同一套规则硬套。1980年代的报告行文风格、地质术语跟今天的完全不同。比如老报告里爱用“奥陶系”“灰岩”这种说法,新报告里可能细化到“大湾组灰岩”。清洗规则最好按“年代分段”配置,老文档用更保守的清洗参数,别让通用规则把老术语给误改掉。
提醒二:文件名隐藏的信息量比你想的大。很多探矿文档的文件名本身就是结构化数据,比如“ZK1206-钻孔柱状图-2016.pdf”。清洗管道应该把文件名解析成一个额外的元数据字段,拼进清洗后的文本里。这样用户搜“ZK1206”时,哪怕正文内容被切得七零八落,文件名这个来源标签也能帮助召回。
提醒三:保留原始文件永远比保留清洗结果值钱。清洗过程中总会丢掉一些信息,尤其是表格的结构、图件的坐标位置、扫描件的格式细节。我现在的习惯是清洗完永远保留一份“原始文件归档目录”,只把清洗产物同步到知识库。这样一旦发现清洗有问题,可以随时回溯重洗,而不是对着已经向量化的Embedding干瞪眼。
这套清洗管道做完之后,给我的感受是:RAG在探矿这种垂直领域能不能用,根本不取决于模型选多大、向量库多高级,而是你有没有老老实实把几万页灰头土脸的档案整理成模型能读懂的样子。清洗做得越扎实,后面的切分和召回越省心。以前我总觉得清洗是预处理里的“体力活”,现在则认为它本身就是RAG流程里决定成败的核心工序。