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)}")
接口处理流程如下:
- 类型校验:检查文件扩展名是否在允许列表中。
- 临时保存:将上传内容写入临时文件,保留原扩展名。
- 文本解析 :调用
__read_document_to_text将文档转为纯文本。 - 空内容校验:若解析结果为空,直接返回错误。
- 文本切分 :当文本长度超过
chunk_size时,使用RecursiveCharacterTextSplitter按配置切分为多个文本块;否则整体作为一个块。 - 向量库写入 :调用
milvus_service确保集合存在,并将切分后的文本块批量写入向量库。
注意:本文暂不展开向量化、集合创建及入库的细节,上述代码中相关调用仅作占位,下一篇实战记录将详细讲解。
5. 使用 Postman 测试文件上传
接口开发完成后,我们使用 Postman 进行联调测试。这里以上传一份 .docx 文件为例,观察接口的返回结果。
5.1 配置请求
- 打开 Postman,新建一个
POST请求。 - 请求地址填写:
http://localhost:8000/upload(端口号以实际服务配置为准)。 - 在
Body选项卡中选择form-data。 - 添加一个键值对:
Key填file,Type选择File,然后在Value处选择本地的.docx文件。 - 点击
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 的完整实现,敬请期待。