1. 探矿数据进RAG之前,为什么必须先过清洗这一关
1.1 探矿领域的文档生态:比想象中更杂
做探矿业务的知识库和做通用知识库完全是两码事。通用场景里的文档再乱,基本也就是网页、PDF、Word这几类,格式相对规整。探矿业务呢?我见过最典型的场景是:一个钻孔编录报告集里,既有上世纪八十年代的扫描件PDF,也有地质队员用老式文本编辑器保存的TXT台账,还有从OA系统导出的带修订痕迹的Word,以及从各省地质资料网站抓下来的公示文件。这四类东西要同时灌进一个RAG知识库里,如果不先把清洗这一步做扎实,后面检索出来的结果基本没法看。
先说TXT。探矿行业很多老一辈地质工作者留下的原始记录,存的就是纯文本。这些TXT文件的问题不是“缺格式”,而是编码混乱。同一批文件中,GB2312、GBK、UTF-8、甚至个别BIG5编码混在一起,你用默认的UTF-8去读GBK文件,满屏乱码,向量化之后整个块就是一堆无意义字符。Word文档呢,问题在“看不见的格式污染”。修订痕迹、批注、域代码、目录域这些内容,你在屏幕上看见的是正常文字,但是一旦用脚本抽取,批注和隐藏域内容会混进来。PDF最麻烦,扫描件要OCR,文字版要处理双栏和表格,稍微处理不当,一整段的地层描述就被切得七零八落。网页倒是相对干净,但导航栏、页脚、cookie弹窗这些噪声会稀释正文的比重,导致分块后语义模糊。
这些文档还有一个共同特征:专有名词密度极高。岩性名称(闪锌矿、黄铁矿、矽卡岩),地层代号(D2、C1、Pt1),化验指标单位(g/t、ppm、μg/g),这些词汇一旦被错误切分或识别,检索效果立刻崩盘。RAG系统检索的是一段段的语义单元,如果语义单元里掺杂着乱码、残缺表格、OCR错字,用户问“某矿区铜矿体平均品位”,系统可能把这段信息错误匹配到“某矿区钻孔柱状图”的无关片段上。
所以在探矿业务里,清洗不是可选项,而是决定RAG能不能落地的关键环节。我自己的经验是:清洗做到位,检索命中率能提升一个量级;清洗不到位,后面所有向量化、检索、重排的努力都会被噪声淹没。这篇文章就围绕探矿业务中最常见的四类数据——TXT、Word、PDF、网页——把每一类数据的清洗思路和实操方案讲透,从乱码处理到结构化恢复,再到检索精度验证,给你一条可以直接照做的路径。
1.2 脏数据进了向量库,检索精度会一路崩到谷底
有人可能觉得,向量化模型不是对噪声有鲁棒性吗?句子里面有几个乱码字符、几个多余符号,向量应该还是能抓住语义吧。这个想法在通用语料上也许成立,但在探矿数据上行不通。原因很简单:探矿文本里的关键信息往往集中在非常短的片段里,而且这些片段之间的区分度不高。
举个例子,一份钻孔岩心编录表里,每行记录就是一个深度区间和对应的岩性描述。比如“120.5-121.3m 灰白色中粒花岗闪长岩,含少量黄铁矿”。如果把表格转成了文本、格式又乱了,变成“120.5-121.3m 灰白色中粒花岗闪长岩,含少量黄铁”或者“120.5-121.3m 灰白色中粒花岗闪长岩,含少量黄铁矿(续)”,那这块内容的向量就和其他钻孔的内容高度相似,用户真正想查的“黄铁矿富集层位”就淹没在一堆相似向量里了。再比如储量计算报告里的化学分析结果表,如果表格被拆散、单位被截断,问答系统给出的数值可能就是错的。探矿业务里一个数值错误,可能影响整个矿体圈定结果的判断。
还有一类问题是数据版本。探矿项目的文档有严格的版本链条:初勘报告、详勘报告、补充勘探报告、储量核实报告,每一份都是后续报告的基础。如果把不同版本的表格数据混在一起灌进知识库,RAG在回答“该矿体平均品位是多少”的时候,可能同时检索到初勘和详勘两个版本的数据,系统不知道选哪个,只能瞎拼。清洗阶段就必须对文档做版本标注,或者明确处理优先级,否则精准检索永远只是空谈。
这一章先把问题背景讲清楚,接下来每一章进入具体的数据格式清洗路径。先说最古老也最容易翻车的TXT。
2. TXT类数据:乱码不是编码问题那么简单,还有语义碎片
2.1 编码识别与转换:先把乱码一次性解决
TXT在探矿业务里的典型来源有几类:早期地质软件导出的文本、老同志的手打记录、从PDF和Word转出来的中间格式、以及各种设备导出的日志。其中最有时代特色的是老电子文档,存盘年代在1990到2005年之间,编码基本以GB2312和GBK为主,少数来自港台设备的文件可能是BIG5。如果你不加处理直接读,十个文件里至少有三四个是乱码。
乱码的根源就一句话:字符集对不上。文件的字节序列是按GBK编码写的,你用UTF-8解码,自然全是“锟斤拷”“烫烫烫”这类经典乱码。反过来,UTF-8文件被GBK解码,会出现一堆奇奇怪怪的汉字组合。处理思路很清晰:先识别编码,再统一转成UTF-8。Python里最常用的工具是chardet,可以给出一段字节的编码概率估计。
import chardet with open('old_report.txt', 'rb') as f: raw = f.read() result = chardet.detect(raw) print(result) # 示例输出:{'encoding': 'GB2312', 'confidence': 0.99}识别出编码之后,转成UTF-8,同时处理BOM问题。BOM是藏在文件头部的几个特殊字节,有些Windows下的TXT会带BOM,不处理的话,写入文件时会多出一个不可见字符,可能在向量化时产生一个异常标记位。
def normalize_text_file(input_path, output_path): with open(input_path, 'rb') as f: raw = f.read() detected = chardet.detect(raw) encoding = detected['encoding'] if detected['encoding'] else 'utf-8' text = raw.decode(encoding, errors='replace') # 去除BOM字符 text = text.lstrip('\ufeff') with open(output_path, 'w', encoding='utf-8') as f: f.write(text)这里有个细节:errors='replace'会把无法解码的字节替换成“�”,这不是彻底解决办法。如果文件里乱码量不大,可以接受;如果乱码率超过一定比例(比如5%),建议人工复查原始文件,很可能文件本身就是损坏的或者被传输过程截断过。编码转换是TXT清洗的第一步,也是最机械的一步,真正有技术含量的是乱码消除之后的语义碎片重组。
2.2 文本碎片重组:让被切断的段落重新长回来
TXT数据的语义碎片问题比乱码更隐蔽。最常见的情况是:原始文件里一个完整段落,因为换行符位置错乱,被拆成了好几行;或者从PDF转出来的TXT,每一行都被强制切成了固定长度;再或者表格转成文本之后,单元格内的内容被割裂成多行,列对应关系完全丢失。
先看段落重组。地质报告有一段描述通常长达几百字,比如“该矿区位于XX背斜南翼,出露地层主要为二叠系下统栖霞组(P1q),岩性为灰黑色厚层状灰岩夹燧石结核,局部见方解石脉充填……”如果这段文字在TXT里被按照屏幕宽度切成了五行,那你直接按行分块,就会把一个完整的地层描述拆成五个毫无意义的小块。正确做法是先做“行合并”:如果某一行结尾没有句号、感叹号、问号等结束标点,并且下一行不是章节标题,就把下一行拼接到当前行后面。
import re def merge_lines(text): lines = text.split('\n') merged = [] current = '' end_punct = set('。!?;:') for line in lines: line = line.strip() if not line: if current: merged.append(current) current = '' continue if current and line and not line[0].isupper() and current[-1] not in end_punct: current += line else: if current: merged.append(current) current = line if current: merged.append(current) return '\n'.join(merged)这只是基本逻辑,实际处理时还要搭配标题识别:如果某一行是“一、区域地质”“二、矿区地质”这种章节标题,即使上一行没有结束标点,也不能接续到上一段文字里。所以合并行的前提是先做一轮标题行标记。
为了避免一条条写正则,我建议先构建一份“探矿业务章节标题词表”,把常见章节名(绪言、区域地质背景、矿床地质特征、矿石类型及品位、资源储量估算、探矿工程布置、采样及化验方法等)整理出来,用精确匹配或前缀匹配去识别。这个动作别看简单,它能保住文本的层级结构,对后续RAG的分块策略非常关键。
再一个典型碎片是表格转文本后的列错位。探矿TXT台账里经常有这样的内容:
ZK101 120.5-121.3 灰白色中粒花岗闪长岩 黄铁矿 2% 1.35g/t ZK101 121.3-122.5 深灰色细粒闪长岩 磁铁矿 5% 0.89g/t ZK102 88.2-90.1 灰岩 方解石脉 3% /这种文本如果直接整体入向量库,检索“ZK101钻孔黄铁矿品位”的时候,系统可能只能捕捉到这一行的一部分字符,更麻烦的是表格列数不一致时,单位与数值对齐错位,检索“1.35g/t”可能匹配到别的行。对于这类行结构明显的文本,清洗思路是先把它们从正文里剥离出来,按行解析成结构化记录,比如用pandas读入,按空格或制表符分列,然后统一编码单位、加工成CSV或JSON,作为独立数据表入库。正文文本和结构化表格分开走,两条线互不干扰,检索效果反而更好。
import pandas as pd rows = [] for line in text.split('\n'): if re.match(r'^ZK\d+\s', line): parts = re.split(r'\s+', line.split('//', 1)[0].strip()) rows.append(parts) df = pd.DataFrame(rows) df.columns = ['钻孔编号', '区间', '岩石名称', '矿物', '含量', '品位'] df.to_csv('drillholes.csv', index=False, encoding='utf-8')2.3 正则清理与专有名词保护
TXT文件的噪声还有几类:页眉页脚残留(比如“XX地质大队 1987年”出现在每一页顶端)、字符上下标丢失后的变形(比如“m2”其实想写的是平方米)、OCR造成的错别字(如果TXT是从扫描件转的,这个很常见)、以及多种空白符和分隔符混用。
正则清理的要点是“先保护,再清理”。把地质专有名词、地名、样品编号、钻孔编号先提取出来占位或加边界符,再执行删除操作,避免误伤。比如你想删掉含“第 页”的页脚,但正文里可能有“第X勘探线”这种内容,不加保护就一起删了。我一般用一个地质术语词典做保护,词典除了常见的岩性矿物名称,还包括地层代号(P1q、T3f、J2x这种格式)、化探元素符号(Cu、Pb、Zn、Au、Ag、Mo)、单位组合(g/t、m%、10^-6)。正则可以匹配这些模式,提前用标记符替换,清理完后再还原。
单位规范化也是探矿数据清洗里的高频操作。一份文档可能同时出现“克/吨”“g/t”“0.0001%”三种表达方式,检索“品位g/t”时命中率肯定会受影响。清洗时建议统一成标准形式。这个操作不复杂,但很值得做,因为探矿用户提问时常常直接说“查一下品位单位g/t的矿体”,如果你的库里面存的是“克/吨”,匹配难度就会增加。
TXT清洗做到位之后,素材里最常见的问题已经消灭了一大半。但探矿业务里大部分核心报告都不是TXT,而是Word。Word文档的格式污染问题,比乱码更让人头疼。
3. Word文档:看不见的格式污染比内容错误更致命
3.1 修订痕迹、批注和域代码:进向量库前必须处理
探矿业务里Word文档几乎绕不开。从储量核实报告、勘查设计书到采样方案、环境恢复治理方案,全部以Word形式流转。而且这些文件很多经过了多轮审核修订,文件里带着大量修订痕迹、批注和域代码。你直接在Word里打开,看到的可能是“已接受修订”的干净版本,但如果你用python-docx或者Apache POI去抽取段落,抽取出来的内容可能同时包含删除线文字、批注文字、域代码的指令内容。
这里就容易出问题。假设一份报告里有这么一句被删除的修订:“该矿体倾向南西,倾角40°”,审稿人删掉了“40°”改成“45°”,修订痕迹完整保留了。你用脚本抽取全部段落时,如果没做修订过滤,两个数值都会被抽出来,向量的语义就自相矛盾了。RAG检索系统可不认识修订前后的逻辑关系,它只会把这句自相矛盾的话当一个完整语义单元。
处理方案是:抽取段落时,过滤掉修订删除的内容;如果需要保留修改后的内容,优先提取修订接受状态下的文本。python-docx处理修订痕迹比较麻烦,它不像浏览Word的批注窗格那么直观,需要操作w:ins和w:del等底层XML标签。Apache POI处理Word表格的单元格宽度是强项,但对修订痕迹的处理同样绕不开XML解析。我的建议是:如果是带修订痕迹的复杂Word文档,先用Word桌面版或WPS执行“接受全部修订并保存”,再进入后续抽取流程。这一步人为操作一劳永逸,比写脚本解析XML省心得多。
批注的处理原则只有一个:全部剔除。批注是给审稿人看的交流信息,不是报告内容的一部分。python-docx提取批注相对容易,doc.comments就能拿到的批注内容,抽取正文时直接跳过。域代码也同理,域代码的显示结果一般是正确的,我们要的是显示文本,不是域的指令字段。
宏也要注意。部分老式Word文档带有宏,可能有宏安全风险。处理这类文档时,不要用你自己的办公软件直接打开并允许宏运行,我习惯用python-docx在只读模式下抽取,或者先转成docx副本再处理。清洗环节本身就要规避执行宏,安全永远是第一位的。
3.2 标题层级与表格结构:提取策略决定分块质量
Word清洗里我比较看重的是标题层级提取。一份储量报告动辄几百页,章节编号有“第1章”“1.1”“1.1.1”三层,甚至还有“附则”“附表”分区。如果抽取出来做成纯文本流水账,后续不管是按固定长度切块还是按段落切块,都会切出大量跨章节碎片。准确提取标题层级,让文档形成树状结构,才能保证RAG分块在“章节语义完整”的边界上进行。
python-docx读取段落样式是常规思路:
from docx import Document def extract_headings(path): doc = Document(path) for p in doc.paragraphs: if p.style.name.startswith('Heading'): print(p.style.name, p.text)但实际情况里,很多探矿报告的标题不是使用“标题1”“标题2”样式,而是通过手动改字体大小实现的。遇到这种情况,python-docx没法直接告诉你哪段是标题,只能靠特征判断:居中、加粗、编号开头(如“第1章”“1.1”“一、”)。这类规则容易误判,我的建议是:构建一个探矿报告章节编号正则库,用正则匹配“第[一二三四五六七八九十百]+章”“[0-9]+(.[0-9]+)*”“[一二三四五六七八九十]+、”等模式,再叠加字体特征,双重校验。宁可少数漏判,也不要错判把正文当标题,因为错判带来的分块错误是灾难性的。
表格的提取策略更细。探矿报告里的表格主要有两类:一类是钻孔一览表、样品分析结果表、资源量估算表这类数据型表格,需要完整结构;另一类是描述性、说明性表格,结构相对松散。数据型表格我建议提取为结构化数据再入库,和TXT台账走同一条路。处理Word表格时,Apache POI的setColumnWidth这类宽度控制用得很多,但清洗场景我们更关心内容抽取。python-docx可以直接读取doc.tables,遍历每个表格的rows和cells,输出CSV。
需要注意跨页表格问题。长表格在Word里经常跨页,有的表格会拆分成多个物理Table,或者表头重复出现。抽取时要判断相邻表格是否可能是同一张表,依据是表头是否重复、列数是否一致。如果有图表编号(“表4-1 钻孔岩心采样分析结果”),直接用编号把跨页片段串起来。这一步如果漏了,一张完整的钻孔化学成分表会被拆成好几块,检索“表4-1 中ZK103的Au品位”时只命中其中一块,数值不完整。
3.3 公式、图表与版本混乱
探矿报告里的公式主要是储量计算相关,比如块段法储量计算公式、体积公式、加权平均品位公式。Word里的公式可能是MathType或OMML格式,清洗时有两种处理路径:简单场景把公式的文本描述抽出来,比如“Q = V × d,式中Q为矿石量,V为体积,d为矿石体重”;复杂场景建议将公式从Word里导出为图片,再把公式的LaTeX或文本描述作为相邻文本补充。热词里有“Word公式转LaTeX”,探矿报告里的公式普遍不难,转成LaTeX或文本描述完全够用,不需要保留图片做识别,这一步能大幅降低向量化的噪声。注意MathType和OMML的解析工具不完全一样,MathType对象是OLE嵌对象,python-docx直接读变量内容可能不够,需要单独处理。
图表更简单也更重要:图片本身不进入RAG文本链路,但图片的标题、图例说明必须保留。很多勘探图件的说明文字里包含了岩性、构造、矿体形态特征,是检索的高价值信息。清洗时把图表标题(“图2-3 XX矿区地质简图”)和紧随其后的图例说明作为独立段落提取,归入对应章节的上下文。
版本混乱是Word清洗的最后一道坎。同一份储量报告可能同时存在“储量报告(最终版).docx”“储量报告(终版).docx”“储量报告(再也不改版).docx”,内容可能还各不相同。清洗入库前必须先做版本核对:比对目录、页数、修改时间,保留内容最新且完整的那一版;如果拿不准,宁缺毋滥,不要多个版本都进库。RAG最怕的就是一个知识点有多个版本互相打架。
说完Word,最重的戏来了:PDF。探矿业务的老档案基本都是PDF,而且什么形态都有。
4. PDF的三种形态:扫描件、原生文本、表格混排的处理差异
4.1 文字版PDF:抽取顺序与布局还原的坑
先分清PDF的类型,类型不同处理路径完全不同。我见过太多人拿到一个PDF就直接跑OCR,结果一个文字版PDF还被OCR出大量错字,得不偿失。判断是否文字版,很简单:用工具直接抽取看看有没有文本层。PyMuPDF(fitz)是一个我长期在用的库,速度快,抽取质量高。
import fitz doc = fitz.open('勘探报告.pdf') for page_num in range(len(doc)): page = doc[page_num] text = page.get_text('text', sort=True) print(text)文字版PDF的麻烦不在于抽不出来,而在于抽取顺序。双栏排版的地质报告很常见:左栏是正文,右栏是表格或图件说明,有些报告还有页眉页脚。用get_text('text')抽取时,默认按文档内部结构顺序输出,经常出现“左栏半段、右栏半段、再回左栏下半段”这种跳读顺序。处理方式是先用get_text('blocks')拿到块的位置坐标,然后按横坐标排序,区分栏位,再按纵坐标合并。
def extract_sorted_text(page): blocks = page.get_text('blocks') # block结构:(x0, y0, x1, y1, text, block_no, block_type) blocks.sort(key=lambda b: (round(b[1] / 20), b[0])) lines = [] for b in blocks: lines.append(b[4].strip()) return '\n'.join(lines)这个按y分粗组再按x排序的操作,本质上是把同一水平线上的块按从左到右排列,跨栏顺序就不会乱了。实际效果因文档而异,需要抽样验证。
文字版PDF的表格抽取可以交给pdfplumber,它能识别线条并还原表格结构:
import pdfplumber with pdfplumber.open('矿产储量报告.pdf') as pdf: for page in pdf.pages: tables = page.extract_tables() for table in tables: for row in table: print(row)pdfplumber对有线表格识别效果好,对无线边框的表格(很多化学分析表只是空格对齐)识别一般。无线表格我倾向于退回到按坐标抽文本,再拿正则去切分,虽然慢但可控。
还有一类坑是页眉页脚贯穿文本层。比如“XX矿区勘查报告 - 第 23 页”这一串字符会出现在每一页的文本抽取结果里,如果不去掉,整份文档里同一个页眉会重复出现几十次,RAG在检索“XX矿区”的时候可能被这个高频短语带偏。清洗时先用正则匹配页眉页脚的固定模式,删掉,再逐页拼接正文。
4.2 扫描件PDF:OCR流程与词典校准
扫描件PDF在探矿领域极其常见,特别是上世纪的地质报告、老图纸、岩心照片扫描版。这些文件没有文本层,只能OCR。OCR的选择很多,Python生态里PaddleOCR和Tesseract是比较实用的两个。个人经验:中英文混排、含大量表格的地质报告,PaddleOCR识别率更好;纯英文文献或字符规整的页面,Tesseract也够用。两个配合着用,按文档类型分流。
paddleocr --image_dir page_001.png --lang ch --use_gpu False --output_dir ./ocr_resultOCR本身的流程不多说,重点在校准和后处理。地质文本里有很多普通OCR模型容易出错的词,包括岩性名称(“闪长玢岩”容易被识别成“闪长粉岩”)、元素符号(“Ag”识别成“Aq”、“Au”识别成“A”)、地层代号(“P1q”识别成“P1g”)以及单位(“g/t”识别成“g/I”)。针对这个问题,我的做法是准备两份词典:
- 地质专有名词词典:从术语标准、勘探规范里提取核心词条。
- OCR常见混淆词典:记录模型容易出错的字符对,比如
0/O、1/l/I、2/Z、6/G、8/B、g/9/q等,以及“黄铁”被识别成“黄钱”这类整词错误。
后处理时先跑一遍OCR得到原始文本,再对文本里出现的候选词做词典匹配替换。替换逻辑要小心,不要整篇文章无差别替换,只处理高置信度的混淆词。举例:
corrections = { '闪长粉岩': '闪长玢岩', '黄钱矿': '黄铁矿', 'Aq': 'Ag', } for wrong, right in corrections.items(): text = text.replace(wrong, right)还有一个非常实用的小技巧:OCR前先做图像预处理。老扫描件经常有倾斜、暗角的毛病,用OpenCV做一次二值化和倾斜校正,识别率能提升一大截。倾斜页面里的表格线会变成折线,影响表格识别,校正角度一般在10度以内校正效果较好。
扫描件里的表格识别属于高难操作。如果表格区域是完整的矩形线条框,PaddleOCR的表格结构识别模型(TableRec)可以输出HTML结构,再转成CSV;如果是手绘表格或者表格线模糊,人工勾选区域反而更快。探矿业务的扫描件表格价值很高(像老的地质剖面表、钻孔柱状图的数据表),值得花人工做几份高质量的,构建一个结构化样本,再反哺自动化脚本。OCR输出的表格结构完整性决定后续数值检索的精读质量,这个钱和时间不该省。
4.3 混排表格PDF:结构化提取与回填
大量勘探报告属于第三类:文字版PDF里嵌着表格、图件、甚至部分扫描插入页。处理这种混排文档,不能单一用文字抽取或单一用OCR,而是要把页面拆开处理。
我通常的做法是:先用PyMuPDF检测每一页的文本覆盖率。如果某页文本量极低、但包含图像,判定为扫描页或图件页,进入OCR链路;如果文本量正常但存在大块跨栏图片,则保留文本层,只对图片做区域识别(针对图件中的标注文字)。表格用pdfplumber识别,再和文本抽取结果做拼接。
输出格式上,强烈建议把清洗后的文档转成带层级标记的Markdown,而不是纯文本。好处在于:后续RAG分块时,可以直接按Markdown标题层级来做边界划分;即使用固定窗口切割,也能感知当前的章节归属。示例:
## 2.3 矿区地质特征 该矿区出露地层主要为二叠系下统栖霞组(P1q),岩性为灰黑色厚层状灰岩夹燧石结核,局部见方解石脉充填。 表2-1 主要岩石化学成分分析结果 | 样品编号 | SiO2(%) | Al2O3(%) | Fe2O3(%) | |------|------|------|------| | H-01 | 63.25 | 15.42 | 4.81 |PDF清洗到这个程度,才能说具备了进RAG的基本素质。但这还只是单一文件层面的清洗,网页数据这一类体量不大但格式问题突出的数据源,也需要单独处理。
5. 网页抓取:正文提取、去广告与结构化编号保留
5.1 从导航页到正文:抓取流程与噪点过滤
探矿业务里需要抓取的网页数据通常有几类:地质资料馆的公开目录页、招标公告、行业标准的在线版本、政府公示的勘查许可证信息、以及相关新闻动态。网页数据的特点是不需要做编码转换(基本都是UTF-8),但页面结构噪声非常多:顶部导航、侧栏推荐、页脚版权、Cookie弹窗、相关链接推荐,都会混进正文抽取结果。
直接用requests加BeautifulSoup硬解析是最容易踩坑的方式。每次改版、每个网站结构不同,解析规则就要跟着改。我的经验是优先用专门针对正文提取的工具,比如trafilatura或readability-lxml,它们会自动识别正文区域,去除导航和页脚,输出干净文本。trafilatura还能保留标题层级和段落结构,对RAG友好。
import trafilatura downloaded = trafilatura.fetch_url('https://example.com/notice/12345') text = trafilatura.extract(downloaded, include_comments=False, include_tables=True, output_format='markdown') print(text)动态渲染页面是另一类难题。很多地质公告页面用JavaScript渲染内容,直接抓HTML可能拿不到正文。这种页面需要先渲染再提取,可以在本地起一个Playwright或Selenium脚本,等页面加载完再执行trafilatura的抽取逻辑。注意渲染会显著增加抓取成本,只对确实需要的页面走这条通路,不要把整站都动态渲染。
网页正文抽取完成之后,还要做一轮针对网页噪声的清洗:去掉“收藏”“打印”“字号”“责任编辑”这类的功能性文字;去重(同一公告可能在不同栏目重复发布);把常见无信息量的段落(“关于我们”“联系我们”)从正文里剔除。清洗后的网页文本同样统一转成Markdown,保留标题层级。
5.2 法规与公告的编号结构:只能在清洗时保住
探矿业务里有一类非常重要的网页文本是法规、标准和公告。这些文本的结构化程度很高:有编号(如“第XX号令”)、有条文序号(第X条)、有章节结构(第一章总则、第二章勘查许可),还会引用其他法规条款。
如果清洗时把这些编号全抹掉,RAG在回答“关于探矿权延续登记的具体规定在第几条”这类问题时,就完全没有引用依据。所以清洗策略和普通文本不同:条文编号必须保留,而且要作为结构锚点。我建议把法规网页转换为结构化度更高的格式——每一“条”作为一个语义单元,前面加编号标记。
## 第三章 探矿权转让 第二十四条 探矿权转让应当符合下列条件: (一)持有探矿权满2年; (二)已缴纳探矿权使用费; (三)完成了规定的最低勘查投入; ……这里的(一)(二)(三)子项也要保留,很多检索场景是用户问“探矿权转让需要满足什么条件”,子项的完整性直接决定回答质量。
网页抓取时还经常遇到一个问题:页面正文和上下文的关联信息丢失。比如一份公告说“依据××规定第九条”,但前文对××规定的完整引用可能在另一个页面。清洗时要考虑跨页面合并,把同一政策不同页面的内容拼成一份完整文档,这样后续RAG不会在回答中引用一个“看情况再说”的悬空条款。
5.3 时效性与链接去重
网页数据的时效性比TXT、Word、PDF都要强。地质公告有发布期限,勘查许可证有有效期,行业标准有废止和替代关系。清洗入库时一定要增加两个字段:发布时间和时效状态。发布时间可以直接从页面正文或数据字段里提取;时效状态则需要判断,比如页面标注“已废止”“已失效”“自2023年5月1日起施行”,清洗时要自动识别并标记。
时效状态的判断在RAG链路里特别重要。用户提问“现在还需要提交××材料吗”,如果库里的依据是已废止的文件,系统会理直气壮地给出一个过时答案。所以清洗时不但要保留时效字段,还要把它和分块内容绑定,或者在文本头部加一行的概述,比如“注意:本文件已于2024年1月废止”。这样即使被切分到的块,也能让模型感知到状态信息。
链接去重也别忽略。同一份公告可能同时存在于“通知公告”和“政策法规”两个栏目,页面URL不同但正文完全一致。清洗时用正文的哈希值做去重,保留来源可靠、发布时间最早或最新的一份。数据量大的时候,还可以用MinHash或局部敏感哈希做近似去重,处理那些只有细微差别的页面。
网页清洗相对于文档清洗来说,工程量不大,但容易让人掉以轻心——往往是抓了几千个页面,正文里混着导航词和页脚词,灌进去之后一搜全是不相关内容。
到这里,四类数据的清洗路径都说完了。但清洗完不等于能上线,你还需要验证清洗效果,否则可能带着问题上线而不自知。
6. 清洗效果怎么验证:从召回率到答案准确率
6.1 规则阀门:机器自查的硬指标
清洗完一批文档,第一件事不是急着向量化,而是跑一轮规则阀门。规则阀门是一组轻量脚本,检查清洗后文本是否还残留典型的脏数据模式。我的检查项包括:
- 乱码残留率:文本里是否还出现“锟斤拷”“�”和异常Unicode字符。
- 空行比例:清洗后文档是否还有大量连续空行。
- 段落平均长度:如果一段话少于15个字符,多半是页眉页脚或表格碎片残留。
- 重复率:相邻段落是否存在重复文本,比如页眉重复。
- 结构完整率:一个文档应该有多少章标题,实际识别到多少。
- 编码一致性:清洗后文档是否统一为UTF-8无BOM。
这些检查项可以用脚本批量跑,生成一份报告。报告里把不合格的文件挑出来重新处理。规则阀门的意义在于:把清洗从“手工作坊”变成“可度量流程”,每一步都能知道丢了多少数据。
6.2 检索链路验证:用真实业务问题集打分
规则阀门解决的是“清洁度”问题,但清洁度高不等于检索精度高。要验证清洗是否真正提升了RAG效果,还需要一组真实业务问题作为评测集。探矿业务问题集我一般分三类来构造:
- 术语实体类:比如“闪锌矿的赋存层位是什么”“矿区出露的地层有哪些”.这类问题考察专有名词和实体的召回能力。
- 数值检索类:比如“ZK103钻孔在120米处的岩性是什么”“铜矿体平均品位是多少”。这类问题考察表格抽取和结构还原效果。
- 规范引用类:比如“申请探矿权延续登记需要提交哪些材料”。这类问题考察条文编号和章节结构的保留情况。
构造好问题集之后,跑一遍RAG链路,对每个问题检查两个指标:top-K命中率(正确答案是否在检索返回的前几块里)和最终答案准确率(基于检索结果的生成回答是否可接受)。我还要强调一个非常容易被忽视的细节:检索命中率比生成答案准确率更能反映清洗效果。生成答案可以靠模型“脑补”蒙对,但检索命中率直接反映清洗质量——相关资料没有被搜到,就是向量化之前的清洗没做好。
实操中,清洗前和清洗后的对比结果非常直观。我之前处理过一批老的地下岩心编录扫描件PDF,清洗前问“矿区矽卡岩的分布范围”,检索返回的是OCR错字“砂卡岩”的乱文本块,回答完全不可用;清洗后同一个问题,检索返回了目标段落的准确上下文,答案可以直接采纳。这就是为什么要花时间在清洗上——它直接影响数据进库后的可用性。
6.3 分块与清洗的配合:光清洗不够,还要切得对
清洗产出的是干净的Markdown文本,下一步是分块。分块策略的选择要和清洗方式强配合。如果清洗时保留了标题层级,分块边界就能按章节语义划分:一个标题下设一个块,块的内容是这个标题下的完整段落集。块过大(超过1000字符)会稀释语义,块过小(少于100字符)会丢失上下文。探矿报告里一块200到800字符之间通常效果较好,但具体要根据文档类型调整:钻孔编录短小精悍,可以分小块;储量报告章节丰富,适合用章节边界划分。
分块时还要考虑重叠(overlap)策略。清洗后的文档本身段落间有强逻辑关系,如果分块完全无重叠,跨段落的问答(比如“根据上一段内容,这个矿体的平均品位如何”)会丢失信息。适当设置20-50字符的重叠可以缓解这个问题。
分块完成后,需要对每个块再抽检一遍,看是否出现跨表格、跨章节的内容拼接。出现这种情况,优先调整分块边界,而不是硬调向量化参数。很多人在RAG精度不足时第一反应是换更好的向量模型,但模型换完了效果依然一般,问题往往就出在清洗和分块环节。
验证通过之后,可以进入实际使用阶段。但探矿数据的清洗不是一次性的,库会增长,文档会更新,清洗规则也要跟着迭代。这些是落地时的经验问题,最后单独说。
7. 落地过程中的真实体会与建议
7.1 先建清洗流水线,再谈知识库
探矿业务的数据清洗千万不要做成一次性的“大扫除”。真实情况是:每隔一段时间就会有一批新文档进来,比如新完成的勘查报告、新公示的出让公告、新修订的规范。如果每次都重新跑一遍手工脚本,效率太低,且容易漏步骤。我强烈建议把清洗流程脚本化、流水线化:
- 第一步,文件格式识别,分发到对应清洗模块。
- 第二步,各模块执行格式清洗,输出标准Markdown和结构化CSV。
- 第三步,规则阀门自动质检。
- 第四步,人工抽检通过后,进入向量化入库。
流水线的价值不只是节省人力,更在于清洗规则可以版本化管理。每次发现一个之前没覆盖的乱码模式或OCR错词,就更新规则库,流水线重跑一遍历史数据也不会遗漏。这个机制让清洗质量持续进化,而不是做完一次就停滞。
7.2 清洗规则与词典要持续更新
没有哪个体量足够的探矿数据能靠一次性正则清干净。每个矿区的文档都有自己的行文习惯,每个时期的地质报告也有不同的格式偏好。比如有些老报告用“品位1.35克/吨”,新报告用“品位1.35g/t”,你需要不断把这类变体纳入规范化词典。地质专有名词词典同样如此,不同矿种有新术语,比如随着稀有金属勘探的展开,锂辉石、花岗伟晶岩这类词汇会频繁出现,词典里没有就容易被OCR或分块环节误处理。
我维护的词典是一个单独的文本文件,每个词条一行,里面不仅有标准术语,还有常见的错误拼写映射。更新流程很简单:在质检阶段发现某个词频繁出问题,就追加到词典。方法论不重要,重要的是坚持记录。
7.3 人工抽检由业务人员来做,开发人员只做兜底
清洗脚本质检能抓住大部分机器可识别的问题,但有一些错漏是脚本发现不了的。比如OCR把“矽卡岩型铜矿床”识别成了“砂卡岩型铜矿床”,单独看这个词似乎没什么问题,但在地质语境里“矽卡岩”才是标准写法。这种问题规则阀门很可能判断“合格”,但业务人员一眼就能看出来。
所以清洗流程的最后一道质检必须由业务人员参与。抽检比例不用太高,每批抽30%到50%就够了。更有效的做法是:让地质工程师直接试用检索系统,发现检索结果里出现怪异词汇或错误术语,反馈回来,再去修正清洗规则。这样形成一个闭环——清洗、入库、检索测试、反馈、再清洗,比一次性追求极致的清洗质量更现实。
7.4 宁缺毋滥:让合格的数据进库
最后一条经验,也是我踩过最多坑之后总结出来的:清洗环节一定要敢做减法。一个PDF扫描件OCR之后文本质量实在不行,与其硬着头皮入库,不如标记为“待人工处理”,暂时不参与检索。RAG的精度取决于最高质量的上下文,一条脏数据有可能会把正确答案的排序挤下去。宁可库里少几份文档,也比让脏数据污染答案强。
文件级别的质量阈值也要设置清楚。我的经验是:OCR错字率超过5%的文档,或者表格结构完全无法还原的文档,先挂起,等人工处理或找替代来源。很多老报告网上有二次整理的Word版或重新录入版,质量比直接OCR高得多,优先用高质量来源。
7.5 一个实用的小技巧:优先清洗高频、高价值文档
如果你手头的文档数量特别大,比如几千份,而人力资源有限,不要平均用力。把所有文档按使用频率和价值分两级:高频高价值文档——储量报告、采样分析数据、勘探规范、招拍挂公告——优先清洗,建立严格的质量标准;长尾文档——行业新闻、历史动态、周边资讯——可以放宽阈值,用一条简单流水线批量处理。先让核心数据保证高精度,再逐步扩大清洗范围。我见过太多项目一上来就追求“把全部数据洗得干干净净再上线”,结果洗到一半失去了耐心,反而拖垮了整个落地节奏。正确的路子是:小步快跑,核心优先。
探矿业务的RAG清洗,本质上是在做一件很枯燥但价值极高的事:把历史数据从“能看”提升到“能查、能答、能引用”。从TXT的乱码与碎片重组,到Word的格式污染清理,再到PDF的OCR校准与表格结构化,最后是网页的正文抽取和时序标记,每一类数据都有自己的方法论。清洗完,再用问题集验证检索效果,形成闭环。这套流程跑通之后,你手里的知识库才真正具备高精度检索的底子。我也还在不断迭代自己这套清洗规则,每次处理新批次的数据,都能发现一些没见过的脏数据形态。这个过程没有终点,但每一步的改进都在实打实地提升回答质量,这就是做这件事最有价值的地方。