☰
探矿RAG知识库构建:TXT、Word、PDF与网页多格式文档清洗实战
2026/10/8 3:15:00 网站建设 项目流程

1. 探矿数据清洗到底难在哪:从一堆“乱码”说起

搞探矿这行的人都有一个共同的痛:数据来源太杂了。地质队的原始记录可能是手写的TXT台账,钻探报告是Word文档,化验分析单是PDF,历史资料还散落在各种内部网页上。你想把这些东西喂给RAG知识库做智能检索,第一步就卡住了——文件打开全是乱码,表格错位,公式变成一堆问号。

我接手过一个探矿项目的知识库搭建,光是数据清洗就花了整整三周。当时拿到手的资料包括:127个TXT文件(编码从GBK到UTF-8都有,还有几个是GB2312混着BOM头)、89份Word报告(里面嵌了AxMath和MathType的公式)、200多份PDF(有扫描件也有原生电子版)、以及从内部系统导出的HTML页面。这些东西如果不做清洗直接入库,检索出来的结果基本没法看——你搜“铜品位”,它给你返回一段乱码;你搜“钻孔倾角”,它把表格里的数字全拼成了一行。

这篇文章就是把我踩过的坑和最终跑通的方案完整拆开讲。核心关键词是RAG、TXT、Word、PDF、网页,但我不打算讲那些泛泛的“RAG框架怎么选”,而是聚焦在一个具体场景:探矿业务中多格式文档的清洗与结构化入库。适合谁看?如果你正在搭建地质、矿业、勘探相关的知识库,或者你手头有一堆格式混乱的技术文档要处理,这篇内容可以直接抄作业。

先说清楚一个基本认知:RAG的效果上限,在清洗阶段就已经决定了。很多人把精力花在调模型、换向量库上,结果检索精度死活上不去,回头一看,入库的文本本身就是垃圾。Garbage in, garbage out,这句话在RAG项目里是铁律。

2. 整体清洗架构:为什么我不推荐一把梭

2.1 分而治之:按格式拆管道而不是统一转换

我见过不少团队的做法是:不管什么格式,先统一转成TXT,然后再做后续处理。这个思路听起来简单,但实际跑下来问题很大。PDF转TXT会丢失表格结构,Word转TXT会把公式变成乱码,网页转TXT会把导航栏和正文混在一起。你后面再想从这堆纯文本里恢复结构,成本比一开始就分格式处理要高得多。

我的方案是按文件格式拆成四条独立管道,每条管道有自己的解析策略和输出规范,最后汇入统一的结构化中间层。具体来说:

  • TXT管道:重点解决编码识别和字段切分
  • Word管道:重点解决公式提取和表格还原
  • PDF管道:重点区分原生电子版和扫描件,走不同路径
  • 网页管道:重点做正文抽取和噪声去除

这四条管道的输出统一为带元数据的结构化JSON,包含三个核心字段:content(清洗后的正文)、metadata(来源、日期、作者、文件类型)、structure(标题层级、表格、公式的定位信息)。这样做的好处是,后面不管你是入向量库还是入图数据库,都有干净的原料可用。

2.2 清洗深度的取舍:不是越干净越好

这里有一个容易被忽略的问题:清洗到什么程度算合适?我一开始追求“极致干净”,把所有特殊字符、所有格式标记全删了,结果发现检索效果反而变差了。为什么?因为探矿文档里很多关键信息恰恰藏在那些“看起来像噪声”的东西里——比如化学式里的下标数字、坐标数据里的度分秒符号、岩性描述里的特殊代号。

后来我调整了策略:保留语义相关的特殊字符,只清除真正的噪声。具体判断标准是:

内容类型处理方式理由
化学式下标/上标保留并标准化Cu²⁺、Fe₂O₃等是检索高频词
坐标度分秒符号保留原格式°′″是地质数据核心标识
页眉页脚清除纯噪声,干扰检索
表格边框线转为结构化数据保留行列关系
公式图片OCR+LaTeX双路提取纯文本化会丢失语义
乱码字符按编码修复后保留可能是编码问题而非真乱码

注意:清洗的目标是“可检索、可理解”,不是“看起来整洁”。这两个目标有时候是冲突的,遇到冲突时优先保检索。

3. TXT文件清洗:编码问题是第一道坎

3.1 编码检测:为什么chardet经常不靠谱

TXT文件看起来最简单,实际上编码问题最让人头疼。我拿到的127个TXT文件里,用chardet检测的结果有将近三成是错的。原因很简单:探矿领域的TXT文件通常很短,而且包含大量专业术语和数字,chardet的统计模型在这种文本上表现很差。

我的做法是多引擎交叉验证+业务规则兜底。具体流程:

import chardet import cchardet def detect_encoding(file_path): with open(file_path, 'rb') as f: raw = f.read() # 多引擎检测 results = [] results.append(chardet.detect(raw)) results.append(cchardet.detect(raw)) # 业务规则:探矿TXT常见编码优先级 priority = ['utf-8', 'gbk', 'gb2312', 'gb18030', 'utf-16'] # 如果多引擎结果一致且置信度高,直接采用 if len(set(r['encoding'] for r in results)) == 1 and results[0]['confidence'] > 0.9: return results[0]['encoding'] # 否则按优先级逐个尝试解码 for enc in priority: try: raw.decode(enc) return enc except UnicodeDecodeError: continue return 'utf-8' # 兜底

这里有个关键点:GB18030是GBK的超集,如果GBK解不出来,试试GB18030往往能成。另外,有些TXT文件开头有BOM头,utf-8-sig能自动处理,但如果你用utf-8去读就会在开头多出一个\ufeff字符,这个字符进入向量库后会污染检索结果。

3.2 字段切分:从自由文本到结构化记录

探矿TXT文件的内容通常是有固定格式的,比如钻孔记录可能是这样的:

ZK001 钻孔 开孔日期:2023-05-12 终孔深度:156.3m 0-5m 第四系覆盖层 黄色粘土 5-12m 强风化花岗岩 褐灰色 岩芯采取率85% 12-45m 中风化花岗岩 灰白色 见黄铜矿化 ...

这种文本如果不做切分直接入库,检索“黄铜矿化”会返回整段文本,用户还得自己找。我的做法是用正则+规则模板做字段抽取:

import re def parse_drill_log(text): records = [] # 匹配钻孔头部信息 header_pattern = r'(ZK\d+)\s+钻孔\s+开孔日期:(\S+)\s+终孔深度:(\S+)' header = re.search(header_pattern, text) # 匹配分层记录 layer_pattern = r'(\d+-\d+m)\s+(\S+)\s+(\S+)\s+(.+)' for match in re.finditer(layer_pattern, text): records.append({ 'depth_range': match.group(1), 'layer_name': match.group(2), 'color': match.group(3), 'description': match.group(4) }) return { 'hole_id': header.group(1) if header else None, 'date': header.group(2) if header else None, 'depth': header.group(3) if header else None, 'layers': records }

这样切分之后,每条分层记录都是独立的检索单元,检索“黄铜矿化”直接命中对应层位,精度提升非常明显。

3.3 实操心得:TXT清洗的三个坑

第一个坑:换行符不统一。Windows的\r\n、Linux的\n、老Mac的\r混在一起,如果不统一处理,后面按行切分会出问题。我一般先用text.replace('\r\n', '\n').replace('\r', '\n')统一。

第二个坑:全角半角混用。探矿文档里经常出现全角数字和半角数字混用的情况,比如“深度156.3m”和“深度156.3m”。检索时如果不做归一化,用户搜“156.3”可能匹配不到全角的版本。我的做法是在清洗阶段统一转为半角,但保留原始文本在metadata里。

第三个坑:空行和空格的处理。有些TXT文件用大量空行做视觉分隔,有些用空格对齐表格。我的策略是:连续空行压缩为一个,行首行尾空格去除,但行内多个空格如果是对齐用途则保留(通过检测列对齐模式判断)。

4. Word文档清洗:公式和表格是两大难关

4.1 公式提取:AxMath、MathType与OMML的三角关系

Word文档里的公式是最让人头疼的部分。探矿报告里大量使用AxMath和MathType插入公式,这两种工具生成的公式在Word里存储为OLE对象,直接用python-docx读取只能拿到一个占位符,拿不到公式内容。

我试过几种方案,最终跑通的是OMML转换+图片OCR兜底的组合拳:

from docx import Document from lxml import etree def extract_formulas(docx_path): doc = Document(docx_path) formulas = [] # 方法1:提取OMML公式(Word原生公式) nsmap = {'m': 'http://schemas.openxmlformats.org/officeDocument/2006/math'} for para in doc.paragraphs: omml_elements = para._element.findall('.//m:oMath', nsmap) for omml in omml_elements: # OMML转LaTeX latex = omml_to_latex(omml) formulas.append({'type': 'omml', 'latex': latex}) # 方法2:提取嵌入的OLE对象(AxMath/MathType) for rel in doc.part.rels.values(): if 'oleObject' in rel.reltype: # 提取OLE对象,转为图片后OCR ole_data = rel.target_part.blob img = ole_to_image(ole_data) latex = ocr_formula(img) # 使用公式OCR模型 formulas.append({'type': 'ole', 'latex': latex}) return formulas

这里的关键是:OMML转LaTeX有现成的XSLT样式表,可以直接用。而AxMath和MathType的OLE对象需要先转成图片,再用公式OCR模型识别。我实测下来,OMML转换的准确率接近100%,OLE+OCR的准确率在85%左右,对于复杂的积分和矩阵公式可能会出错,需要人工校验。

提示:如果你发现Word里同时装了AxMath和MathType,插入公式时可能会跳转到MathType。这不是bug,是MathType抢占了默认公式编辑器。在AxMath设置里把“设为默认公式编辑器”勾上就能解决。

4.2 表格还原:合并单元格是最大的坑

Word表格的清洗难点不在简单表格,而在合并单元格。探矿报告里的钻孔数据表经常有这样的结构:

钻孔编号深度区间岩性品位
ZK0010-5m粘土-
5-12m花岗岩0.3%
12-45m花岗岩0.8%

ZK001这个单元格是跨行合并的。如果你用python-docx直接遍历单元格,会发现合并单元格的内容只在第一个位置出现,后面的是空的。我的处理方式是先检测合并模式,再填充:

def parse_word_table(table): rows = [] for i, row in enumerate(table.rows): row_data = [] for j, cell in enumerate(row.cells): # 检测是否是合并单元格的延续 if cell._tc is row.cells[j-1]._tc if j > 0 else False: row_data.append(row_data[-1]) # 沿用上一个单元格的值 else: row_data.append(cell.text.strip()) rows.append(row_data) return rows

这个逻辑的核心是:通过比较底层XML元素判断是否是同一个单元格。如果当前单元格和左边的是同一个tc元素,说明是横向合并,沿用左边的值。纵向合并类似处理。

4.3 样式与层级:标题级别决定了检索粒度

Word文档的标题样式(Heading 1/2/3)是天然的层级结构,这个信息在清洗时一定要保留。我的做法是把标题层级转成面包屑路径,附加到每个段落的metadata里:

def extract_with_hierarchy(docx_path): doc = Document(docx_path) sections = [] current_path = [] for para in doc.paragraphs: if para.style.name.startswith('Heading'): level = int(para.style.name.split()[-1]) # 更新面包屑路径 current_path = current_path[:level-1] + [para.text] else: if para.text.strip(): sections.append({ 'content': para.text, 'path': ' > '.join(current_path), 'style': para.style.name }) return sections

这样检索“ZK001钻孔的岩芯采取率”时,可以精确定位到“第三章 钻探工程 > 3.2 钻孔质量 > ZK001”这个路径下的内容,而不是全文模糊匹配。

5. PDF解析:原生电子版和扫描件要分开处理

5.1 先判断PDF类型:这一步不能省

PDF文件分两种:原生电子版(文字可选)和扫描件(文字是图片)。这两种的处理路径完全不同,如果不做判断直接上OCR,原生电子版会浪费大量时间且准确率反而下降。

判断方法很简单:

import fitz # PyMuPDF def is_scanned_pdf(pdf_path, sample_pages=5): doc = fitz.open(pdf_path) total_text = 0 total_pages = min(len(doc), sample_pages) for i in range(total_pages): page = doc[i] text = page.get_text() total_text += len(text.strip()) # 如果平均每页文字少于50个字符,判定为扫描件 return total_text / total_pages < 50

5.2 原生电子版:表格和公式的提取策略

原生电子版PDF的表格提取,我推荐用pdfplumber,它对表格线的识别比PyMuPDF更准。但探矿报告里很多表格没有明确的边框线,这时候需要用camelot的stream模式,基于文字对齐来推断表格结构。

公式提取更麻烦。PDF里的公式通常是嵌入的字体或者图片。如果是字体,PyMuPDF能提取出文字但会丢失数学结构;如果是图片,就需要走OCR。我的策略是:

  • 先用PyMuPDF提取所有文本块
  • 检测文本块中是否包含数学符号密集区域
  • 对疑似公式区域截图,走公式OCR
  • 其余文本正常提取

5.3 扫描件:OCR之后的清洗更重要

扫描件走OCR是必然的,但OCR出来的文本噪声很大。探矿报告扫描件常见的问题包括:表格线被识别成字符、页眉页脚混入正文、手写批注干扰。我的清洗流程是:

  1. 版面分析:用PaddleOCR的版面分析功能,先区分正文、表格、图片区域
  2. 表格重建:对表格区域单独做OCR,用PP-Structure重建表格结构
  3. 文本后处理:用正则清除OCR常见的噪声模式,比如连续的点号、孤立的单字符行
  4. 人工抽检:随机抽10%的页面人工校验,发现系统性问题及时调整

实操心得:扫描件OCR的准确率再高,也建议保留原始图片的引用。在metadata里存一个source_image字段,指向原始扫描页的截图。这样当用户对检索结果存疑时,可以回溯到原图核对。

6. 网页数据清洗:正文抽取与噪声去除

6.1 正文抽取:Readability算法与自定义规则

探矿业务相关的网页数据来源包括:内部地质资料系统、在线地质词典、矿业新闻网站。这些网页的正文抽取不能用同一套规则。

我一般用readability-lxml做第一轮抽取,然后针对特定站点补充自定义规则:

from readability import Document import requests def extract_web_content(url): resp = requests.get(url, timeout=10) doc = Document(resp.text) # readability抽取正文 content = doc.summary() # 针对特定站点的补充清洗 if 'geology.com' in url: # 移除广告和推荐阅读 content = remove_ads(content) return { 'title': doc.title(), 'content': content, 'url': url }

6.2 动态网页:什么时候该用浏览器渲染

有些地质资料系统是前后端分离的,直接请求HTML拿不到数据,需要等JavaScript渲染完成。这时候就得上Playwright或Selenium。但我的原则是:能不用浏览器就不用,因为浏览器渲染的速度比直接请求慢一个数量级。

判断标准很简单:先用requests请求一次,如果返回的HTML里没有目标内容(比如表格数据是空的),再考虑上浏览器。另外,有些网站有反爬机制,这时候需要设置合理的请求头、控制请求频率,但这些都是常规操作,不展开讲。

6.3 网页清洗的特殊问题:导航栏和评论区

网页数据最烦人的是噪声太多。导航栏、侧边栏、评论区、相关推荐,这些内容如果不清理,会严重干扰检索。我的做法是维护一个噪声选择器黑名单:

NOISE_SELECTORS = [ 'nav', 'header', 'footer', 'aside', '.sidebar', '.comment', '.related-posts', '#nav', '#footer', '#comments' ] def remove_noise(soup): for selector in NOISE_SELECTORS: for element in soup.select(selector): element.decompose() return soup

这个黑名单需要根据实际抓取的站点不断补充。我一般会先抓10个页面,人工看一下有哪些噪声区域,把对应的选择器加进去。

7. 常见问题与排查技巧实录

7.1 编码问题速查表

现象可能原因解决方法
中文显示为乱码编码检测错误尝试GB18030解码
开头多出\ufeffUTF-8 BOM头用utf-8-sig读取
部分字符显示为?编码不支持该字符转用GB18030或UTF-8
全文都是方块字体缺失或编码完全错误检查原始文件编码
数字和字母正常但中文乱码典型的GBK/UTF-8混淆用GBK重新解码

7.2 公式提取失败排查

公式提取失败通常有三种情况:

情况一:OMML元素找不到。检查命名空间是否正确。Word的OMML命名空间是http://schemas.openxmlformats.org/officeDocument/2006/math,但有些文档可能用了不同的命名空间前缀。

情况二:OLE对象无法转换。AxMath和MathType的OLE对象需要对应的解析器。如果没有安装对应的软件,可以尝试用LibreOffice的命令行模式转换。

情况三:OCR识别率低。公式OCR对图片质量要求很高。如果原始图片分辨率低于150dpi,识别率会急剧下降。建议在OCR前先做图像增强:二值化、去噪、放大。

7.3 表格结构错乱的处理

表格清洗最常见的问题是行列错位。排查步骤:

  1. 检查原始文件是否有合并单元格
  2. 检查是否有隐藏的行或列
  3. 检查表格是否跨页
  4. 检查是否有嵌套表格

对于跨页表格,我的处理方式是先合并再解析。用pdfplumber的extract_tables()时,设置page参数为多页,它会自动处理跨页表格。

8. 清洗后的数据如何入RAG知识库

8.1 分块策略:按语义分块而不是按字数

清洗完的文本不能直接整篇入库,需要分块。很多人按固定字数分块(比如每500字一块),这在探矿文档上效果很差,因为一个钻孔的描述可能刚好被切到两块里。

我的分块策略是按语义单元分块:

  • TXT钻孔记录:每个钻孔一个块
  • Word章节:每个三级标题下的内容一个块
  • PDF表格:每个表格一个块,表格的标题和表头作为metadata
  • 网页正文:按段落分块,但保持段落完整性

如果语义单元太长(超过1000字),再按句子边界切分。如果太短(少于50字),和相邻块合并。

8.2 元数据设计:检索精度的隐形推手

元数据在RAG检索中的作用经常被低估。我设计的元数据字段包括:

{ "source_file": "ZK001钻孔记录.txt", "file_type": "txt", "hole_id": "ZK001", "date": "2023-05-12", "depth_range": "12-45m", "content_type": "drill_log", "hierarchy": "钻探工程 > 钻孔记录 > ZK001" }

这些元数据在检索时可以做预过滤。比如用户搜“ZK001的铜品位”,可以先按hole_id=ZK001过滤,再在过滤结果里做向量检索,精度和速度都会大幅提升。

8.3 向量化模型的选择:中文探矿领域要选对

向量化模型的选择直接影响检索效果。我实测下来,在中文探矿领域,BGE-large-zh和M3E-base的表现比较均衡。如果追求更高精度,可以用BGE-M3,它支持多语言和长文本,但计算成本更高。

注意:不要用OpenAI的text-embedding模型处理中文探矿文本,它在中文专业术语上的表现明显不如国产模型。这不是崇洋媚外,是实测结果。

9. 我在实际项目中的几点体会

清洗这活儿,说起来都是细节,但真正决定成败的往往不是技术方案,而是对业务的理解。我一开始用通用的文档清洗流程去处理探矿资料,效果很一般。后来跟着地质队的人跑了几天现场,看了他们怎么记录、怎么查资料、怎么用检索结果,才明白哪些信息是关键的、哪些噪声是可以忽略的。

举个例子:探矿报告里的“岩芯采取率”这个指标,通用清洗流程可能会把它当成普通文本处理。但实际上,这个指标在检索时经常需要做数值比较(比如“采取率大于80%的钻孔”),所以我在清洗时会把数值和单位分开存储,数值字段用float类型,这样后面做范围检索就方便很多。

另一个体会是:清洗规则要可配置、可迭代。我一开始把规则写死在代码里,后来业务方说“这个字段也要提取”“那个噪声也要去掉”,每次都要改代码重新部署。后来我把规则抽成YAML配置文件,业务方自己就能改,效率高了很多。

最后分享一个小技巧:建立清洗质量的自动化评估机制。我写了一个脚本,随机抽取清洗后的数据,用正则检查是否有残留的乱码、是否有明显的格式错误、是否有空字段。每次清洗流程跑完自动运行,发现问题及时告警。这个机制帮我省了很多人工检查的时间。

这个内容后续还可以这样扩展:把清洗后的结构化数据入图数据库,构建探矿知识图谱,支持更复杂的关联检索。比如“查找所有与ZK001钻孔相邻且铜品位大于0.5%的钻孔”,这种查询用向量检索很难做好,但用图数据库就很自然。

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

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

立即咨询