在构建探矿垂域 RAG(检索增强生成)系统时,"垃圾进,垃圾出" 的规律尤为明显。由于探矿业务数据具有专业性强、格式杂乱、多源异构的特点(涵盖野外原始文本、项目设计书、历史普查报告及政务网页),直接索引会导致大模型检索召回率低、甚至产生严重幻觉。
本文将以探矿业务场景为例,深度拆解 TXT、Word、PDF、网页四种核心格式的清洗痛点、对比效果及工程实现方案。
一、 TXT 格式:钻孔编录原始文本清洗
1. 业务场景与痛点
地质队员在野外作业时,通常通过手持野外数据采集终端(PDAS)或简易文本编辑器快速录入钻孔岩芯编录原始文本。这类文件往往存在编码混乱、换行符错乱(硬回车)以及夹杂野外噪声符号的问题。
2. 清洗前后对比
- 清洗前(原始乱序文本) :
text
ZK-001 0.0-15.5m:灰岩,局部见#@!方解石脉.
15.5-30.2m:矽卡岩化大理岩,伴有黄铁矿化,具染
染状结构...
- 清洗后(规范化结构文本) :
text
[钻孔编号: ZK-001] [深度区间: 0.0-15.5m] 岩性描述: 灰岩,局部见方解石脉。
[钻孔编号: ZK-001] [深度区间: 15.5-30.2m] 岩性描述: 矽卡岩化大理岩,伴有黄铁矿化,具浸染状结构。
3. 清洗程序与解析
代码解析:
- 自动编码识别 :通过
chardet动态识别野外设备可能采用的 GBK 或 UTF-8 编码,避免乱码。 - 硬回车修复 :通过中文捕获正则
([\u4e00-\u9fa5])\r?\n([\u4e00-\u9fa5])修复被拦腰截断的地质专业术语。 - 富语义注入:将非结构化文本显式转化为带标签的格式,便于向量检索和模型长文本理解。
python
import re
import chardet
def clean_drill_log_text(file_path):
# 1. 自动检测文件编码并读取内容
with open(file_path, 'rb') as f:
raw_data = f.read()
encoding = chardet.detect(raw_data)['encoding'] or 'utf-8'
text = raw_data.decode(encoding, errors='ignore')
# 2. 基础字符规范化与去噪
text = text.replace('\u3000', ' ').replace('\t', ' ')
text = re.sub(r'[#@!~`]+', '', text)
# 3. 修复硬回车造成的断行
text = re.sub(r'([\u4e00-\u9fa5])\r?\n([\u4e00-\u9fa5])', r'\1\2', text)
# 4. 按行处理并结构化标准化
cleaned_lines = []
for line in text.splitlines():
line = line.strip()
if not line:
continue
match = re.search(r'([A-Za-z0-9\-_]+)\s+(\d+(?:\.\d+)?-\d+(?:\.\d+)?m)[::](.*)', line)
if match:
borehole_id, depth_range, description = match.groups()
description = description.replace('染染状', '浸染状')
structured_line = f"[钻孔编号: {borehole_id}] [深度区间: {depth_range}] 岩性描述: {description}"
cleaned_lines.append(structured_line)
else:
if len(line) > 2:
cleaned_lines.append(f"[通用记录] {line}")
return "\n".join(cleaned_lines)
二、 Word 格式:探矿项目设计书与评审意见清洗
1. 业务场景与痛点
公司内部编写的《某省某县铜多金属矿详查项目设计书》或专家评审意见。这类文档往往带有大量的样式、页眉页脚、目录和未结构化的表格。
- 痛点:每页重复的保密页眉、页码噪声、以及大模型在直接读取未序列化表格时容易发生错位的行列关系。
2. 清洗前后对比
- 清洗前(原始 Word 提取文本) :
text
内部资料 --- 某省地质局XX队保密
第一章 矿产资源储量估算
内部资料 --- 某省地质局XX队保密
1.1 块段储量统计表
块段编号 矿石类型 矿石量(万t) 铜平均品位(%)
K-01 矽卡岩型 120.5 0.85
K-02 斑岩型 340.2 0.42
(第2页,共50页)
- 清洗后(规范化 Markdown 文本) :
text
# 第一章 矿产资源储量估算
## 1.1 块段储量统计表
| 块段编号 | 矿石类型 | 矿石量(万t) | 铜平均品位(%) |
| :--- | :--- | :--- | :--- |
| K-01 | 矽卡岩型 | 120.5 | 0.85 |
| K-02 | 斑岩型 | 340.2 | 0.42 |
- 清洗程序与解析
代码解析:
- 样式提取 :利用
python-docx读取段落内置样式(如Heading 1、Heading 2),精准转换为 Markdown 标题层级,为后续的层级分块(Hierarchical Chunking)提供标准锚点。 - 版面噪声剥离:通过关键字拦截机制,自动清除跨页重复出现的"保密/内部资料"及页码干扰。
- 表格矩阵化:将 Word 的原生表格对象遍历并转换为结构化 Markdown 表格,确保大模型在检索如"K-01块段的铜品位是多少"时,能够准确对齐行列数据。
python
from docx import Document
def clean_word_document(file_path):
doc = Document(file_path)
cleaned_content = []
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if not text:
continue
# 过滤页眉页脚等样板噪音文字(如包含保密字样或页码标志)
if "内部资料" in text or "保密" in text or ("第" in text and "页" in text):
continue
# 根据 Word 样式映射为 Markdown 标题
style_name = paragraph.style.name
if style_name.startswith('Heading 1'):
cleaned_content.append(f"\n# {text}\n")
elif style_name.startswith('Heading 2'):
cleaned_content.append(f"\n## {text}\n")
else:
cleaned_content.append(text)
# 提取并转换表格
for table in doc.tables:
table_md = []
for row_idx, row in enumerate(table.rows):
row_data = [cell.text.strip().replace('\n', ' ') for cell in row.cells]
table_md.append("| " + " | ".join(row_data) + " |")
# 在首行(表头)下方插入 Markdown 分隔线
if row_idx == 0:
table_md.append("| " + " | ".join(["---"] * len(row.cells)) + " |")
if table_md:
cleaned_content.append("\n" + "\n".join(table_md) + "\n")
return "\n".join(cleaned_content)
三、 PDF 格式:历史地质普查报告与储量核实报告清洗
1. 业务场景与核心痛点
在探矿业务中,年代久远的扫描版历史地质报告与《矿产资源储量核实报告》是知识库的核心资产。然而,这类 PDF 文档通常排版极其复杂,直接提取会面临三大工程顽疾:
- 多栏排版错乱:传统工具采用暴力提取,常将左右双栏的正文横向混杂,彻底破坏地质上下文逻辑。
- 跨页长表格断裂:长达数百行的"钻孔坐标及品位成果表"常被分页符切断,续表丢失表头,导致大模型无法识别列数据含义。
- 视觉鸿沟与图文脱节:诸如"勘探线剖面图"、"综合柱状图"等图形包含关键的空间构造证据,但 RAG 无法直接"看懂"图像像素。必须借助 VLM 将其转写为文本摘要,若处理不当,正文中的"如图 3-1 所示"便与转写后的摘要彻底脱节,导致大模型在检索时上下文断裂。
2. 清洗前后对比(含长表拼接与图文原地对齐)
- 清洗前(PDF 原始粗暴提取:双栏混杂、长表断页、图文分离) :
text
--- 第 12 页 ---
图 3-1 某地 3 号勘探线剖面图 [二进制乱码流]
矿区位于区域性大断裂下盘,地层受韧性剪切带控制...
表 3-1 探矿工程成果表
| 钻孔编号 | X坐标 | Y坐标 | 铜品位(%) |
| ZK01 | 3421500.2 | 452100.5 | 0.76 |
[跨页断开,第 13 页顶部无表头]
| ZK02 | 3421501.4 | 452101.8 | 0.54 |
- 清洗后(版面重构、长表无缝拼接、图文原地对齐的规范 Markdown) :
text
矿区位于区域性大断裂下盘,地层受韧性剪切带控制...

> **[VLM 智能地质图文解析]**:该剖面图展示了 ZK01 与 ZK02 钻孔穿透 F1 断层的空间几何形态,矿体埋深 15.5m,倾向北东。
如上图所示,矿体在标高 +500m 处发生分支复合。
# 表 3-1 探矿工程成果表
| 钻孔编号 | X坐标 | Y坐标 | 铜品位(%) |
| :--- | :--- | :--- | :--- |
| ZK01 | 3421500.2 | 452100.5 | 0.76 |
| ZK02 | 3421501.4 | 452101.8 | 0.54 |
3. 现代化工程流水线设计:如何破解"文本快、图片慢"的矛盾?
在真实的工程落地中,纯文本和表格解析是毫秒级的(极快),而调用 VLM 大模型让其"看图说话"是秒级的(极慢) 。
为了在桥接"视觉鸿沟"的同时保证"图片转写的文字必须紧挨着正文上下文(防脱节)" ,工业界普遍采用"两阶段异步并发架构":
- 阶段一(极速同步扫描与占位锁定) :程序按页顺畅扫描,瞬间提取文本并完成跨页表格拼接;同时将图片抠出并存盘,在文本流的精准位置植入唯一占位符,彻底锁定图文的相对顺序。
- 阶段二(多线程异步并发 VLM) :将全书所有提取出的图片集中收集,扔进线程池中并行并发请求多模态大模型,将其"翻译"为地质摘要文本。
- 阶段三(占位符精准回填与 JSON 封装) :将 VLM 异步返回的文本摘要无缝回填到对应的占位符中,最终组装成携带丰富元数据的标准 JSON 结构化文档。
4. 生产级 Python 实现代码
该代码专为采矿行业地质普查报告、储量核实报告等复杂 PDF 文档设计,核心解决版面错乱、跨页长表断裂、以及 RAG 无法读懂图片而引发的图文脱节与性能瓶颈。
4.1整体架构:三阶段异步并发流水线
该代码专为采矿行业地质普查报告、储量核实报告等复杂 PDF 文档设计,核心解决版面错乱、跨页长表断裂、以及 RAG 无法读懂图片而引发的图文脱节与性能瓶颈。输入pdf文档后:
- 阶段一:极速同步扫描(CPU 本地运行)
- 过滤页眉页脚与版面噪声
- 状态机拼接跨页长表格
- 提取图片并植入唯一占位符(锁定图文上下文位置)
-
阶段二:多线程并发 VLM(网络 I/O 异步并发) 线程池并发调用多模态大模型(如 Qwen-VL),将图片"翻译"为地质文本摘要
-
阶段三:回填与结构化封装
- 将 VLM 文本摘要精准回填替换占位符
- 输出带丰富 metadata 的标准 JSON 结构化对象
4.2 核心功能模块介绍
-
VLM 视觉转写模块(
vlm_image_captioning)- 解决问题:攻克传统 RAG 读不懂图片像素的"视觉鸿沟"。
- 实现方式:将抠出的地质图件(剖面图、柱状图)转为 Base64 编码,调用多模态大模型(如 Qwen-VL-Max)。
- 专业提示词 :引导大模型从专业地质视角出发,提取图名、构造、矿体埋深及钻孔对应关系,输出高质量的地质文本摘要。
-
跨页长表格状态机处理器
- 解决问题:数百行的钻孔坐标与品位成果表常被 PDF 分页符拦腰截断、续表丢失表头的问题。
- 实现方式 :系统维护一个
last_table_headers状态变量。遇到表格时自动校验,若是断页续表则自动复用上一页表头进行无缝拼接,保障地质结构化数据的完整性。
-
异步并发调度器(
ThreadPoolExecutor)- 解决问题:平衡"毫秒级文本解析"与"秒级 VLM 推理"的速度差。
- 实现方式 :利用 Python 线程池(
max_workers=5)将图片任务并发丢给大模型,彻底突破单线程网络 I/O 的性能瓶颈。
python
import fitz # PyMuPDF
import os
import base64
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from openai import OpenAI
# 初始化多模态大模型客户端(如 Qwen-VL 或 OpenAI GPT-4o 兼容接口)
vlm_client = OpenAI(api_key="your-api-key", base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
def vlm_image_captioning(image_path):
"""独立封装的 VLM 请求函数,将图片转写为文本摘要,支持线程池并发调用"""
try:
with open(image_path, "rb") as f:
b64_img = base64.b64encode(f.read()).decode('utf-8')
resp = vlm_client.chat.completions.create(
model="qwen-vl-max",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "这是一张地质报告中的剖面图或柱状图。请专业分析:提取图名,并总结其核心构造、矿体埋深、钻孔对应关系。"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_img}"}}
]
}],
max_tokens=400
)
return resp.choices[0].message.content
except Exception as e:
return f"[VLM 解析异常: {str(e)}]"
def clean_pdf_production_pipeline(file_path, output_dir="pdf_assets"):
os.makedirs(output_dir, exist_ok=True)
doc = fitz.open(file_path)
all_vlm_tasks = []
page_raw_records = []
last_table_headers = None
# ====================================================
# 阶段一:极速同步扫描(文本清洗、表格跨页拼接、图像占位锁定)
# ====================================================
for page_idx, page in enumerate(doc):
page_num = page_idx + 1
page_blocks = []
# 1. 文本流清洗(过滤页眉、页脚及"第X页"干扰)
raw_text = page.get_text("text") or ""
cleaned_lines = [line.strip() for line in raw_text.split('\n')
if "内部参考" not in line and not re.search(r'第\s*\d+\s*页', line)]
if cleaned_lines:
page_blocks.append("\n".join(cleaned_lines))
# 2. 图像提取、过滤与占位符植入(跨越视觉鸿沟,防图文脱节)
image_list = page.get_images(full=True)
for img_idx, img in enumerate(image_list):
base_img = doc.extract_image(img[0])
# 过滤小图标、徽标等视觉噪声
if base_img["width"] < 150 or base_img["height"] < 150:
continue
img_filename = f"p{page_num}_img_{img_idx+1}.{base_img['ext']}"
img_path = os.path.join(output_dir, img_filename)
with open(img_path, "wb") as f:
f.write(base_img["image"])
# 生成唯一占位符标识,锁定图文上下文位置
placeholder_id = f"__VLM_PLACEHOLDER_{page_num}_{img_idx+1}__"
all_vlm_tasks.append({
"placeholder_id": placeholder_id,
"image_path": img_path
})
# 在当前页的准确正文缝隙中插入带有占位符的 Markdown 结构
image_placeholder_md = f"\n\n> **[VLM 智能地质图文解析]**:\n> {{{placeholder_id}}}\n"
page_blocks.append(image_placeholder_md)
# 3. 跨页长表格状态机处理
tables = page.extract_tables()
for table in tables:
if not table or len(table) < 1:
continue
# 判断是否为上一页长表格的断页续表
is_continuation = False
if last_table_headers and len(table[0]) == len(last_table_headers):
if not any(kw in "".join([str(c) for c in table[0]]) for kw in ["编号", "坐标", "品位"]):
is_continuation = True
table_md = []
if is_continuation:
start_idx = 0 # 续表直接复用上一页表头
else:
headers = [str(c).strip().replace('\n', ' ') for c in table[0]]
last_table_headers = headers
table_md.append("| " + " | ".join(headers) + " |")
table_md.append("| " + " | ".join(["---"] * len(headers)) + " |")
start_idx = 1
for row in table[start_idx:]:
clean_row = [str(c).strip().replace('\n', ' ') if c else "" for c in row]
table_md.append("| " + " | ".join(clean_row) + " |")
if table_md:
page_blocks.append("\n" + "\n".join(table_md) + "\n")
page_raw_record = {
"page_number": page_num,
"blocks": page_blocks,
"has_image": len(image_list) > 0,
"has_table": len(tables) > 0
}
page_raw_records.append(page_raw_record)
# ====================================================
# 阶段二:多线程并发调用 VLM(突破大模型推理速度瓶颈)
# ====================================================
vlm_results = {}
if all_vlm_tasks:
print(f"正在并发处理共计 {len(all_vlm_tasks)} 张地质图件的 VLM 视觉解析...")
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_task = {
executor.submit(vlm_image_captioning, task["image_path"]): task["placeholder_id"]
for task in all_vlm_tasks
}
for future in as_completed(future_to_task):
p_id = future_to_task[future]
try:
vlm_results[p_id] = future.result()
except Exception as exc:
vlm_results[p_id] = f"[VLM 执行异常: {exc}]"
# ====================================================
# 阶段三:回填占位符并封装为标准的最终 JSON 结构
# ====================================================
structured_chunks = []
for record in page_raw_records:
full_page_text = "\n".join(record["blocks"])
# 将文本流中的占位符精准替换为真实的 VLM 文本摘要结果
for p_id, summary_text in vlm_results.items():
full_page_text = full_page_text.replace(f"{{{p_id}}}", summary_text)
if full_page_text.strip():
json_chunk = {
"chunk_id": f"doc_{os.path.basename(file_path)}_p{record['page_number']}",
"source_file": file_path,
"page_number": record["page_number"],
"page_content": full_page_text, # 完美的、图文语义对齐的 Markdown 主体
"metadata": {
"has_image": record["has_image"],
"has_table": record["has_table"]
}
}
structured_chunks.append(json_chunk)
return structured_chunks
四、 网页(Web)格式:矿业权公示、政策法规与行业资讯清洗
1. 业务场景与核心痛点
探矿行业的 RAG 知识库需要频繁同步网页端的前沿资讯与官方公示。然而,网页数据天然具有"杂、乱、变"的特点,直接抓取会面临三大工程挑战:
- 重度视觉噪声(Boilerplate Noise) :网页普遍包含大量的导航栏、侧边栏广告、页脚版权、关联推荐及浮窗脚本。若直接送入大模型,会严重稀释核心知识的密度。
- 动态渲染与异步加载:许多政务或矿权交易网站采用复杂的 JavaScript 渲染或翻页机制,传统的静态爬虫(如 Requests)无法获取完整内容。
- 表格与结构嵌套混乱:矿权公示中的"竞得人、坐标范围、出让收益"等多以 HTML 表格或嵌套 div 形式存在,标签属性凌乱,极易导致正文与表格错位。
2. 清洗前后对比
- 清洗前(原始 HTML:充斥导航、广告及复杂标签) :
html
<html>
<head><title>某省自然资源厅矿业权挂牌公告 - 2026版</title></head>
<body>
<div id="header">首页 | 政策法规 | 矿业权公示 | 登录 </div>
<div class="ads">热烈庆祝我省探矿权改革取得重大突破...</div>
<div class="main-content">
<h1>关于某县铜多金属矿探矿权挂牌出让的公告</h1>
<p>发布时间:2026-04-01 来源:矿业权交易中心</p>
<p>经省人民政府批准,现对某铜多金属矿进行公开挂牌出让。</p>
<table>
<tr><td>区块名称</td><td>勘查面积(km²)</td></tr>
<tr><td>某县铜矿普查</td><td>25.40</td></tr>
</table>
</div>
<div id="footer">版权所有 © 2026 矿业网 页面加载耗时 0.2s</div>
</body>
</html>
- 清洗后(去噪、转结构化 Markdown 的规范文本) :
text
# 关于某县铜多金属矿探矿权挂牌出让的公告
> **元数据**:[发布时间] 2026-04-01 | [来源] 矿业权交易中心 | [URL] https://example.com/mining/101
经省人民政府批准,现对某铜多金属矿进行公开挂牌出让。
| 区块名称 | 勘查面积(km²) |
| :--- | :--- |
| 某县铜矿普查 | 25.40 |
3. 现代化网页清洗工程流水线
针对网页数据的异构性与高噪声,工业界通常构建一套"智能抓取 ==> 正文提取(去噪)==> 结构化 Markdown 转换 ==> 向量化封装"的流水线:
-
降噪优先 :通过
decompose()暴力剥离网页模板中的干扰元素(如导航、广告、脚本),确保送入大模型的每一个 Token 都是高价值的行业情报。 -
格式统一(HTML to Markdown) :利用
markdownify将网页中的富文本结构(标题、段落、表格)完美转化为 Markdown 格式,确保网页数据与前面处理的 PDF、Word 数据在 RAG 知识库中具有统一的语义结构和解析标准。 -
元数据对齐 :为网页内容强制绑定
source_url和source_type,当大模型检索到某条矿权公示时,能够精准溯源其官方网络出处,满足探矿业务对数据真实性与时效性的严苛要求。
python
import requests
from bs4 import BeautifulSoup
import markdownify
import re
def clean_web_page_pipeline(url):
"""
网页数据清洗生产级流水线:
实现去噪、正文提取、HTML转Markdown及元数据封装
"""
try:
# 1. 模拟请求(可按需升级为 Playwright 应对动态渲染网页)
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) MiningRAGBot/1.0'}
response = requests.get(url, headers=headers, timeout=10)
response.encoding = response.apparent_encoding
html_content = response.text
soup = BeautifulSoup(html_content, 'html.parser')
# 2. 剥离重度噪声:精准剔除导航栏、页脚、广告、脚本及样式标签
for unwanted in soup(["script", "style", "nav", "header", "footer", "aside", ".ads", ".sidebar"]):
unwanted.decompose()
# 3. 提取核心元数据(标题、发布时间等)
title_tag = soup.find('h1') or soup.find('title')
title = title_tag.get_text().strip() if title_tag else "无标题"
# 4. 寻找正文核心区(通过语义标签或常见正文 Class 缩小范围)
main_body = soup.find('article') or soup.find('div', class_=re.compile('content|article|main', re.I))
if not main_body:
main_body = soup.body # 若无明确标签,则退化为清洗后的 body
# 5. 将精简后的 HTML 转换为规范的 Markdown
markdown_text = markdownify.markdownify(str(main_body), heading_style="ATX")
# 6. 清理多余空行,保持文本整洁
cleaned_markdown = re.sub(r'\n\s*\n', '\n\n', markdown_text).strip()
# 7. 封装为标准的 RAG 结构化 JSON 文档
json_chunk = {
"chunk_id": f"web_{abs(hash(url))}",
"source_url": url,
"title": title,
"page_content": f"# {title}\n\n{cleaned_markdown}",
"metadata": {
"source_type": "web_announcement",
"domain": "mining_rights"
}
}
return json_chunk
except Exception as e:
return {"error": f"网页清洗异常: {str(e)}"}
结语:高质量的数据清洗------构建探矿行业大模型 RAG 的"基石"
大模型和向量数据库的算法本身已经足够强大,但真正决定系统成败的,往往不是模型本身,而是作为输入端源头的数据清洗质量。
在找矿勘探与储量核实的高精尖业务场景中,地质报告、钻孔日志、矿业权公示等数据天然具备"多源、异构、长表断裂、图文脱节"等极端复杂的基因。如果忽视清洗,盲目将带有视觉噪音、排版错乱和语义断裂的"混沌数据"直接喂给 RAG 系统,必然会导致检索召回率低下、图文指代错乱以及大模型幻觉频出。
可以说,严苛的数据清洗,是打通"历史地质资产"与"现代大模型智能"之间不可逾越的桥梁:
- 通过版面重构与状态机长表拼接,守护了地质数值与空间结构的绝对完整;
- 通过"占位符+异步 VLM"架构,跨越了视觉鸿沟,彻底根治了图文脱节的顽疾;
- 通过网页去噪与格式统一,确保了离线报告与在线资讯在知识库中拥有标准化的语义表达。
"Garbage In, Garbage Out(垃圾输入,垃圾输出)" 在 RAG 时代从未过时。通过构建工业级、多阶段的数据清洗流水线,将原始文档精细化转化为带丰富元数据的结构化 Markdown 与 JSON 资产,才能真正确保大模型在面对复杂的找矿空间推理时,做到"召回精准、证据确凿、推理可靠" ,让知识库真正成为赋能矿业生产与决策的强大生产力。