AI智能体项目实战记录(06):RAG项目实战之文件上传及文档读取解析

1. 引言

在之前的几篇文章中,我们已经完成了项目创建、向量库模型下载以及向量库部署等准备工作。从本篇开始,我们将正式进入 RAG(检索增强生成)项目的核心业务开发。

本文作为实战记录的第 06 篇,聚焦于 RAG 链路中的数据入口 ------文件上传与文档数据解析。我们将实现一个支持 md/txt/pdf/doc/docx 五种格式的文件上传接口,将上传的文档解析为纯文本,并按需切分为文本块,为后续写入向量库做好准备。

说明:本文暂不涉及向量化、向量集合创建及入库部分,这部分内容留待下一篇实战记录详细展开。

2. 环境准备:添加文档处理依赖

首先,我们需要在 requirements.txt 中添加文档解析所需的第三方库。前面第一章已经安装过了依赖的话可以不用管,没有安装的话在文件末尾追加以下内容:

txt 复制代码
# ---- 文档处理 ----
pymupdf>=1.24.0,<2.0.0
python-docx==1.1.2
openpyxl==3.1.5

各依赖说明如下:

  • pymupdf:用于解析 PDF 文档,提取其中的文本内容。
  • python-docx :用于读取 .docx 格式的 Word 文档。
  • openpyxl:用于处理 Excel 文件(当前版本虽未直接使用,但为后续扩展预留)。

添加完成后,在项目虚拟环境中执行安装命令:

bash 复制代码
pip install -r requirements.txt

3. 实现文档读取方法

接下来,我们在项目中添加文档读取的核心方法。该方法负责将不同格式的文档统一解析为纯文本字符串。

3.1 主读取方法 __read_document_to_text

python 复制代码
def __read_document_to_text(file_path: str) -> str:
    """
    读取多种文档格式为文本字符串
    :param file_path: 文档路径
    :return: 拼接后的文本内容
    :raises ValueError: 文件类型不支持时抛出
    """
    ext = os.path.splitext(file_path)[1].lower()
    try:
        if ext == '.txt' or ext == '.md':
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        elif ext == '.pdf':
            with pymupdf.open(file_path) as doc:
                text = '\n'.join(page.get_text() for page in doc)
            if not text.strip():
                raise ValueError('PDF 未提取到文字,可能是扫描件/图片型 PDF(暂不支持 OCR)')
            return text
        elif ext == '.docx':
            document = docx.Document(file_path)
            return '\n'.join(p.text for p in document.paragraphs)
        elif ext == '.doc':
            return __read_doc_to_text(file_path)
        else:
            raise ValueError(f"Unsupported file type: {ext}")
    except Exception as e:
        raise ValueError(f"Failed to read {file_path}: {str(e)}")

该方法根据文件扩展名分发到不同的解析逻辑:

  • .txt / .md:直接以 UTF-8 编码读取文本。
  • .pdf :使用 pymupdf 逐页提取文本;若提取结果为空,则提示可能是扫描件/图片型 PDF(暂不支持 OCR)。
  • .docx :使用 python-docx 遍历段落对象,拼接文本。
  • .doc :调用辅助方法 __read_doc_to_text 处理老版二进制格式。

3.2 辅助方法 __read_doc_to_text

老版 .doc 是二进制格式,无法直接解析,需要借助外部工具或系统能力:

python 复制代码
def __read_doc_to_text(file_path: str) -> str:
    """
    读取老版二进制 .doc 文本。
    Linux 用 antiword / catdoc;Windows 用 Word COM(需安装 pywin32 且本机装有 MS Word)。
    """
    import platform
    if platform.system() == 'Windows':
        try:
            import win32com.client
            word = win32com.client.Dispatch('Word.Application')
        except Exception:
            raise ValueError('Windows 下读取 .doc 需安装 pywin32(pip install pywin32)且本机装有 MS Word')
        word.Visible = False
        try:
            document = word.Documents.Open(file_path, ReadOnly=True)
            text = document.Content.Text
            document.Close(False)
            return text
        finally:
            word.Quit()
    # Linux:优先 antiword,其次 catdoc
    for cmd in ('antiword', 'catdoc'):
        try:
            result = subprocess.run([cmd, file_path], capture_output=True, text=True)
        except FileNotFoundError:
            continue
        if result.returncode == 0:
            return result.stdout
    raise ValueError('读取 .doc 失败:未找到 antiword/catdoc(Linux 请安装 antiword)')
  • Windows :通过 Word COM 组件(pywin32)打开文档并读取内容。
  • Linux :依次尝试 antiword 和 catdoc 两个命令行工具;若均不可用则抛出异常。

4. 添加上传文件接口

现在,我们来实现文件上传的 HTTP 接口。该接口接收用户上传的文件,解析为文本后按需切分,为后续入库做准备。

python 复制代码
from uuid import uuid4

from fastapi import APIRouter
from loguru import logger

from app.config import setting
from app.entity.vo.response_body import RestResp
from fastapi import File, UploadFile
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os
import tempfile
import subprocess
import docx
import pymupdf

from app.service.system import milvus_service
router = APIRouter()

@router.post("/upload")
async def upload_file(file: UploadFile = File(...)):
    """
    文件上传接口,支持 md/txt/pdf/doc/docx 格式
    """
    # 检查文件类型
    allowed_types = ['.txt', '.pdf', '.md', '.doc', '.docx']
    file_extension = os.path.splitext(file.filename)[1].lower()

    if file_extension not in allowed_types:
        return RestResp(message="不支持的文件类型", status=-1)

    try:
        # 创建临时文件保存上传内容
        with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as temp_file:
            content = await file.read()
            temp_file.write(content)
            temp_path = temp_file.name

        doc_str = __read_document_to_text(temp_path)

        if not doc_str or not doc_str.strip():
            return RestResp(message='文档内容为空,无法写入知识库', status=-1)

        # 小于文本字数时无需切割
        if len(doc_str) < setting.chunk_size:
            splits = [doc_str]
        else:
            # 创建文本切割器
            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=setting.chunk_size,
                chunk_overlap=setting.chunk_overlap
            )
            splits = text_splitter.split_text(doc_str)

        # 确保文档集合存在且 schema 正确(doc_str 为文本字段、vector 为向量字段)
        # milvus_service.ensure_doc_collection(setting.milvus_collection_doc)

        # 写入向量库
        # milvus_service.batch_save_update(collection_name=setting.milvus_collection_doc,
                                           document_list=[{'id': str(uuid4()), 'name': file.filename, 'doc_str': split} for split in splits],
                                           embedding_column_name='doc_str',
                                         save=True)

        return RestResp(message=f'写入{len(splits)}份文档', status=0)
    except Exception as e:
        logger.error(f"文件处理失败: {str(e)}")
        return RestResp(message=f"文件处理失败: {str(e)}")

接口处理流程如下:

  1. 类型校验:检查文件扩展名是否在允许列表中。
  2. 临时保存:将上传内容写入临时文件,保留原扩展名。
  3. 文本解析 :调用 __read_document_to_text 将文档转为纯文本。
  4. 空内容校验:若解析结果为空,直接返回错误。
  5. 文本切分 :当文本长度超过 chunk_size 时,使用 RecursiveCharacterTextSplitter 按配置切分为多个文本块;否则整体作为一个块。
  6. 向量库写入 :调用 milvus_service 确保集合存在,并将切分后的文本块批量写入向量库。

注意:本文暂不展开向量化、集合创建及入库的细节,上述代码中相关调用仅作占位,下一篇实战记录将详细讲解。

5. 使用 Postman 测试文件上传

接口开发完成后,我们使用 Postman 进行联调测试。这里以上传一份 .docx 文件为例,观察接口的返回结果。

5.1 配置请求

  1. 打开 Postman,新建一个 POST 请求。
  2. 请求地址填写:http://localhost:8000/upload(端口号以实际服务配置为准)。
  3. 在 Body 选项卡中选择 form-data。
  4. 添加一个键值对:Key 填 file,Type 选择 File,然后在 Value 处选择本地的 .docx 文件。
  5. 点击 Send 发送请求。

5.2 观察返回结果

请求成功后,接口会返回类似如下的 JSON 响应:

json 复制代码
{
    "message": "写入3份文档",
    "status": 0
}

其中 message 中的数字表示本次上传的文档被切分成了多少个文本块。若文档内容较短(小于 chunk_size),则返回 写入1份文档。

5.3 异常情况验证

  • 上传不支持的格式 (如 .exe):返回 {"message": "不支持的文件类型", "status": -1}。
  • 上传空文档 :返回 {"message": "文档内容为空,无法写入知识库", "status": -1}。
  • 上传扫描版 PDF :返回 {"message": "PDF 未提取到文字,可能是扫描件/图片型 PDF(暂不支持 OCR)", "status": -1}。

6. 总结与下一步

本文完成了 RAG 项目的数据入口部分:

  • 添加了文档处理相关依赖;
  • 实现了多格式文档读取方法(md/txt/pdf/doc/docx);
  • 实现了文件上传接口,包含类型校验、文本解析与切分逻辑;
  • 使用 Postman 验证了 .docx 文件的上传流程。

下一篇实战记录将聚焦于向量化与向量库入库,包括向量集合的创建、文本向量化以及批量写入 Milvus 的完整实现,敬请期待。

相关推荐
什么嘻嘻果冻1 小时前
无代码开发平台的技术架构,恐象 AI 的生成引擎拆解
大数据·人工智能·低代码
解决问题no解决代码问题1 小时前
Java金融后端转LLM应用开发 Day1:初识生成式AI与大模型本质
人工智能
自养Agent1 小时前
Agent Harness 从用到会造:Skill 不是一次 API 调用,是一段会复用的行为
人工智能·llm
stereohomology1 小时前
我对deepseek4.1flash, glm5.3及flash能力的感受与基准测试相同
人工智能·llm·测评分
谁在黄金彼岸1 小时前
2026年第42周GitHub趋势周报
人工智能
预知同行1 小时前
今日AI大事件 | 2026.10.19:Meta开源816B巨兽、ChatGPT接管Chrome、Claude破解NIST 25年悬案
人工智能
2601_965969061 小时前
不会复杂公式,也能用AI整理表格和做基础数据分析吗?
人工智能
七夜zippoe1 小时前
Spring AI 是什么?Java 工程师的 AI 开发第一站
java·人工智能·spring·ai·开发
xier_ran1 小时前
【infra之路】AI 编译器学习笔记(十四):为什么要 tiling,以及矩阵计算单元的“固定“
人工智能·笔记·学习