【接上篇】多格式文档支持扩展方案(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

相关推荐
小趴菜克鲁里2 小时前
游戏Excel配置自动化导出二进制工具链并生成对应配置类详解
游戏·自动化·excel
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— Word 文档解析插件功能全景与适配价值
flutter·word·harmonyos
reasonsummer2 小时前
【办公类-109-10】20260228圆牌被子牌(接送卡&被子卡&床卡&入园卡_word编辑单面_添加有效期)
python·word
道纪书生2 小时前
解决报错:很抱歉,powerpoint/word/excel遇到错误,使其无法正常工作......
word·powerpoint·excel
南部余额2 小时前
Apache POI 从入门到实战:Excel 与 Word操作攻略
java·word·excel·poi
鸿乃江边鸟3 小时前
用 oh-my-opencode 写一个word转pdf skill
pdf·大模型·opencode
qq_546937271 天前
Word _ WPS 通用公文排版助手,支持标题、正文一键规范,发文机关、函线、装订线、公章、页码等常用部件一键解决
word·wps
软件资深者1 天前
2026 版初中几何辅助线教材 PDF|打印即提分,中考几何 “分水岭” 一键通关
学习·数学·pdf·教学·初中数学
Loo国昌1 天前
【AI应用开发实战】07_文档解析路由与质量评估:从传统PDF解析到Docling现代化方案
人工智能·后端·python·自然语言处理·pdf