1. 引言
在之前的几篇文章中,我们已经完成了项目创建、向量库模型下载以及向量库部署等准备工作。从本篇开始,我们将正式进入 RAG(检索增强生成)项目的核心业务开发。
本文作为实战记录的第 06 篇,聚焦于 RAG 链路中的数据入口——文件上传与文档数据解析。我们将实现一个支持md/txt/pdf/doc/docx五种格式的文件上传接口,将上传的文档解析为纯文本,并按需切分为文本块,为后续写入向量库做好准备。
说明:本文暂不涉及向量化、向量集合创建及入库部分,这部分内容留待下一篇实战记录详细展开。
2. 环境准备:添加文档处理依赖
首先,我们需要在requirements.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 文件(当前版本虽未直接使用,但为后续扩展预留)。
添加完成后,在项目虚拟环境中执行安装命令:
pipinstall-rrequirements.txt3. 实现文档读取方法
接下来,我们在项目中添加文档读取的核心方法。该方法负责将不同格式的文档统一解析为纯文本字符串。
3.1 主读取方法__read_document_to_text
def__read_document_to_text(file_path:str)->str:""" 读取多种文档格式为文本字符串 :param file_path: 文档路径 :return: 拼接后的文本内容 :raises ValueError: 文件类型不支持时抛出 """ext=os.path.splitext(file_path)[1].lower()try:ifext=='.txt'orext=='.md':withopen(file_path,'r',encoding='utf-8')asf:returnf.read()elifext=='.pdf':withpymupdf.open(file_path)asdoc:text='\n'.join(page.get_text()forpageindoc)ifnottext.strip():raiseValueError('PDF 未提取到文字,可能是扫描件/图片型 PDF(暂不支持 OCR)')returntextelifext=='.docx':document=docx.Document(file_path)return'\n'.join(p.textforpindocument.paragraphs)elifext=='.doc':return__read_doc_to_text(file_path)else:raiseValueError(f"Unsupported file type:{ext}")exceptExceptionase:raiseValueError(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是二进制格式,无法直接解析,需要借助外部工具或系统能力:
def__read_doc_to_text(file_path:str)->str:""" 读取老版二进制 .doc 文本。 Linux 用 antiword / catdoc;Windows 用 Word COM(需安装 pywin32 且本机装有 MS Word)。 """importplatformifplatform.system()=='Windows':try:importwin32com.client word=win32com.client.Dispatch('Word.Application')exceptException:raiseValueError('Windows 下读取 .doc 需安装 pywin32(pip install pywin32)且本机装有 MS Word')word.Visible=Falsetry:document=word.Documents.Open(file_path,ReadOnly=True)text=document.Content.Text document.Close(False)returntextfinally:word.Quit()# Linux:优先 antiword,其次 catdocforcmdin('antiword','catdoc'):try:result=subprocess.run([cmd,file_path],capture_output=True,text=True)exceptFileNotFoundError:continueifresult.returncode==0:returnresult.stdoutraiseValueError('读取 .doc 失败:未找到 antiword/catdoc(Linux 请安装 antiword)')- Windows:通过 Word COM 组件(
pywin32)打开文档并读取内容。 - Linux:依次尝试
antiword和catdoc两个命令行工具;若均不可用则抛出异常。
4. 添加上传文件接口
现在,我们来实现文件上传的 HTTP 接口。该接口接收用户上传的文件,解析为文本后按需切分,为后续入库做准备。
fromuuidimportuuid4fromfastapiimportAPIRouterfromloguruimportloggerfromapp.configimportsettingfromapp.entity.vo.response_bodyimportRestRespfromfastapiimportFile,UploadFilefromlangchain.text_splitterimportRecursiveCharacterTextSplitterimportosimporttempfileimportsubprocessimportdocximportpymupdffromapp.service.systemimportmilvus_service router=APIRouter()@router.post("/upload")asyncdefupload_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()iffile_extensionnotinallowed_types:returnRestResp(message="不支持的文件类型",status=-1)try:# 创建临时文件保存上传内容withtempfile.NamedTemporaryFile(delete=False,suffix=file_extension)astemp_file:content=awaitfile.read()temp_file.write(content)temp_path=temp_file.name doc_str=__read_document_to_text(temp_path)ifnotdoc_strornotdoc_str.strip():returnRestResp(message='文档内容为空,无法写入知识库',status=-1)# 小于文本字数时无需切割iflen(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}forsplitinsplits],embedding_column_name='doc_str',save=True)returnRestResp(message=f'写入{len(splits)}份文档',status=0)exceptExceptionase:logger.error(f"文件处理失败:{str(e)}")returnRestResp(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 响应:
{"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 的完整实现,敬请期待。