1. 探矿业务文档处理的真实困境
1.1 为什么探矿场景下的RAG落地这么难
探矿行业的信息化程度,说实话,比大多数人想象的要低。地质报告、钻孔编录、化验分析单、物化探数据表、历史勘探总结——这些东西散落在各个年代的文件夹里,格式横跨手写扫描件、90年代的Word 97文档、PDF、Excel表格,还有大量从内部网页系统导出的HTML页面。我接触过的一个典型项目,光是整理一个中型矿区的历史资料,就涉及超过12000个文件,总体量接近40GB。
这些文件如果只是存档,那倒没什么问题。但一旦要做RAG知识库,问题就全暴露出来了。RAG的核心逻辑是“检索增强生成”,检索的质量直接决定了生成的质量。而检索的前提,是文档被正确地解析、清洗、分块、向量化。探矿业务的文档,恰恰在“解析”这一步就卡死了。
我见过太多团队,拿着LangChain的默认文档加载器直接往上怼,结果就是:PDF里的表格变成了一坨乱码,Word里的公式全丢了,TXT文件的编码五花八门,网页导出的内容夹杂着大量导航栏和广告。检索出来的东西驴唇不对马嘴,最后得出结论说“RAG不适合探矿业务”。这不是RAG的问题,是清洗没做到位。
1.2 四类文档各自的“坑”在哪里
先给不太熟悉的朋友铺垫一下。探矿业务中常见的文档类型,大致可以分成四类,每一类的清洗难点完全不同:
TXT文件:看起来最简单,实际上最阴险。编码问题首当其冲——GBK、GB2312、UTF-8、UTF-8 with BOM,甚至还有早期Unix系统留下的Latin-1编码。一个文件用UTF-8读出来是乱码,换成GBK就正常了,但另一个文件可能反过来。更麻烦的是,有些TXT是从老式数据库导出的,字段之间用奇怪的符号分隔,比如“|”或者“^”,还有的用固定宽度对齐,解析时稍不注意就把数据切错了。
Word文档:.doc和.docx是两个完全不同的世界。.doc是二进制格式,Python生态里能处理它的库屈指可数,而且效果参差不齐。.docx虽然本质是XML,但表格嵌套、公式对象、批注、修订记录这些东西,处理起来非常棘手。探矿报告里经常有地质柱状图、品位等值线图,这些图片如果直接丢弃,检索时就丢失了关键信息。
PDF文件:这是最让人头疼的。扫描版PDF需要OCR,文字版PDF又分单栏、双栏、多栏排版。地质报告里的表格往往跨页,表头在上一页,数据在下一页,解析时如果不知道它们是同一张表,就会把表头和数据割裂。还有公式,PDF里的公式要么是图片,要么是特殊字体编码,直接提取出来就是乱码。
网页文件:从内部系统导出的HTML,或者用网页抓取工具采集的数据,最大的问题是“噪音”。导航栏、页脚、侧边栏、广告、JavaScript生成的动态内容,这些如果不剔除,会严重污染检索结果。而且网页的DOM结构千变万化,没有一个通用的规则能适配所有页面。
1.3 清洗的目标:从“能读”到“能检索”
很多人对清洗的理解停留在“能把文字提取出来就行”。但在RAG场景下,这个标准太低了。清洗的目标应该是:让每一段文本都具备独立的语义完整性,并且携带足够的元数据,以便检索时能精准定位。
举个例子。一份钻孔编录报告里有一句话:“ZK001孔在125.3米处见矿,品位1.2g/t。”如果清洗后变成“ZK001孔在125.3米处见矿,品位1.2g/t”孤零零的一段,检索时用户问“哪些孔见矿品位超过1g/t”,这段文本可能被检索到,但用户无法知道它来自哪个报告、哪个矿区、什么时间。但如果清洗时保留了元数据——矿区名称、报告年份、钻孔编号——检索的精准度会大幅提升。
所以,清洗不只是“去乱码”,更是“结构化”和“元数据化”。这是探矿业务RAG清洗的核心思路,也是后面所有实操的出发点。
2. 整体清洗管道的设计与选型逻辑
2.1 为什么不用“一把梭”的方案
市面上有不少“一站式”文档解析工具,比如某些商业API,上传文件就能返回文本。我试过几个,在通用场景下表现还行,但放到探矿业务里就露怯了。原因很简单:探矿文档的领域特异性太强。地质术语、钻孔编号规则、品位单位、地层代号,这些通用模型根本没见过,解析出来的结果经常把“Ar”识别成“Argon”(氩气),把“Pt”识别成“铂”而不是“板岩”。
所以我的方案是:分类型处理,每一类文档用专门的工具链,最后统一输出成结构化的JSON格式。这样做的好处是,每个环节都可以单独调试和优化,出了问题容易定位。坏处是管道比较长,需要维护的代码多一些。但对于探矿这种对精度要求高的场景,这个代价是值得的。
整个管道的设计思路是这样的:
原始文件 → 类型识别 → 分类解析 → 文本清洗 → 结构化输出 → 分块 → 向量化其中“分类解析”是核心,TXT、Word、PDF、网页各走各的通道。“文本清洗”是公共环节,负责去噪、规范化。“结构化输出”是把解析结果统一成带元数据的JSON,方便后续处理。
2.2 工具选型的考量与对比
工具选型这块,我踩过不少坑,这里把经验分享一下。
TXT处理:Python内置的chardet库用来检测编码,准确率大概在85%左右,剩下的15%需要人工兜底。对于固定宽度的TXT,pandas.read_fwf比手动切片靠谱得多。如果是分隔符分隔的,csv模块配合自定义dialect就能搞定。
Word处理:.docx用python-docx,这是目前最成熟的方案。.doc的话,antiword和textract都试过,antiword对中文支持更好,但需要单独安装二进制文件。如果服务器环境不允许装额外软件,可以考虑用LibreOffice的无头模式转换,虽然慢一点,但兼容性最好。
PDF处理:这是工具选型的重灾区。PyPDF2和pdfplumber适合文字版PDF,pdfplumber对表格的支持更好。扫描版PDF必须上OCR,PaddleOCR中文识别效果不错,Tesseract也可以但需要调参。对于公式,Mathpix的API效果最好,但收费;开源方案可以试试pix2tex,准确率稍低但够用。
网页处理:BeautifulSoup是基础,但光靠它不够。trafilatura专门用于正文提取,能自动剔除导航和广告,效果比手动写规则好得多。如果网页是JavaScript渲染的,那就得上Playwright或Selenium,但速度会慢很多。
下面这张表是我实际项目中总结的选型对照:
| 文档类型 | 推荐工具 | 备选方案 | 关键考量 |
|---|---|---|---|
| TXT | chardet + pandas | iconv + 手动解析 | 编码检测准确率 |
| Word (.docx) | python-docx | docx2python | 表格和公式处理 |
| Word (.doc) | antiword | LibreOffice无头转换 | 中文兼容性 |
| PDF (文字版) | pdfplumber | PyMuPDF | 表格提取能力 |
| PDF (扫描版) | PaddleOCR | Tesseract | 中文识别准确率 |
| 网页 | trafilatura | BeautifulSoup + 规则 | 正文提取精度 |
2.3 元数据设计:被大多数人忽略的关键
元数据设计是清洗管道里最容易被忽视的环节,但它对检索质量的影响极大。我的做法是,在解析阶段就为每个文本块打上尽可能丰富的标签。
探矿业务的元数据至少应该包括:矿区名称、报告类型、年份、钻孔编号、地层代号、坐标范围、数据来源。这些信息有些能从文件名提取,有些能从文档内容里正则匹配,有些需要人工标注。我的策略是:能自动提取的自动提取,提取不了的留空,后续用规则或人工补全。
为什么要这么细?因为探矿业务的检索需求往往带有强烈的过滤条件。比如“2020年以后XX矿区的见矿记录”,如果元数据里没有年份和矿区,检索就只能靠语义相似度,准确率会大打折扣。有了元数据,就可以先做结构化过滤,再做向量检索,两者结合,精度提升非常明显。
提示:元数据字段不要贪多,先确定3-5个最核心的,跑通流程后再逐步扩展。一开始就设计几十个字段,最后大概率大部分都是空的。
3. 四类文档的清洗实操细节
3.1 TXT文件:编码检测与结构化解析
TXT文件的处理,第一步永远是编码检测。我写了一个小函数,用chardet检测编码,如果置信度低于0.8,就尝试用常见编码逐个解码,看哪个能解出正常的中文字符。
import chardet def detect_encoding(file_path): with open(file_path, 'rb') as f: raw = f.read() result = chardet.detect(raw) if result['confidence'] > 0.8: return result['encoding'] # 置信度低时,尝试常见编码 for enc in ['utf-8', 'gbk', 'gb2312', 'latin-1']: try: raw.decode(enc) return enc except UnicodeDecodeError: continue return 'utf-8' # 兜底编码搞定之后,就是结构化解析。探矿业务的TXT大致分两种:一种是自由文本,比如地质描述;另一种是结构化数据,比如化验结果表。自由文本直接按段落切分就行,结构化数据需要根据分隔符或固定宽度来解析。
我遇到过一个典型的坑:某矿区的化验数据TXT,字段之间用“|”分隔,但有些行的末尾多了一个“|”,导致解析时多出一个空字段。这种问题没有通用解法,只能针对具体数据写清洗规则。我的经验是,先抽样100行看看数据长什么样,再写解析逻辑,不要上来就写通用代码。
3.2 Word文档:表格、公式与批注的处理
Word文档的处理,python-docx是主力。但有几个细节需要注意。
表格处理:python-docx读取表格时,默认是按行读取,每个单元格是一个Paragraph对象。如果单元格里有多个段落,需要遍历。表格的元数据(比如表头)要单独提取,作为该表格所有数据的上下文。
from docx import Document def extract_tables(doc_path): doc = Document(doc_path) tables_data = [] for table in doc.tables: headers = [cell.text.strip() for cell in table.rows[0].cells] for row in table.rows[1:]: row_data = {} for i, cell in enumerate(row.cells): row_data[headers[i]] = cell.text.strip() tables_data.append(row_data) return tables_data公式处理:Word里的公式是OMML格式,python-docx不支持直接提取。我的做法是用docx2python,它能把公式转成LaTeX。如果公式不多,也可以手动处理,把公式图片单独OCR。
批注和修订:探矿报告经常有批注和修订记录,这些信息有时候很重要(比如“此数据待核实”),有时候是噪音。我的策略是:批注单独提取,作为元数据附加到对应段落;修订记录只保留最终版本,丢弃修订痕迹。
3.3 PDF文档:文字版与扫描版的差异化处理
PDF处理的第一步是判断它是文字版还是扫描版。方法很简单:用pdfplumber打开,随便取一页,看extract_text()返回的字符数。如果字符数很少(比如少于50),基本可以判定是扫描版。
文字版PDF用pdfplumber提取文本和表格。pdfplumber的表格提取能力比PyPDF2强很多,它能识别表格的边界线,把表格还原成二维数组。但跨页表格需要特殊处理:如果上一页的最后一行和下一页的第一行结构相似,就合并成一张表。
扫描版PDF必须上OCR。PaddleOCR的安装稍微麻烦一点,但中文识别效果确实好。我的流程是:先用pdf2image把PDF转成图片,再用PaddleOCR逐页识别。识别结果里,表格区域需要单独处理,因为OCR对表格的识别是按行进行的,会丢失表格结构。我的做法是先用PaddleOCR的表格识别模型提取表格,再和文本识别结果合并。
注意:OCR的准确率再高,也会有错误。探矿数据里的数字特别关键,一个小数点错了,品位就差十倍。所以OCR结果一定要有校验环节,比如用正则表达式检查数字格式,异常值人工复核。
3.4 网页文件:正文提取与噪音剔除
网页文件的清洗,核心是“去噪”。从内部系统导出的HTML,往往包含大量模板代码。我的做法是分三步:
第一步,用trafilatura提取正文。trafilatura会自动识别文章主体,剔除导航、页脚、侧边栏。它的准确率在通用网页上能达到90%以上。
第二步,对提取结果做二次清洗。trafilatura有时候会把一些无关的段落也带进来,比如“相关阅读”“上一篇/下一篇”。这些可以用关键词黑名单过滤。
第三步,提取元数据。网页的<title>、<meta>标签、URL路径,都包含有用的信息。比如URL里如果有“/report/2023/”,就能提取出年份和文档类型。
import trafilatura def extract_web_content(html_path): with open(html_path, 'r', encoding='utf-8') as f: html = f.read() text = trafilatura.extract(html, include_tables=True, include_links=False) metadata = trafilatura.extract_metadata(html) return text, metadata如果网页是JavaScript渲染的,trafilatura就无能为力了。这时候需要用Playwright先渲染页面,拿到完整的HTML后再用trafilatura处理。但Playwright的速度慢,建议只对必要的页面使用。
4. 清洗后的分块与向量化策略
4.1 分块策略:固定长度还是语义分块
分块是RAG里另一个容易被轻视的环节。很多人直接用RecursiveCharacterTextSplitter,设个chunk_size=500就完事了。但在探矿业务里,这样做会出问题。
探矿文档的语义单元往往不是固定长度的。一段地质描述可能只有两句话,但一个化验表格可能有几十行。如果强行按500字切分,表格会被切得七零八落,检索时只能检索到半张表,信息不完整。
我的策略是混合分块:对于自由文本,按段落分块,段落太长再按句子切分;对于表格,整张表作为一个块,如果表格太大,按行分组,但每组都带上表头;对于列表,按列表项分块,但保留列表的上下文标题。
具体参数上,我一般设chunk_size=800,chunk_overlap=100。800字大约是一页A4纸正文的量,语义相对完整。重叠100字是为了避免关键信息刚好被切在边界上。
4.2 向量化模型的选择与微调
向量化模型的选择,直接决定了检索的语义匹配能力。通用场景下,text-embedding-ada-002或者开源的bge-large-zh都够用。但探矿业务有大量专业术语,通用模型对这些术语的语义理解不够精准。
我的做法是:先用通用模型跑一版,看看检索效果。如果专业术语的检索准确率低,就用领域数据微调一个模型。微调的数据不需要太多,几百条“查询-文档”对就够了。微调后的模型,对“品位”“见矿”“蚀变”这些术语的语义区分度会明显提升。
如果不想微调,还有一个折中方案:在向量化之前,先用领域词典做一次术语规范化。比如把“g/t”统一成“克/吨”,把“ZK”统一成“钻孔”。这样通用模型也能更好地理解。
4.3 元数据过滤与向量检索的结合
前面强调了元数据的重要性,这里说说怎么用。检索时,我的流程是:
- 用户输入查询,先用规则或小模型提取过滤条件(矿区、年份、文档类型)。
- 用过滤条件在元数据库里筛选候选文档。
- 对候选文档的向量做相似度检索。
- 返回Top-K结果,附带元数据。
这样做的好处是,检索范围大幅缩小,准确率提升。比如用户问“2020年XX矿区的见矿记录”,如果没有元数据过滤,向量检索可能会返回其他矿区的相似记录;有了过滤,就只会在2020年XX矿区的文档里检索。
提示:元数据过滤的字段不要太多,否则候选集太小,可能漏掉相关结果。一般2-3个过滤条件就够了。
5. 常见问题与排查技巧实录
5.1 乱码问题的系统排查方法
乱码是清洗过程中最常见的问题,但乱码的原因有很多种,需要系统排查。
第一步,确认乱码类型。如果显示的是“锟斤拷”,那是UTF-8被误读为GBK;如果显示的是“佔,那是UTF-8被误读为Latin-1;如果显示的是“????”,那是编码转换时丢失了字符。
第二步,定位乱码环节。是文件本身编码不对,还是读取时编码设错了,还是输出时编码设错了?我的做法是,在管道的每个环节都打印前100个字符,看乱码从哪一步开始出现。
第三步,针对性修复。如果是文件本身编码问题,用chardet检测后重新读取;如果是读取时设错了,改encoding参数;如果是输出时的问题,检查输出文件的编码设置。
下面这张表是我总结的乱码速查表:
| 乱码表现 | 可能原因 | 解决方法 |
|---|---|---|
| 锟斤拷 | UTF-8误读为GBK | 用UTF-8重新读取 |
| ä½ | UTF-8误读为Latin-1 | 用UTF-8重新读取 |
| ???? | 编码转换丢失字符 | 找到原始编码,避免转换 |
| 方框或问号 | 字体不支持 | 更换字体或编码 |
| 部分乱码 | 混合编码 | 分段检测编码 |
5.2 表格解析错位的修复思路
表格解析错位是PDF和Word处理中的高频问题。表现是:表格的列对不齐,或者表头和数据错行。
PDF表格错位,通常是因为PDF里的表格没有明确的边界线,pdfplumber只能靠文字位置推断列边界。如果文字间距不均匀,推断就会出错。解决方法是调整pdfplumber的table_settings参数,比如snap_tolerance和join_tolerance,让边界推断更宽松或更严格。
Word表格错位,通常是因为合并单元格。python-docx读取合并单元格时,会把合并后的单元格内容重复填充到每个子单元格。解决方法是检测单元格的_tc属性,判断是否是合并单元格,然后去重。
5.3 OCR识别准确率提升的实战技巧
OCR准确率是扫描版PDF处理的核心指标。我试过很多方法,总结下来有几个技巧最有效:
图像预处理:在OCR之前,先对图像做二值化、去噪、纠偏。OpenCV的adaptiveThreshold和fastNlMeansDenoising效果不错。如果扫描件有倾斜,用HoughLines检测直线后旋转校正。
分区域识别:把页面分成文本区、表格区、图片区,分别用不同的OCR模型处理。文本区用通用模型,表格区用表格识别模型,图片区单独保存。
后处理校验:OCR结果里的数字,用正则表达式校验格式。比如品位数据通常是“数字+单位”,如果识别出“1.2g/t”但单位缺失,就标记为可疑,人工复核。
词典辅助:把探矿领域的专业术语做成词典,OCR时用词典做后处理,把识别错的术语纠正过来。比如“板岩”被识别成“板岩”,词典里没有“板岩”,就自动纠正为“板岩”。
5.4 清洗管道的性能优化
清洗管道跑起来之后,性能往往是个瓶颈。12000个文件,如果每个文件处理要10秒,总共就是33小时。我的优化经验是:
并行处理:用multiprocessing或concurrent.futures做多进程并行。注意,OCR是CPU密集型,并行度不要超过CPU核心数;IO密集型任务可以适当提高并行度。
缓存中间结果:解析结果、OCR结果都缓存到磁盘,避免重复处理。用文件哈希作为缓存键,文件没变就直接读缓存。
增量处理:只处理新增或修改的文件。用文件的修改时间和大小做判断,没变的文件跳过。
资源限制:OCR很吃内存,如果并行度太高,容易OOM。我的做法是给每个进程设内存上限,超了就排队等待。
6. 从清洗到检索的完整链路验证
6.1 构建小规模测试集验证清洗效果
清洗管道写完之后,不要急着上全量数据。先构建一个小规模测试集,比如100个文件,覆盖四种类型,人工检查清洗结果。
测试集的构建要讲究代表性:TXT要包含不同编码的,Word要包含表格和公式的,PDF要包含文字版和扫描版的,网页要包含不同模板的。每个类型至少20个文件。
验证的指标包括:文本提取完整率(提取出的文本占原文的比例)、表格还原准确率(表格结构是否正确)、元数据提取准确率(关键字段是否提取正确)。我的经验是,完整率要达到95%以上,表格准确率要达到90%以上,元数据准确率要达到85%以上,才算合格。
6.2 检索效果的人工评估方法
清洗效果最终要体现在检索上。我的评估方法是:准备50个典型查询,人工标注每个查询应该返回哪些文档,然后看实际检索结果和标注的匹配度。
查询的设计要覆盖不同场景:简单查询(“XX矿区的见矿记录”)、复杂查询(“2020年以后品位超过1g/t的钻孔”)、模糊查询(“关于蚀变带的地质描述”)。每个场景至少10个查询。
评估指标用召回率和准确率。召回率是检索到的相关文档占所有相关文档的比例,准确率是检索到的文档中相关文档的比例。我的目标是召回率90%以上,准确率80%以上。
6.3 持续迭代:从反馈中优化清洗规则
清洗管道不是一次性的工作,需要持续迭代。我的做法是:在检索界面加一个“反馈”按钮,用户觉得检索结果不对,就点一下,记录下查询和实际期望的结果。每周汇总一次反馈,分析是清洗问题还是检索问题,然后针对性优化。
常见的优化方向包括:补充元数据字段、调整分块参数、更新术语词典、微调向量模型。每次优化后,重新跑一遍测试集,看指标是否提升。
提示:迭代要有节奏,不要频繁改动。我的节奏是两周一次小优化,一个月一次大优化。频繁改动会导致无法判断哪个改动有效。
7. 一些踩坑后的个人体会
探矿业务的RAG清洗,说到底是一个“脏活累活”。没有哪个工具能一键搞定,必须针对每一类文档、每一种数据格式写专门的规则。我最大的体会是:不要追求通用,要追求可用。一个只适用于本矿区文档的清洗规则,比一个号称通用的但准确率只有70%的方案,价值大得多。
另外,元数据的重要性怎么强调都不为过。我见过太多团队,花大力气优化向量模型,却忽略了元数据,结果检索效果始终上不去。实际上,在探矿这种强结构化场景下,元数据过滤带来的精度提升,往往比换一个更强的向量模型更明显。
最后分享一个小技巧:清洗管道里加一个“人工复核队列”。对于置信度低的解析结果(比如OCR置信度低于0.9的、编码检测置信度低于0.8的),自动进入复核队列,人工确认后再入库。这样既保证了效率,又保证了质量。这个队列一开始可能很长,但随着规则优化,会越来越短。