多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

探矿RAG文档清洗实战:TXT、Word、PDF与网页多格式处理方案

探矿RAG文档清洗实战:TXT、Word、PDF与网页多格式处理方案 1. 探矿业务文档处理的真实困境地质勘探行业有一个很反直觉的现实最核心的资料往往不是数据库里那些规整的钻孔数据而是散落在各个项目组电脑里的TXT记录、Word报告、PDF图件和内部网页。我刚接手一个探矿知识库项目的时候光是整理一个矿区近十年的资料就差点崩溃——同一个钻孔的编录信息2015年存的是TXT2018年变成了Word2021年又变成了扫描版PDF还有一部分只存在于内网某个没人维护的HTML页面里。这些资料如果只是“存着”倒还好问题是探矿业务对检索精度的要求极高。你搜“ZK1201 金品位”不能给我返回“ZK1201 见矿化”这种模糊结果你搜“蚀变带宽度”不能把“蚀变带走向”也混进来。这就逼着我们必须做RAG而且必须做高质量的RAG清洗。TXT、Word、PDF、网页这四种格式每一种都有自己的一套坑清洗策略完全不能通用。这篇文章就是把我踩过的坑和最终跑通的方案完整拆开讲。不管你是刚接触RAG的地质信息化人员还是正在为多格式文档头疼的算法工程师下面这些内容都能直接拿去用。我会从整体设计思路讲到每个格式的具体清洗步骤再给出参数选择的计算依据和常见问题的排查方法。2. 整体清洗架构的设计思路2.1 为什么不能“一把梭”统一处理很多人做RAG清洗的第一反应是把所有文档转成纯文本然后统一分块、统一向量化。这个思路在通用场景下勉强能用但在探矿业务里会出大问题。原因很简单——探矿文档的信息密度分布极不均匀。一份PDF地质报告里可能前二十页都是区域地质背景这种低价值内容而第三十七页的一张钻孔柱状图表格才是核心一份TXT编录文件里可能每一行都是关键数据没有任何废话。如果统一按固定字数分块PDF里的低价值内容会稀释检索结果TXT里的高价值数据又可能被切断。所以我最终采用的方案是“分格式预处理 统一语义分块 元数据增强”的三层架构。第一层针对TXT、Word、PDF、网页分别做结构化提取第二层把提取结果统一成带层级标记的中间格式第三层再根据探矿业务的语义特征做分块和元数据注入。这个架构的核心考量是清洗的目标不是“把文档变成文本”而是“把文档变成可检索的知识单元”。知识单元的边界应该由内容语义决定而不是由原始格式决定。2.2 中间格式的设计与选择中间格式我选的是带标记的Markdown变体而不是纯文本或JSON。纯文本丢失了标题层级和表格结构JSON又太啰嗦不利于后续人工校验。Markdown的好处是既能保留结构信息用#表示标题层级、用|表示表格又足够简洁地质人员自己也能看懂。具体来说我在标准Markdown基础上加了几个自定义标记用[TABLE]和[/TABLE]包裹表格区域用[FORMULA]标记公式位置用[FIGURE:描述]标记图件位置。这些标记在后续分块时会被识别确保表格不会被从中间切断公式不会被当成普通文本处理。注意中间格式一定要保留原始文档的页码或段落编号信息。探矿报告经常需要回溯原文如果清洗后丢失了位置信息检索到结果也没法用。2.3 元数据体系的构建探矿业务的元数据和通用文档完全不同。除了常规的文件名、创建时间、作者之外必须注入业务元数据矿区名称、钻孔编号、地层代号、矿种类型、报告类型普查/详查/勘探、坐标范围。这些元数据有两个作用一是作为检索时的过滤条件二是作为分块时的语义锚点。我试过不做元数据直接向量化结果搜“金品位”会把煤矿报告也返回回来。后来加了矿种类型过滤准确率直接提升了四成。元数据的提取不能全靠自动探矿报告里的矿区名称写法五花八门有的写“XX沟金矿”有的写“XX沟Au矿”必须建一个别名映射表来做归一化。3. TXT格式的清洗要点与实操3.1 TXT在探矿业务中的特殊地位TXT在探矿行业里不是“低端格式”恰恰相反很多核心数据就是以TXT形式存在的。钻孔编录仪导出的数据是TXT化探分析结果导出的是TXT甚至一些老地质队员自己写的备忘录也是TXT。这些TXT的编码格式极其混乱GBK、GB2312、UTF-8、UTF-8 with BOM都有还有少量是Latin-1编码混进来的。我遇到过最离谱的一个TXT文件前半部分是GBK编码后半部分因为编辑软件切换变成了UTF-8直接读取会报错。所以TXT清洗的第一步永远是编码检测和统一转换这一步做不好后面全白搭。3.2 编码检测与转换的可靠方案不要用Python内置的open()直接读也不要用chardet单次检测就下结论。我的做法是分三步走先用charset-normalizer做初步检测然后尝试用检测结果解码如果失败则回退到GBK再失败回退到Latin-1并记录警告。对于检测置信度低于0.8的文件强制人工确认。from charset_normalizer import from_path def detect_and_read(filepath): results from_path(filepath) best results.best() if best is None: # 回退策略 for enc in [gbk, gb18030, latin-1]: try: with open(filepath, r, encodingenc) as f: return f.read(), enc except UnicodeDecodeError: continue raise ValueError(f无法解码文件: {filepath}) if best.chaos 0.2: # 置信度阈值 return str(best), best.encoding else: # 低置信度标记待人工确认 return str(best), f{best.encoding}_LOW_CONF实测下来charset-normalizer对中文地质文档的检测准确率比chardet高不少尤其是对GB18030这种超集编码的识别更准。但即便如此我仍然建议对置信度低的文件做人工抽检因为探矿数据一旦编码错了数字会变成乱码而乱码的数字看起来和正常数字差别不大很容易蒙混过关。3.3 结构化解析与字段映射TXT编录文件通常有固定的列结构但不同项目组的列顺序和分隔符可能不同。有的用制表符有的用逗号有的用多个空格。我写了一个自适应解析器先统计每行的分隔符出现次数取众数作为分隔符然后根据表头关键词做字段映射。比如表头里有“孔深”“品位”“岩性”这三个词就映射到标准字段depth、grade、lithology。如果表头缺失就根据数据特征推断第一列全是递增数字的大概率是孔深第二列是0到10之间小数的大概率是品位。这种推断当然不保证百分百准确但配合人工校验能把效率提升好几倍。实操心得TXT解析一定要保留原始行号。探矿数据经常需要和纸质记录核对没有行号根本找不到对应位置。我在中间格式里用!-- line:123 --的形式保留行号检索结果展示时可以直接跳转。4. Word文档的深度清洗策略4.1 Word文档的“暗坑”远比想象的多Word文档看起来比PDF好处理实际上坑更多。探矿报告里的Word文档经常包含嵌入的Excel表格、Visio图件、MathType公式、修订记录、批注、页眉页脚里的钻孔编号。如果你只用python-docx读段落文本这些信息全部丢失。我最初用python-docx直接读paragraphs结果发现表格里的数据全没了。后来改用遍历文档元素的方式同时处理段落和表格。但这样还不够因为有些表格是嵌套的表格里还有表格。最终我写了一个递归函数来处理嵌套结构最大递归深度设到5层超过5层的极少见遇到了就单独处理。4.2 表格提取的精度控制探矿报告里的表格有两种一种是规则表格行列对齐另一种是“伪表格”用空格或制表符模拟的。对于规则表格python-docx的table.rows和table.columns能直接读。但要注意合并单元格的问题——python-docx对合并单元格的处理是返回重复值需要做去重。对于伪表格我的做法是先检测连续多行是否具有相似的分隔模式如果是则按分隔符切分。这里有个经验阈值连续5行以上具有相同数量的分隔符就判定为伪表格。少于5行的可能是偶然对齐强行解析反而会引入噪声。def extract_tables(doc): tables [] for i, table in enumerate(doc.tables): rows [] for row in table.rows: cells [cell.text.strip() for cell in row.cells] # 去重合并单元格 deduped [] prev None for c in cells: if c ! prev: deduped.append(c) prev c rows.append(deduped) tables.append({index: i, data: rows}) return tables4.3 公式与特殊符号的处理探矿报告里的公式主要是品位计算公式、储量计算公式。这些公式在Word里可能是MathType对象也可能是OMML格式。python-docx读不了MathType但可以读OMML。我的方案是先用docx读OMML读不到的再用olefile尝试提取MathType的二进制内容如果还不行就标记为[FORMULA:无法解析]并保留位置信息。对于特殊符号比如地层代号里的下标数字、品位单位里的g/t这些在Word里可能是普通文本也可能是特殊字符。我的做法是统一做Unicode归一化把全角字符转半角把特殊空格转普通空格把各种破折号统一成-。这一步看起来简单但不做的话后续检索会出各种莫名其妙的问题。5. PDF解析的硬骨头怎么啃5.1 文本型PDF与扫描型PDF的分流PDF是探矿资料里最复杂的格式没有之一。首先要区分文本型PDF和扫描型PDF。文本型PDF可以直接提取文字扫描型PDF必须走OCR。判断方法很简单用pdfplumber读第一页如果提取出的字符数少于50个基本可以判定是扫描型。但实际情况更复杂——有些PDF是混合型的前几页是文本后面是扫描件。我的做法是逐页判断对每一页单独决定用文本提取还是OCR。这样虽然慢一些但准确率最高。OCR我用的PaddleOCR对中文地质报告的识别效果比Tesseract好很多尤其是对表格和手写体数字的识别。5.2 表格与图件的分离提取探矿PDF里的表格往往是最有价值的部分但也是最难提取的。pdfplumber的extract_tables()对规则表格效果不错但对跨页表格和合并单元格表格经常出错。我的策略是先用extract_tables()试提取如果返回的表格行列数合理行数大于2列数大于2就采用如果返回空或明显异常就回退到基于文本坐标的聚类方法。基于坐标的方法原理是把页面内所有文本块按y坐标聚类成行再按x坐标聚类成列然后重建表格。这个方法对不规则表格更鲁棒但计算量大。我一般只在extract_tables()失败时才用。图件的处理更麻烦。探矿报告里的图件包括钻孔柱状图、剖面图、等值线图。这些图件里的文字信息图例、标注、坐标如果丢失检索时就找不到。我的做法是用OCR对图件区域做文字识别把识别结果作为图件的描述文本存入元数据。这样搜“ZK1201柱状图”时即使图件本身是图片也能通过OCR文本检索到。5.3 页码与章节结构的重建PDF的页码和实际报告页码经常不一致因为封面、目录可能用了罗马数字或单独编号。我的做法是提取PDF的物理页码同时尝试从页眉页脚中识别报告页码建立映射关系。章节结构则通过字体大小和加粗程度来推断——一级标题通常字号最大且加粗二级标题次之。这个推断当然不完美但比不做要好得多。我试过用机器学习分类器来做章节识别训练了200份标注报告准确率能到85%左右。但对于大多数项目来说基于规则的方案已经够用了没必要上模型。6. 网页资料的抓取与清洗6.1 探矿相关网页的类型分析探矿业务涉及的网页主要有三类一是内部地质资料管理系统页面二是矿业权公示系统页面三是行业资讯网站。这三类的清洗策略完全不同。内部系统页面通常有固定的DOM结构可以用XPath精准提取公示系统页面结构多变需要更通用的正文提取算法资讯网站则要过滤大量广告和导航栏。我用的正文提取方案是trafilatura加自定义规则。trafilatura对新闻类页面的正文提取效果很好但对表格密集的地质数据页面效果一般。所以我在trafilatura之后加了一层表格检测如果页面里表格占比超过30%就改用pandas.read_html()直接读表格。6.2 动态渲染页面的处理有些内部系统页面是JavaScript动态渲染的直接请求HTML拿不到数据。这种情况必须用浏览器自动化。我用的playwright比selenium快且稳定。关键是要设置合理的等待策略——不要用固定sleep而是等待特定元素出现或网络空闲。from playwright.sync_api import sync_playwright def fetch_dynamic_page(url, wait_selector): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, wait_untilnetworkidle) page.wait_for_selector(wait_selector, timeout10000) content page.content() browser.close() return content注意抓取内部系统页面一定要控制频率建议每次请求间隔不低于3秒。探矿单位的内部系统往往负载能力有限抓太快容易把系统搞崩到时候就不是技术问题了。6.3 网页元数据的提取网页的元数据比文件更丰富但也更杂乱。标题、发布时间、作者、来源这些信息可能藏在meta标签里也可能在页面正文里。我的做法是优先读meta读不到再用正则从正文里匹配。对于地质资料页面还要额外提取矿区名称、矿种、坐标这些业务元数据这些通常需要自定义规则。7. 统一分块与向量化策略7.1 探矿语义分块的参数计算分块大小是RAG清洗里最关键的参数之一。分块太大检索精度下降分块太小上下文丢失。对于探矿文档我的经验值是TXT编录数据按50行一块Word报告按段落一块但不超过800字PDF按章节一块但不超过1200字网页按正文段落一块。这个参数不是拍脑袋定的。我做过对比实验用不同分块大小跑同一组查询看召回率和准确率的变化。结果发现TXT数据在50行左右时F1值最高Word在600到800字之间最稳定PDF因为章节长度差异大用固定字数不如用章节边界。7.2 重叠窗口的设置分块之间需要重叠否则跨块的信息会被切断。重叠比例我一般设10%到15%。对于TXT数据重叠5行对于Word和PDF重叠80到120字。重叠太多会导致重复检索太少又起不到衔接作用。这个参数也需要根据实际数据调没有万能值。7.3 向量化模型的选择向量化模型我试过好几个最终选的是bge-large-zh。原因有三一是对中文地质术语的语义捕捉比通用模型好二是支持长文本最大512token适合探矿报告里的长段落三是本地部署方便不依赖外部服务。但bge-large-zh对数字和公式的向量化效果一般。比如“金品位3.5g/t”和“金品位5.3g/t”在向量空间里距离很近检索时容易混淆。我的解决方案是在向量化之前把关键数字和单位提取出来作为独立字段存入元数据检索时用元数据过滤来补充向量检索的不足。8. 常见问题与排查技巧实录8.1 编码乱码问题速查现象可能原因排查方法解决方案中文变问号编码检测错误用charset-normalizer重新检测手动指定GB18030数字变乱码混合编码分段检测编码按段落分别解码后拼接部分字符正常部分乱码BOM问题检查文件头是否有BOM去除BOM后重新解码全角半角混用输入法切换统计全角字符比例Unicode归一化8.2 表格提取失败的排查路径表格提取失败是最常见的问题。我的排查顺序是先看PDF是否是扫描型是则走OCR再看表格是否有合并单元格有则用坐标聚类法再看表格是否跨页跨页则先合并页面再提取最后看表格是否有旋转旋转则先矫正再提取。这个顺序能解决九成以上的表格提取问题。8.3 检索结果不准确的调优方法如果检索结果不准确先别急着换模型。按这个顺序排查第一检查分块是否合理有没有把关键信息切断第二检查元数据是否完整过滤条件是否生效第三检查查询语句是否需要改写探矿术语有很多别名用户搜“金”可能实际想要的是“Au”第四检查向量模型是否适合当前数据必要时换模型或做微调。实操心得我习惯在清洗完成后做一个“检索冒烟测试”——准备20个典型查询看返回结果是否合理。这个测试能在正式上线前发现大部分问题比上线后被用户投诉要好得多。8.4 性能优化的几个关键点清洗大量文档时性能很容易成为瓶颈。我的优化经验是编码检测用多进程PDF解析用多线程因为PDF解析是IO密集型OCR用GPU加速向量化用批处理。另外中间结果一定要缓存不要每次重新跑。我用sqlite做缓存key是文件路径加修改时间这样文件没变就直接读缓存。9. 我踩过的几个大坑第一个坑是低估了TXT编码的复杂性。我一开始用chardet检测结果一批GB18030编码的文件被误判为GBK导致部分生僻字变成乱码。后来换成charset-normalizer并加了人工抽检才解决。这个教训是编码检测没有百分百可靠的方案必须留人工兜底。第二个坑是PDF表格提取时忽略了跨页表格。有一份报告的关键品位表格跨了两页我按单页提取后两个半截表格都没法用。后来加了跨页检测逻辑——如果上一页最后一个表格和下一页第一个表格的列数相同且表头相似就自动合并。第三个坑是网页抓取时没控制频率把内部系统搞挂了。虽然没造成严重后果但被地质队的人说了好几天。从那以后我所有抓取任务都加了限速并且尽量在非工作时间跑。第四个坑是向量化时没处理数字。早期版本搜“品位3.5”会返回一堆“品位5.3”“品位3.8”的结果因为向量模型对数字不敏感。后来把数字提取为元数据字段检索时先做数值范围过滤再做向量检索准确率才上来。10. 后续可以扩展的方向这套清洗方案目前跑通了两万多份探矿文档检索准确率在典型查询上能到八成以上。但还有几个方向可以继续优化。一是公式的深度解析目前只能标记位置还不能把公式转成可计算的形式二是图件的自动描述目前靠OCR提取文字还不能理解图件的语义内容三是多语言支持有些探矿报告里有英文或俄文资料目前的方案对非中文支持有限。另外知识图谱和RAG的结合也值得尝试。探矿业务里实体关系很明确——钻孔属于矿区样品属于钻孔分析结果属于样品。如果能把这些关系抽出来建成图谱再和向量检索结合检索精度还能再上一个台阶。我目前在做这方面的实验有进展了再另写一篇分享。
返回列表