PDF解析实现

作者:马里马里奥-日期:2026/7/12

1. 项目背景与需求分析

在现代Web应用中,PDF 文档处理是一个常见但复杂的需求。无论是企业 OA 系统、在线教育平台还是知识管理工具,都需要能够高效解析 PDF 内容。本次作业要求实现一个中间件,专门处理前端上传的PDF文件,具体要求如下:

  1. 输入格式:接收 Base64 编码的 PDF 数据
  2. 核心功能:将 Base64 转换为正常 PDF 文件并提取文本内容
  3. 输出要求:将提取的文本内容完整放入 system_message 中,作为后续对话的上下文
  4. 扩展目标:最终版本应支持上传或发送包含完整路径信息的任何文件(包括 URL 地址),都能进行合理的解析和处理

2. 技术栈选择

2.1 后端框架

  • FastAPI:现代、高性能的 Python Web 框架,支持异步处理
  • Pydantic:数据验证和设置管理

2.2 PDF处理库

  • PyPDF2 / pdfplumber:PDF文本提取
  • pdf2image:PDF转图像(可选,用于OCR场景)
  • base64:标准库,用于Base64编解码

2.3 文件处理

  • aiofiles:异步文件操作
  • httpx:异步 HTTP 客户端(用于 URL 下载)

2.4 环境配置

1# requirements.txt
2fastapi==0.104.1
3uvicorn==0.24.0
4pydantic==2.5.0
5PyPDF2==3.0.1
6pdfplumber==0.10.3
7aiofiles==23.2.1
8httpx==0.25.1
9python-multipart==0.0.6
10

3. 核心中间件实现

3.1 基础模型定义

1from pydantic import BaseModel, Field
2from typing import Optional, Union
3from enum import Enum
4
5class FileSourceType(str, Enum):
6    BASE64 = "base64"
7    URL = "url"
8    LOCAL_PATH = "local_path"
9    UPLOAD = "upload"
10
11class PDFParseRequest(BaseModel):
12    """PDF 解析请求模型"""
13    source_type: FileSourceType = Field(..., description="文件来源类型")
14    content: Optional[str] = Field(None, description="Base64 内容或 URL 路径")
15    file_path: Optional[str] = Field(None, description="本地文件路径")
16    file_upload: Optional[bytes] = Field(None, description="上传的文件二进制数据")
17    include_metadata: bool = Field(True, description="是否包含 PDF 元数据")
18    language: str = Field("chi_sim", description="OCR 语言设置(如需要)")
19
20class PDFParseResponse(BaseModel):
21    """PDF 解析响应模型"""
22    success: bool
23    text_content: str
24    page_count: int
25    metadata: Optional[dict] = None
26    error_message: Optional[str] = None
27

3.2 中间件核心类

1import base64
2import tempfile
3import os
4from pathlib import Path
5import pdfplumber
6import httpx
7from typing import Tuple, Optional
8import logging
9
10logger = logging.getLogger(__name__)
11
12class PDFParserMiddleware:
13    """PDF解析中间件"""
14    
15    def __init__(self, temp_dir: str = None):
16        """
17        初始化PDF解析中间件
18        
19        Args:
20            temp_dir: 临时文件目录,默认为系统临时目录
21        """
22        self.temp_dir = temp_dir or tempfile.gettempdir()
23        Path(self.temp_dir).mkdir(parents=True, exist_ok=True)
24    
25    async def parse_pdf(self, request: PDFParseRequest) -> PDFParseResponse:
26        """
27        解析PDF文件的主入口方法
28        
29        Args:
30            request: PDF 解析请求
31            
32        Returns:
33            PDFParseResponse: 解析结果
34        """
35        try:
36            # 根据来源类型获取PDF文件路径
37            pdf_path = await self._get_pdf_file(request)
38            
39            # 提取文本内容
40            text_content, page_count, metadata = self._extract_text_from_pdf(pdf_path)
41            
42            # 清理临时文件
43            self._cleanup_temp_file(pdf_path, request.source_type)
44            
45            return PDFParseResponse(
46                success=True,
47                text_content=text_content,
48                page_count=page_count,
49                metadata=metadata
50            )
51            
52        except Exception as e:
53            logger.error(f"PDF 解析失败: {str(e)}", exc_info=True)
54            return PDFParseResponse(
55                success=False,
56                text_content="",
57                page_count=0,
58                error_message=str(e)
59            )
60    
61    async def _get_pdf_file(self, request: PDFParseRequest) -> str:
62        """
63        根据来源类型获取PDF文件路径
64        
65        Returns:
66            str: 本地PDF文件路径
67        """
68        if request.source_type == FileSourceType.BASE64:
69            return await self._save_base64_to_file(request.content)
70        elif request.source_type == FileSourceType.URL:
71            return await self._download_from_url(request.content)
72        elif request.source_type == FileSourceType.LOCAL_PATH:
73            return request.file_path
74        elif request.source_type == FileSourceType.UPLOAD:
75            return await self._save_upload_to_file(request.file_upload)
76        else:
77            raise ValueError(f"不支持的来源类型: {request.source_type}")
78    
79    async def _save_base64_to_file(self, base64_content: str) -> str:
80        """将Base64内容保存为临时PDF文件"""
81        if not base64_content:
82            raise ValueError("Base64内容不能为空")
83        
84        # 移除可能的data URL前缀
85        if "," in base64_content:
86            base64_content = base64_content.split(",")[1]
87        
88        # 解码Base64
89        pdf_bytes = base64.b64decode(base64_content)
90        
91        # 保存到临时文件
92        temp_file = tempfile.NamedTemporaryFile(
93            suffix=".pdf",
94            dir=self.temp_dir,
95            delete=False
96        )
97        temp_file.write(pdf_bytes)
98        temp_file.close()
99        
100        return temp_file.name
101    
102    async def _download_from_url(self, url: str) -> str:
103        """从URL下载PDF文件"""
104        if not url:
105            raise ValueError("URL不能为空")
106        
107        async with httpx.AsyncClient() as client:
108            response = await client.get(url)
109            response.raise_for_status()
110            
111            # 保存到临时文件
112            temp_file = tempfile.NamedTemporaryFile(
113                suffix=".pdf",
114                dir=self.temp_dir,
115                delete=False
116            )
117            temp_file.write(response.content)
118            temp_file.close()
119            
120            return temp_file.name
121    
122    async def _save_upload_to_file(self, file_data: bytes) -> str:
123        """保存上传的文件数据到临时文件"""
124        if not file_data:
125            raise ValueError("上传文件数据不能为空")
126        
127        temp_file = tempfile.NamedTemporaryFile(
128            suffix=".pdf",
129            dir=self.temp_dir,
130            delete=False
131        )
132        temp_file.write(file_data)
133        temp_file.close()
134        
135        return temp_file.name
136    
137    def _extract_text_from_pdf(self, pdf_path: str) -> Tuple[str, int, dict]:
138        """
139        从PDF文件中提取文本内容
140        
141        Returns:
142            Tuple[str, int, dict]: (文本内容, 页数, 元数据)
143        """
144        text_parts = []
145        metadata = {}
146        page_count = 0
147        
148        try:
149            with pdfplumber.open(pdf_path) as pdf:
150                page_count = len(pdf.pages)
151                metadata = pdf.metadata
152                
153                for page_num, page in enumerate(pdf.pages, 1):
154                    page_text = page.extract_text()
155                    if page_text:
156                        text_parts.append(f"=== {page_num} ===\n{page_text}\n")
157                    else:
158                        # 如果无法提取文本,可能是扫描版PDF
159                        text_parts.append(f"=== {page_num} ===\n[扫描图像,需要OCR处理]\n")
160        
161        except Exception as e:
162            logger.warning(f"pdfplumber 提取失败,尝试 PyPDF2: {str(e)}")
163            # 回退到PyPDF2
164            import PyPDF2
165            with open(pdf_path, 'rb') as file:
166                pdf_reader = PyPDF2.PdfReader(file)
167                page_count = len(pdf_reader.pages)
168                metadata = pdf_reader.metadata
169                
170                for page_num, page in enumerate(pdf_reader.pages, 1):
171                    page_text = page.extract_text()
172                    if page_text:
173                        text_parts.append(f"=== {page_num} ===\n{page_text}\n")
174        
175        full_text = "\n".join(text_parts)
176        return full_text, page_count, metadata
177    
178    def _cleanup_temp_file(self, file_path: str, source_type: FileSourceType):
179        """清理临时文件(除本地路径外)"""
180        if source_type != FileSourceType.LOCAL_PATH:
181            try:
182                os.unlink(file_path)
183            except:
184                pass
185

3.3 FastAPI路由集成

1from fastapi import FastAPI, UploadFile, File, HTTPException
2from fastapi.middleware.cors import CORSMiddleware
3from contextlib import asynccontextmanager
4import uvicorn
5
6# 创建应用实例
7app = FastAPI(title="PDF 解析中间件 API")
8
9# 添加CORS中间件
10app.add_middleware(
11    CORSMiddleware,
12    allow_origins=["*"],
13    allow_credentials=True,
14    allow_methods=["*"],
15    allow_headers=["*"],
16)
17
18# 全局PDF解析器实例
19pdf_parser = PDFParserMiddleware()
20
21@asynccontextmanager
22async def lifespan(app: FastAPI):
23    """应用生命周期管理"""
24    # 启动时初始化
25    print("PDF解析中间件服务启动...")
26    yield
27    # 关闭时清理
28    print("PDF解析中间件服务关闭...")
29
30app = FastAPI(lifespan=lifespan)
31
32@app.post("/api/parse/pdf", response_model=PDFParseResponse)
33async def parse_pdf(request: PDFParseRequest):
34    """
35    解析 PDF 文件接口
36    
37    - 支持 Base64、URL、本地路径、文件上传多种方式
38    - 返回提取的文本内容和元数据
39    """
40    return await pdf_parser.parse_pdf(request)
41
42@app.post("/api/parse/upload", response_model=PDFParseResponse)
43async def parse_upload_pdf(
44    file: UploadFile = File(..., description="上传的PDF文件"),
45    include_metadata: bool = True
46):
47    """
48    通过文件上传方式解析 PDF
49    
50    - 支持 multipart/form-data 上传
51    - 自动检测文件类型
52    """
53    if not file.filename.lower().endswith('.pdf'):
54        raise HTTPException(400, "只支持PDF文件")
55    
56    content = await file.read()
57    
58    request = PDFParseRequest(
59        source_type=FileSourceType.UPLOAD,
60        file_upload=content,
61        include_metadata=include_metadata
62    )
63    
64    return await pdf_parser.parse_pdf(request)
65
66@app.get("/api/health")
67async def health_check():
68    """健康检查接口"""
69    return {"status": "healthy", "service": "pdf-parser-middleware"}
70

4. 系统集成与上下文管理

4.1 集成到对话系统

1class ConversationSystem:
2    """对话系统集成示例"""
3    
4    def __init__(self):
5        self.pdf_parser = PDFParserMiddleware()
6        self.system_messages = {}
7    
8    async def process_user_query(self, user_id: str, query: str, pdf_request: PDFParseRequest = None):
9        """
10        处理用户查询,支持PDF上下文
11        
12        Args:
13            user_id: 用户ID
14            query: 用户查询文本
15            pdf_request: PDF解析请求(可选)
16        """
17        system_message = ""
18        
19        # 如果有PDF解析请求,先解析PDF
20        if pdf_request:
21            pdf_result = await self.pdf_parser.parse_pdf(pdf_request)
22            
23            if pdf_result.success:
24                # 将PDF内容作为系统消息上下文
25                system_message = self._format_pdf_context(pdf_result)
26                self.system_messages[user_id] = system_message
27            else:
28                return {
29                    "error": f"PDF解析失败: {pdf_result.error_message}",
30                    "query": query
31                }
32        
33        # 如果有历史系统消息,合并
34        if user_id in self.system_messages:
35            system_message = self.system_messages[user_id]
36        
37        # 构建完整的对话上下文
38        full_context = f"""
39系统上下文(来自PDF文档):
40{system_message}
41
42用户查询:{query}
43
44请基于以上上下文回答用户问题。
45"""
46        
47        # 这里可以调用LLM接口
48        response = await self._call_llm_api(full_context)
49        
50        return {
51            "response": response,
52            "has_pdf_context": bool(system_message)
53        }
54    
55    def _format_pdf_context(self, pdf_result: PDFParseResponse) -> str:
56        """格式化PDF内容为系统消息"""
57        metadata_str = ""
58        if pdf_result.metadata:
59            metadata_items = []
60            for key, value in pdf_result.metadata.items():
61                if value:
62                    metadata_items.append(f"{key}: {value}")
63            if metadata_items:
64                metadata_str = f"\n文档元数据:\n" + "\n".join(metadata_items)
65        
66        return f"""
67PDF 文档内容(共 {pdf_result.page_count} 页):
68{pdf_result.text_content}
69{metadata_str}
70"""
71    
72    async def _call_llm_api(self, context: str):
73        """调用LLM API(示例)"""
74        # 这里可以集成OpenAI、文心一言、通义千问等
75        # 返回LLM的响应
76        return "这是基于PDF上下文的回答示例"
77

4.2 使用示例

1import asyncio
2import base64
3
4async def demo_usage():
5    """使用示例"""
6    parser = PDFParserMiddleware()
7    
8    # 示例1:Base64 方式
9    with open("sample.pdf", "rb") as f:
10        base64_content = base64.b64encode(f.read()).decode()
11    
12    request1 = PDFParseRequest(
13        source_type=FileSourceType.BASE64,
14        content=base64_content
15    )
16    
17    result1 = await parser.parse_pdf(request1)
18    print(f"Base64 解析结果: {result1.success}, 页数: {result1.page_count}")
19    
20    # 示例2:URL 方式
21    request2 = PDFParseRequest(
22        source_type=FileSourceType.URL,
23        content="https://example.com/document.pdf"
24    )
25    
26    # 示例3:集成到对话系统
27    conv_system = ConversationSystem()
28    response = await conv_system.process_user_query(
29        user_id="user123",
30        query="请总结文档的主要内容",
31        pdf_request=request1
32    )
33    print(f"对话响应: {response}")
34
35if __name__ == "__main__":
36    asyncio.run(demo_usage())
37

5. 高级功能扩展

5.1 OCR 支持(处理扫描版 PDF)

1import pytesseract
2from pdf2image import convert_from_path
3from PIL import Image
4
5class EnhancedPDFParser(PDFParserMiddleware):
6    """增强版 PDF 解析器,支持 OCR"""
7    
8    def __init__(self, temp_dir: str = None, tesseract_path: str = None):
9        super().__init__(temp_dir)
10        if tesseract_path:
11            pytesseract.pytesseract.tesseract_cmd = tesseract_path
12    
13    def _extract_text_with_ocr(self, pdf_path: str, language: str = "chi_sim") -> str:
14        """使用 OCR 提取扫描版 PDF 文本"""
15        text_parts = []
16        
17        #  PDF 转换为图像
18        images = convert_from_path(pdf_path)
19        
20        for page_num, image in enumerate(images, 1):
21            # 使用 Tesseract 进行 OCR
22            page_text = pytesseract.image_to_string(image, lang=language)
23            
24            if page_text.strip():
25                text_parts.append(f"=== {page_num}页(OCR识别) ===\n{page_text}\n")
26            else:
27                text_parts.append(f"=== {page_num} ===\n[OCR未识别到文字]\n")
28        
29        return "\n".join(text_parts)
30    
31    def _extract_text_from_pdf(self, pdf_path: str, language: str = "chi_sim") -> Tuple[str, int, dict]:
32        """重写提取方法,增加OCR支持"""
33        try:
34            # 先尝试普通文本提取
35            text, page_count, metadata = super()._extract_text_from_pdf(pdf_path)
36            
37            # 检查文本提取是否成功
38            if text and "需要OCR处理" not in text:
39                return text, page_count, metadata
40            
41            # 如果普通提取失败或需要OCR,使用OCR
42            ocr_text = self._extract_text_with_ocr(pdf_path, language)
43            return ocr_text, page_count, metadata
44            
45        except Exception as e:
46            logger.error(f"PDF文本提取失败: {str(e)}")
47            raise
48

5.2 批量处理与进度跟踪

1from typing import List
2from concurrent.futures import ThreadPoolExecutor
3import asyncio
4
5class BatchPDFProcessor:
6    """批量 PDF 处理器"""
7    
8    def __init__(self, max_workers: int = 4):
9        self.parser = PDFParserMiddleware()
10        self.executor = ThreadPoolExecutor(max_workers=max_workers)
11    
12    async def process_batch(self, requests: List[PDFParseRequest]) -> List[PDFParseResponse]:
13        """批量处理 PDF 文件"""
14        tasks = []
15        for request in requests:
16            task = asyncio.create_task(self._process_single(request))
17            tasks.append(task)
18        
19        results = await asyncio.gather(*tasks, return_exceptions=True)
20        
21        # 处理异常结果
22        processed_results = []
23        for result in results:
24            if isinstance(result, Exception):
25                processed_results.append(PDFParseResponse(
26                    success=False,
27                    text_content="",
28                    page_count=0,
29                    error_message=str(r
30

PDF解析实现》 是转载文章,点击查看原文


相关推荐


【Agent 学习日记】从问题到答案:RAG系统完整处理流程与核心机制深度拆解
小假是真的2026/7/4

目录 🍬前言 🍬一、 RAG 系统全流程总览(宏观视角) 🍬二、 离线预处理:决定 RAG 效果的上限 🍬三、 在线推理第一步:问题是如何被“拆解”的?(Query 拆解) 🍬四、 向量检索与重排序:从“大海捞针”到“精准定位” 🍬五、 大模型(LLM)在 RAG 中到底负责什么? 🍬六、 最终输出:不仅是“答案” 🍬七、 关键痛点与优化方向(总结展望) 🍬八、面试回答 🍡RAG完整处理流程 🍡问题如何拆解? 🍡大模型负责什么? 🍡最终输出什么


GitHub 热榜项目 - 周榜(2026-06-21)
CoderJia_2026/6/26

GitHub 热榜项目 - 周榜(2026-06-21) 生成于:2026-06-21 统计摘要 共发现热门项目: 21 个 Token赞助:siliconflow 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 本期热点趋势总结 本期 GitHub 热榜呈现出明显的 AI 工程化 与基础设施化 趋势:MCP 服务、Agent 技能、提示词安全扫描、RAG 压缩、代码知识图谱等项目集中爆发,说明开发重点已


《PyTorch 深度修炼》Dataset 和 DataLoader:数据如何喂给模型
闵孚龙2026/6/17

一、模型吃的不是文件,是 Batch Tensor 很多人刚学 PyTorch,会把数据加载理解成“读文件”。这个理解太浅。 训练模型时,真正进入模型的不是图片路径,不是 JSON,不是数据库记录,而是整理好的 Batch Tensor。 Dataset 负责回答一个问题:一个样本怎么取。DataLoader 负责回答另一个问题:怎样高效、稳定、成批地把样本送到训练循环。 所以 DataLoader 不是一个普通 for 循环。它是一条数据流水线。它管顺序、管批次、管拼接、管多进程、管预


Java Spring Data JPA 实战指南:Repository 查询、分页与实体映射
唐青枫2026/6/10

简介 Spring Data JPA 是 Spring Data 家族里专门用来简化 JPA 开发的模块。 它不是一个新的 ORM 规范。 更准确地说: JPA 是规范 Hibernate 是常见实现 Spring Data JPA 是 Spring 对 JPA Repository 的封装 在 Spring Boot 项目里,常见调用链大致是: Controller | v Service | v Repository | v Spring Data JPA |


阿里云ECS部署YOLO教程
MR_Colorful2026/6/2

1、阿里云注册 在官网注册账号:阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台 2、ECS配置选择 3、在阿里云 Workbench里为Ubuntu 18/20/22/24安装XFCE桌面(不推荐在这个里面使用,不好用!) stesteps1、通过VNC连接实例 step2、更新软件包列表和已安装的包 sudo apt update && sudo apt upgrade -y step3、安装XFCE桌面环境 sudo apt install -y xfce4 xfc


HarmonyOS 鸿蒙PC平台三方库移植:使用 vcpkg 移植 libzen(ZenLib)
展菲2026/5/25

网罗开发 (小红书、快手、视频号同名)   大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。 图书作者:《ESP32-C3 物联网工程开发实战》 图书作者:《SwiftUI 入门,进阶与实战》 超级个体:COC上海社区主理人 特约讲师:大学讲师,谷歌亚马逊分享嘉宾 科技


决定命运的,从来不是市场,而是你看待市场的方式
怕浪猫2026/5/4

真正拉开差距的,从来不是信息,而是认知。 在金融市场中,绝大多数人输掉的,不是技术,而是思维方式。 真正站在顶层的投资者,往往遵循一些看似简单、却极难长期执行的原则。这些原则,构成了投资世界的底层逻辑。 以下 9 种投资思维,不仅被反复验证,更塑造了历史上最成功的一批投资者。 一、长期主义:真正的财富,是"熬"出来的 "如果你不打算持有一只股票10年,那就不要持有10分钟。" —— Warren Buffett 真实故事:可口可乐的"慢性暴利" 1988年,巴菲特开始大量买入 Coca-


S10-蓝桥杯 17822 乐乐的积木塔
郑恩赐2026/4/25

S10-蓝桥杯 17822 乐乐的积木塔 摘要:本文详细解析蓝桥杯17822题"乐乐的积木塔",重点剖析题目中"连续递减积木塔"等绕口表述的真实含义,分享个人从困惑到理解的解题历程,并提供简洁的Python题解。 1. 题目描述 📋 1.1 问题描述 在乐乐的玩具箱中,有一系列的积木,每个积木上都标记有一个高度值。乐乐想要找出每个积木能够成为最高的连续递减积木塔的一部分的最大高度。对于每个积木,求出包含该积木的最长递减积木序列的长度。 1.2 输入格式 第一行包含一个整数 NNN。 第二行


GitHub 热榜项目 - 日榜(2026-04-14)
CoderJia_2026/4/16

GitHub 热榜项目 - 日榜(2026-04-14) 生成于:2026-04-14 统计摘要 共发现热门项目: 16 个 榜单类型:日榜 Token赞助:siliconflow 本期热点趋势总结 本期热榜彰显了 AI Agent 深度渗透垂直场景的技术趋势。Claude 生态及其相关的代码辅助开发工具(如 Claude Code 高级优化技巧、记忆插件及自动开发框架)成为绝对核心,反映出开发者对提高 LLM 编码确定性和确定化工作流的强烈需求。同时,金融领域的 Found


《从同步到消息驱动:现代后端交互模式的深度解析与工程实践》
铭渊老黄2026/4/8

《从同步到消息驱动:现代后端交互模式的深度解析与工程实践》 ——以百万行报表导出为例,谈用户体验、可观测性、失败处理与成本权衡 在过去十多年里,我见证了 Python 从“小巧优雅的脚本语言”成长为支撑全球互联网、数据科学、AI 产业的核心力量。无论是 Web 服务、自动化任务、数据处理,还是如今的 LLM 应用,Python 都以其灵活、可读、生态丰富的特性成为开发者的首选。 而在所有后端系统中,一个绕不开的问题是: “系统应该如何与用户交互?” 是同步返回?异步任务?还是彻底消息驱动? 这篇

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读