【接上篇】多格式文档支持扩展方案(PDF_Word_Excel)

本可直接替换原有的文档加载逻辑,无缝集成到之前的RAG系统中

一、核心原理

不同格式的文档本质都是"文本内容+格式标记",我们要做的就是:

  1. 剥离格式,提取纯文本:用PyPDF2解析PDF、python-docx解析Word、openpyxl解析Excel,去掉所有格式标记(字体、颜色、表格样式等),只保留可读的纯文本

  2. 统一格式入库:不管是PDF/Word/Excel,提取出的纯文本都按照"文件名+文本内容"的结构,批量导入到Elasticsearch知识库,后续的语义检索、大模型问答逻辑完全不变

  3. 离线适配:所有解析库都提前下载离线whl包,无外网也能安装,无任何外部依赖

二、第一步:离线准备多格式解析依赖包

2.1 有网环境下载离线whl包

Bash 复制代码
# 创建whl包存放目录
mkdir -p /opt/offline-packages/whl-doc/
# 下载所有多格式解析依赖包(指定版本,避免兼容性问题)
pip download PyPDF2==3.0.1 python-docx==1.1.0 openpyxl==3.1.2 -d /opt/offline-packages/whl-doc/

下载完成后,将/opt/offline-packages/whl-doc/目录下的所有whl文件,拷贝到离线环境的/opt/offline-packages/whl-doc/目录。

2.2 离线环境安装依赖包

Bash 复制代码
# 进入whl包目录
cd /opt/offline-packages/whl-doc/
# 离线安装所有依赖包
pip install --no-index --find-links=./ PyPDF2 python-docx openpyxl

2.3 验证安装成功

执行以下命令,无报错则说明安装成功:

Bash 复制代码
python3 -c "import PyPDF2, docx, openpyxl; print('所有依赖安装成功')"

三、第二步:多格式文档解析核心函数

3.1 核心解析函数

将以下函数添加到原有rag_offline.py脚本中,替换原来的load_documents_from_folder函数:

Python 复制代码
import PyPDF2
from docx import Document
import openpyxl

def extract_text_from_pdf(file_path):
    """离线解析PDF文件,提取纯文本"""
    try:
        with open(file_path, 'rb') as f:
            pdf_reader = PyPDF2.PdfReader(f)
            text = ""
            # 遍历所有页面,提取文本
            for page in pdf_reader.pages:
                page_text = page.extract_text()
                if page_text:
                    text += page_text + "\n"
        return text.strip()
    except Exception as e:
        print(f"⚠️ PDF文件 {file_path} 解析失败:{str(e)}")
        return ""

def extract_text_from_docx(file_path):
    """离线解析Word文件(.docx),提取纯文本"""
    try:
        doc = Document(file_path)
        text = ""
        # 提取段落文本
        for para in doc.paragraphs:
            if para.text.strip():
                text += para.text + "\n"
        # 提取表格文本(关键优化:Excel/Word表格转成可读文本)
        for table in doc.tables:
            for row in table.rows:
                row_text = "\t".join([cell.text.strip() for cell in row.cells])
                text += row_text + "\n"
        return text.strip()
    except Exception as e:
        print(f"⚠️ Word文件 {file_path} 解析失败:{str(e)}")
        return ""

def extract_text_from_excel(file_path):
    """离线解析Excel文件(.xlsx),提取纯文本"""
    try:
        wb = openpyxl.load_workbook(file_path, data_only=True)  # data_only=True读取单元格计算后的值
        text = ""
        # 遍历所有工作表
        for sheet_name in wb.sheetnames:
            sheet = wb[sheet_name]
            text += f"工作表名称:{sheet_name}\n"
            # 遍历所有行
            for row in sheet.iter_rows(values_only=True):
                # 过滤空单元格,拼接行文本
                row_text = "\t".join([str(cell) if cell is not None else "" for cell in row])
                if row_text.strip():
                    text += row_text + "\n"
        return text.strip()
    except Exception as e:
        print(f"⚠️ Excel文件 {file_path} 解析失败:{str(e)}")
        return ""

def extract_text_from_txt(file_path):
    """离线解析TXT文件,兼容中文编码"""
    try:
        # 优先UTF-8,失败则用GBK(兼容中文文档)
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read().strip()
    except UnicodeDecodeError:
        with open(file_path, 'r', encoding='gbk') as f:
            return f.read().strip()
    except Exception as e:
        print(f"⚠️ TXT文件 {file_path} 解析失败:{str(e)}")
        return ""

def load_documents_from_folder():
    """【多格式适配版】加载所有支持格式的文档,生成ES批量写入数据"""
    # 支持的文件格式
    SUPPORTED_FORMATS = {
        '.pdf': extract_text_from_pdf,
        '.docx': extract_text_from_docx,
        '.xlsx': extract_text_from_excel,
        '.txt': extract_text_from_txt
    }

    if not os.path.exists(DATASET_FOLDER):
        print(f"❌ 知识库目录 {DATASET_FOLDER} 不存在,请先创建目录并放入文档")
        exit(1)
    
    file_count = 0
    success_count = 0
    # 遍历目录下所有文件
    for filename in os.listdir(DATASET_FOLDER):
        # 获取文件后缀
        file_ext = os.path.splitext(filename)[1].lower()
        # 跳过不支持的格式
        if file_ext not in SUPPORTED_FORMATS:
            print(f"⚠️ 跳过不支持的文件格式:{filename}")
            continue
        
        file_path = os.path.join(DATASET_FOLDER, filename)
        # 根据格式调用对应的解析函数
        extract_func = SUPPORTED_FORMATS[file_ext]
        file_content = extract_func(file_path)
        
        # 跳过解析失败/空内容的文件
        if not file_content:
            print(f"⚠️ 文件 {filename} 解析后无内容,跳过导入")
            continue
        
        # 生成ES批量写入数据
        yield {
            "_index": INDEX_NAME,
            "_source": {
                "file_name": filename,
                "file_content": file_content,
                "file_format": file_ext[1:]  # 新增字段:记录文件格式
            }
        }
        file_count += 1
        success_count += 1
    
    if file_count == 0:
        print(f"❌ 知识库目录 {DATASET_FOLDER} 中没有找到支持的文档(PDF/Word/Excel/TXT)")
        exit(1)
    print(f"📄 共找到 {file_count} 个支持格式的文档,成功解析 {success_count} 个,准备导入ES")

3.2 关键优化点说明

  1. 编码兼容:TXT文件优先用UTF-8解析,失败则自动切换GBK,解决中文乱码问题

  2. 表格解析:Word/Excel表格转成"单元格内容\t单元格内容"的格式,保留表格的行列逻辑,大模型能识别表格内容

  3. 容错处理:单个文件解析失败不会中断整体流程,只会跳过该文件并打印警告

  4. 格式记录 :新增file_format字段,记录文件原始格式(pdf/docx/xlsx/txt),便于后续检索过滤

四、第三步:更新ES索引映射(适配新增字段)

修改原有脚本中的setup_knowledge_index函数,在mappings.properties中新增file_format字段,确保ES能正确存储文件格式信息:

Python 复制代码
def setup_knowledge_index():
    """创建知识库索引,适配多格式文档"""
    try:
        if es_client.indices.exists(index=INDEX_NAME):
            print(f"✅ 知识库索引 {INDEX_NAME} 已存在,无需重复创建")
            return False

        print(f"📦 正在创建知识库索引 {INDEX_NAME}...")
        es_client.indices.create(
            index=INDEX_NAME,
            body={
                "settings": {
                    "number_of_shards": 1,
                    "number_of_replicas": 0,
                    "refresh_interval": "1s"
                },
                "mappings": {
                    "properties": {
                        "file_name": {
                            "type": "text",
                            "copy_to": "semantic_content"
                        },
                        "file_content": {
                            "type": "text",
                            "copy_to": "semantic_content"
                        },
                        "file_format": {  # 新增:存储文件格式
                            "type": "keyword"  # keyword类型,支持精确过滤
                        },
                        "semantic_content": {
                            "type": "semantic_text",
                            "inference_id": "offline-e5-small-embedding"
                        }
                    }
                }
            }
        )
        print(f"✅ 知识库索引 {INDEX_NAME} 创建成功")
        return True
    except Exception as e:
        print(f"❌ 知识库索引创建失败,错误信息:{str(e)}")
        exit(1)

五、第四步:扩展语义检索(支持按文件格式过滤)

新增一个可选的检索过滤函数,支持只检索指定格式的文档(比如只查PDF、只查Excel),按需使用:

Python 复制代码
def semantic_search_with_filter(user_query, top_n=3, file_format=None):
    """语义检索,支持按文件格式过滤(可选)"""
    start_time = time.time()
    # 基础语义检索查询
    search_query = {
        "query": {
            "bool": {
                "must": [
                    {
                        "semantic": {
                            "field": "semantic_content",
                            "query": user_query
                        }
                    }
                ]
            }
        },
        "size": top_n,
        "_source": ["file_name", "file_content", "file_format"]
    }

    # 如果指定了文件格式,添加过滤条件
    if file_format:
        search_query["query"]["bool"]["filter"] = [
            {"term": {"file_format": file_format.lower()}}
        ]

    response = es_client.search(index=INDEX_NAME, body=search_query)
    search_latency = (time.time() - start_time) * 1000
    return response["hits"]["hits"], search_latency

使用示例(只检索PDF文档)

在脚本的主函数中,将原来的semantic_search调用替换为:

Python 复制代码
# 只检索PDF格式的文档
search_results, search_latency = semantic_search_with_filter(USER_QUESTION, top_n=3, file_format="pdf")

六、完整测试流程(离线环境)

6.1 准备测试文档

/opt/rag-offline/dataset目录放入:

  • 测试PDF文档(比如《API性能报告.pdf》)

  • 测试Word文档(比如《会议纪要.docx》)

  • 测试Excel文档(比如《项目进度表.xlsx》)

  • 测试TXT文档(比如《需求说明.txt》)

6.2 运行脚本

Bash 复制代码
cd /opt/rag-offline/
python3 rag_offline.py

6.3 预期输出

Plain 复制代码
📄 共找到 4 个支持格式的文档,成功解析 4 个,准备导入ES
✅ 成功导入 4 个文档到知识库
🔍 用户问题:请总结API存在的性能问题
✅ 找到 3 个相关文档,检索耗时:12ms
🤖 正在调用本地大模型 dolphin3.0-qwen2.5-0.5b-instruct.Q4_K_M.gguf 生成答案...

============================================================
💡 问题:请总结API存在的性能问题
📝 答案:
1. API在并发量超过1000次/分钟时,响应时间从200ms飙升至3秒(来源:[1] API性能报告.pdf)
2. 复杂查询无缓存层,导致重复计算,CPU占用率达到100%(来源:[2] 会议纪要.docx)
3. 数据库索引未优化,多条件过滤查询耗时超过2秒(来源:[3] 项目进度表.xlsx)

📚 引用来源:
  [1] API性能报告.pdf
  [2] 会议纪要.docx
  [3] 项目进度表.xlsx
============================================================
🔍 语义检索耗时:12ms
🤖 大模型生成耗时:15800ms | 生成速度:9.6 tokens/s
✅ 离线RAG问答执行完成

七、离线环境常见问题与解决方案

问题现象 根因分析 解决方案
PDF解析乱码/无内容 PDF是扫描件(图片),或加密PDF 1. 扫描件需先OCR转文本(离线OCR工具如Tesseract);2. 加密PDF需先解密
Word解析丢失表格内容 表格嵌套层级过深,或使用了旧版.doc格式 1. 转换为.docx格式;2. 脚本已适配基础表格解析,复杂表格需手动调整解析逻辑
Excel解析数字为None 单元格是公式,未勾选data_only=True 脚本中已设置data_only=True,会读取公式计算后的值,确保Excel保存时已计算所有公式
中文文档解析乱码 文档编码非UTF-8/GBK TXT文件用记事本另存为UTF-8;PDF/Word/Excel无需处理,脚本已适配
大文件解析内存溢出 单文件超过100MB,内存不足 新增分块解析逻辑,将大文件拆分成多个小文本块导入ES

八、进阶优化(可选)

8.1 大文件分块解析(解决内存溢出)

修改load_documents_from_folder函数,对超过指定大小的文件进行分块:

Python 复制代码
def split_large_text(text, max_chunk_size=5000):
    """将大文本分块,避免单条记录过大"""
    chunks = []
    start = 0
    while start < len(text):
        end = start + max_chunk_size
        # 按句子分割,避免截断语义
        if end < len(text):
            end = text.rfind('\n', start, end) + 1
            if end <= start:
                end = start + max_chunk_size
        chunks.append(text[start:end].strip())
        start = end
    return chunks

# 在load_documents_from_folder函数中,生成ES数据时添加分块逻辑
file_content = extract_func(file_path)
# 大文件分块
if len(file_content) > 5000:
    chunks = split_large_text(file_content)
    for i, chunk in enumerate(chunks):
        if chunk:
            yield {
                "_index": INDEX_NAME,
                "_source": {
                    "file_name": f"{filename}_chunk{i+1}",
                    "file_content": chunk,
                    "file_format": file_ext[1:],
                    "original_file": filename  # 记录原始文件名
                }
            }
            success_count += 1
else:
    # 小文件直接导入
    yield {
        "_index": INDEX_NAME,
        "_source": {
            "file_name": filename,
            "file_content": file_content,
            "file_format": file_ext[1:]
        }
    }
    success_count += 1

8.2 离线OCR支持(解析扫描件PDF)

  1. 有网环境下载Tesseract离线安装包和中文语言包

  2. 离线安装Tesseract,安装pytesseract离线whl包

  3. 扩展PDF解析函数,扫描件PDF先OCR转文本再导入:

Python 复制代码
import pytesseract
from PIL import Image

def extract_text_from_scanned_pdf(file_path):
    """离线OCR解析扫描件PDF"""
    try:
        # 需先安装poppler-utils(离线包),用pdf2image转PDF为图片
        from pdf2image import convert_from_path
        images = convert_from_path(file_path)
        text = ""
        for img in images:
            text += pytesseract.image_to_string(img, lang='chi_sim') + "\n"
        return text.strip()
    except Exception as e:
        print(f"⚠️ 扫描件PDF {file_path} OCR解析失败:{str(e)}")
        return ""

总结

  1. 核心能力:通过PyPDF2/python-docx/openpyxl实现PDF/Word/Excel/TXT多格式文档离线解析,提取纯文本后统一导入ES,RAG问答逻辑完全复用

  2. 离线适配:所有依赖包均提供离线安装方式,无外网也能部署

  3. 容错处理:单个文件解析失败不影响整体流程,支持中文编码、表格解析、大文件分块

  4. 扩展能力:支持按文件格式过滤检索,可扩展离线OCR解析扫描件PDF

相关推荐
远洪16 小时前
excel 找出两列不同的数据
excel
pcplayer17 小时前
非常好用的 Excel 读写控件
excel·delphi·office
Navicat中国20 小时前
使用 Navicat 导入向导导入 Excel 数据时,系统提示导入成功,表中也能看到数据,但行数统计显示为 0,这是什么原因?
数据库·excel·导入
穿着内裤的外星人1 天前
触控精灵远程读写Excel步骤配置
excel
jiangbqing1 天前
职场动物进化手册(升级版).pdf 免费分享
pdf·职场动物净化·职场必读潜规则
合合技术团队1 天前
智能合同审查搭建教程:低质量PDF怎么处理?先解析清洗,再分路审阅(附GitHub项目地址)
pdf·prompt·github·textin
dbkx_291 天前
Word域操作记录(从2开始的公式编号排版)
word
南风微微吹1 天前
【管综】考研199管理类综合联考历年真题及答案解析PDF电子版(2009-2026年)
考研·pdf
优化控制仿真模型1 天前
【英一】考研英语一历年真题及答案解析PDF电子版(1980-2026年)
经验分享·pdf
其实秋天的枫1 天前
【英一】考研英语一历年真题及答案解析PDF电子版(1980-2026年)
经验分享·pdf