地质RAG实战:TXT/Word/PDF/网页四类文档清洗全流水线

文章目录

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提取有三大顽疾,一个比一个气人:

  1. **多栏排版错乱:**传统工具暴力提取,左右双栏横向一混,上下文逻辑直接稀碎。相当于你开着两台收音机同时收两个台,还得靠大脑自动分离频道。
  2. **跨页长表断裂:**几百行的"钻孔坐标及品位成果表",被分页符拦腰砍断,续表没有表头。大模型看到第二页,根本分不清这一列是X坐标还是Y坐标,跟猜谜似的。
  3. **图文脱节:**勘探线剖面图、综合柱状图里全是关键空间构造证据,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![地质图件]({img_path})\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 三大工程挑战

探矿知识库要同步矿业权公示、政策法规,可网页天生自带"杂、乱、变"三件套:

  1. **重度视觉噪声:**导航栏、侧边栏广告、页脚版权、关联推荐、浮窗脚本,正文300字,周边噪声3万字。你把整页喂给大模型,它得先学会在一堆"热烈庆祝"里找到"矿业权挂牌公告"------这不是做RAG,这是考阅读理解。
  2. **动态渲染:**好多政务网站是JS渲染的,静态爬虫requests过去,人家连正文都懒得给你。
  3. **表格嵌套混乱:**矿权公示的"竞得人、坐标范围、出让收益"全是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

相关推荐
lie..1 小时前
30天从零开始学AI应用开发(Day 16):Embedding 是什么?用人话讲明白“向量检索”
人工智能·算法·embedding
倔强的石头1061 小时前
【Transformer】上下文长度扩展技术综述
人工智能·深度学习·transformer
Rocky Ding*1 小时前
MaskGIT技术深度解析:图像生成如何从逐Token排队走向掩码并行预测
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·maskgit
笨笨饿2 小时前
#141_ Codex 本地 AI 代理工具链工作流重构:Headroom、Scrapling、Archify 统一接入
开发语言·人工智能·stm32·单片机·嵌入式硬件·ui·重构
Rocky Ding*3 小时前
Muse技术深度解析:用掩码并行生成图像,速度、语义与编辑能力如何同时成立
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·muse
HIT_Weston3 小时前
15、【数学】【基础】欧拉公式是怎么来的:从级数、微分方程到旋转
人工智能·模型部署
数智前线4 小时前
AI Coding,正在把存储推向前台
人工智能
唠点键盘之外的9 小时前
15 微调 vs RAG:到底怎么选
人工智能·机器学习·面试·aigc
具身AGI10 小时前
人机协同预训练:三条路线,一条拉开差距
人工智能