文章目录
-
- 前言
- 一、TXT:地质队员的野外狂草
-
- [1.1 先看看原生态长什么样](#1.1 先看看原生态长什么样)
- [1.2 三招搞定](#1.2 三招搞定)
- [1.3 代码伺候](#1.3 代码伺候)
- 二、Word:设计书里的牛皮癣广告
-
- [2.1 痛点](#2.1 痛点)
- [2.2 清洗前后](#2.2 清洗前后)
- [2.3 三个关键操作](#2.3 三个关键操作)
- [2.4 代码](#2.4 代码)
- 三、PDF:历史报告的三座大山
-
- [3.1 痛点不是一般的多](#3.1 痛点不是一般的多)
- [3.2 工业界的解法:三阶段异步并发](#3.2 工业界的解法:三阶段异步并发)
- [3.3 生产级代码](#3.3 生产级代码)
- 四、网页:广告比正文还抢戏
-
- [4.1 三大工程挑战](#4.1 三大工程挑战)
- [4.2 标准流水线](#4.2 标准流水线)
- [4.3 代码](#4.3 代码)
- 写在最后:给大模型请个营养师
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看, 传送门https://blog.csdn.net/qq_74013365
前言
搞RAG的人都知道一句话:垃圾进,垃圾出。
翻译成人话就是:你给大模型喂什么,它就给你吐什么。你喂它糊墙的泥巴,它就能给你糊出一堵会一本正经胡说八道的墙。
我这两年天天跟探矿业务的数据死磕,算是把这句话彻底吃透了。探矿数据有多野?野外原始文本、项目设计书、历史普查报告、政务网页,四路神仙各带各的格式,谁都不服谁。你要敢把这一坨直接丢进向量库,检索召回率能低到让你怀疑人生------大模型还能顺着你给的乱码,给你编出一座根本不存在的矿山,连带坐标都给你配齐。
所以今天必须聊聊:TXT、Word、PDF、网页这四种格式,到底怎么洗,才能让RAG从"人工智障"变回"人工智能"。
一、TXT:地质队员的野外狂草
1.1 先看看原生态长什么样
地质队员在野外录钻孔编录,别以为是坐在工位上慢慢敲键盘。人家是一手扶着岩芯箱,一手戳着手持终端的屏幕,还得随时防着蚊子和毒蛇。出来的东西能有多工整?你自己品:
ZK-001 0.0-15.5m:灰岩,局部见#@!方解石脉. 15.5-30.2m:矽卡岩化大理岩,伴有黄铁矿化,具染 染状结构...
编码可能是GBK也可能是UTF-8,打开就是一片乱码;硬回车把"浸染状"劈成"染 染状",专业术语被拦腰截断;中间还混着#@!这种野外噪声符号。不知道的还以为这是摩斯密码,知道的也得拿放大镜看半天。
清洗之后再看:
[钻孔编号: ZK-001] [深度区间: 0.0-15.5m] 岩性描述: 灰岩,局部见方解石脉。 [钻孔编号: ZK-001] [深度区间: 15.5-30.2m] 岩性描述: 矽卡岩化大理岩,伴有黄铁矿化,具浸染状结构。
是不是瞬间从"工地废纸"变成了"正经档案"?区别就是洗没洗。
1.2 三招搞定
- **第一招,自动识别编码。**chardet这个库就是给文件做DNA鉴定的,管你是GBK还是UTF-8,先验明正身再解码,乱码直接原地消失。
- **第二招,修复硬回车。**用中文捕获正则,看到两个汉字中间夹着换行符,直接给它焊上。地质术语被拦腰截断这种事,必须零容忍。
- **第三招,富语义注入。**把干巴巴的文本变成带标签的结构化格式,向量检索和长文本理解都会默默感谢你。
1.3 代码伺候
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:设计书里的牛皮癣广告
2.1 痛点
公司内部的《某省某县铜多金属矿详查项目设计书》、专家评审意见,你以为里面全是干货?天真。
每页顶着一个"内部资料---保密"页眉,每页坠着一个页码,50页文档你光看页眉页脚就能看一整天。最要命的是表格------大模型直接读未序列化的表格,行列关系错位得离谱。你问它"K-01块段的铜品位是多少",它能一本正经地告诉你"120.5"------那是矿石量啊兄弟,单位都是万吨,差着十万八千里。
2.2 清洗前后
清洗前(原生态):
内部资料 --- 某省地质局XX队保密
第一章 矿产资源储量估算
内部资料 --- 某省地质局XX队保密
1.1 块段储量统计表
块段编号 矿石类型 矿石量(万t) 铜平均品位(%)
K-01 矽卡岩型 120.5 0.85
K-02 斑岩型 340.2 0.42
(第2页,共50页)
清洗后(结构化Markdown):
# 第一章 矿产资源储量估算
## 1.1 块段储量统计表
| 块段编号 | 矿石类型 | 矿石量(万t) | 铜平均品位(%) |
| :--- | :--- | :--- | :--- |
| K-01 | 矽卡岩型 | 120.5 | 0.85 |
| K-02 | 斑岩型 | 340.2 | 0.42 |
2.3 三个关键操作
- **样式提取:**用python-docx读段落内置样式,Heading 1、Heading 2直接映射成Markdown标题。层级锚点有了,后面分块切分才不会切错地方。
- **版面噪声剥离:**写个关键字拦截,见到"保密""内部资料""第X页"直接毙掉,页眉页脚这些牛皮癣一秒钟清干净。
- **表格矩阵化:**遍历原生表格转成Markdown表格。行列对齐了,大模型再也不会把"矿石量"当"品位"回答。
2.4 代码
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) + " |")
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:历史报告的三座大山
3.1 痛点不是一般的多
扫描版的历史地质普查报告、储量核实报告,是知识库的核心资产,但PDF提取有三大顽疾,一个比一个气人:
- **多栏排版错乱:**传统工具暴力提取,左右双栏横向一混,上下文逻辑直接稀碎。相当于你开着两台收音机同时收两个台,还得靠大脑自动分离频道。
- **跨页长表断裂:**几百行的"钻孔坐标及品位成果表",被分页符拦腰砍断,续表没有表头。大模型看到第二页,根本分不清这一列是X坐标还是Y坐标,跟猜谜似的。
- **图文脱节:**勘探线剖面图、综合柱状图里全是关键空间构造证据,RAG看不懂像素,全靠VLM转写。转写结果放不对地方,正文里的"如图3-1所示"就变成了"图呢?图在哪?"
3.2 工业界的解法:三阶段异步并发
核心矛盾是速度差:纯文本解析是毫秒级,VLM看图说话是秒级。你要同步串行,等于闪电侠带着树懒秘书跑接力赛,急死个人。
- **阶段一:极速同步扫描。**按页扫,文本清洗、跨页表格用状态机拼接,图片抠出来存盘,并在文本流的精准位置植入唯一占位符。这一步就是拍电影先打板,画面和声音的对应关系先锁死。
- **阶段二:多线程并发VLM。**把所有图收集起来,丢进线程池并发请求多模态大模型,让Qwen-VL这些家伙专心看图说话。
- **阶段三:占位符精准回填。**VLM的摘要回到各自的位置,最后封装成带metadata的JSON。图文终于不脱节了,强迫症都治好了。
3.3 生产级代码
python
import fitz # PyMuPDF
import os
import base64
import re
from concurrent.futures import ThreadPoolExecutor, as_completed
from openai import OpenAI
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})
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_records.append({
"page_number": page_num,
"blocks": page_blocks,
"has_image": len(image_list) > 0,
"has_table": len(tables) > 0,
})
# ========== 阶段二:多线程并发 VLM ==========
vlm_results = {}
if all_vlm_tasks:
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"])
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():
structured_chunks.append({
"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,
"metadata": {
"has_image": record["has_image"],
"has_table": record["has_table"],
}
})
return structured_chunks
四、网页:广告比正文还抢戏
4.1 三大工程挑战
探矿知识库要同步矿业权公示、政策法规,可网页天生自带"杂、乱、变"三件套:
- **重度视觉噪声:**导航栏、侧边栏广告、页脚版权、关联推荐、浮窗脚本,正文300字,周边噪声3万字。你把整页喂给大模型,它得先学会在一堆"热烈庆祝"里找到"矿业权挂牌公告"------这不是做RAG,这是考阅读理解。
- **动态渲染:**好多政务网站是JS渲染的,静态爬虫requests过去,人家连正文都懒得给你。
- **表格嵌套混乱:**矿权公示的"竞得人、坐标范围、出让收益"全是HTML表格套div,标签属性乱成一锅粥,正文和表格极易错位。
4.2 标准流水线
智能抓取 → 正文提取(去噪) → 结构化Markdown → 向量化封装。
- **降噪优先:**decompose()暴力剥离导航、广告、脚本。确保进大模型的每一个token都是高价值行业情报,不是广告位招租信息。
- **格式统一:**markdownify把富文本转成Markdown,让网页数据和PDF、Word在知识库里讲同一种语言。
- **元数据对齐:**强制绑定source_url和source_type,大模型引用矿权公示时能精确溯源,数据真实性经得起查。
4.3 代码
python
import requests
from bs4 import BeautifulSoup
import markdownify
import re
def clean_web_page_pipeline(url):
"""网页清洗:去噪、正文提取、HTML转Markdown、元数据封装"""
try:
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')
# 剥离重度噪声:导航栏、页脚、广告、脚本、样式
for unwanted in soup(["script", "style", "nav", "header", "footer", "aside", ".ads", ".sidebar"]):
unwanted.decompose()
title_tag = soup.find('h1') or soup.find('title')
title = title_tag.get_text().strip() if title_tag else "无标题"
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
markdown_text = markdownify.markdownify(str(main_body), heading_style="ATX")
cleaned_markdown = re.sub(r'\n\s*\n', '\n\n', markdown_text).strip()
return {
"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",
}
}
except Exception as e:
return {"error": f"网页清洗异常: {str(e)}"}
写在最后:给大模型请个营养师
大模型和向量数据库的算法已经强得离谱,但真正决定系统成败的,往往不是模型本身,而是输入端的清洗质量。
地质报告、钻孔日志、矿业权公示,天生自带"多源、异构、长表断裂、图文脱节"的硬核基因。你无视清洗,把带视觉噪音、排版错乱、语义断裂的混沌数据直接喂进去,召回率低是必然的,图文指代错乱是必然的,幻觉频出也是必然的------这不是模型不行,是你喂的东西不行。
说白了,"Garbage In, Garbage Out"在RAG时代依然是铁律。数据清洗,就是把历史地质资产翻译成现代大模型能听懂的人话:版面重构加状态机长表拼接,保住地质数值和空间结构的完整;占位符加异步VLM,跨过视觉鸿沟;网页去噪加格式统一,让离线报告和在线资讯在知识库里说同一套语言。
你天天给大模型喂垃圾食品,还怪它满嘴跑火车------兄弟,锅不在模型那张嘴,在你喂食这只手上啊。把清洗流水线搞成工业级,召回精准、证据确凿、推理可靠,知识库才能真正变成赋能矿业生产的生产力。
不然你以为,"高精度检索"四个字,是白来的?
P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者,这个教程里内容讲解通俗易懂且风趣幽默,对我帮助很大。我想与大家分享这个宝藏教程,请点击下方链接查看,传送门https://blog.csdn.net/qq_74013365