FastAPI+Celery+Redis搭建企业级PDF翻译异步任务系统

前言

之前给一家跨境电商公司做技术咨询时,他们的CTO提到一个具体场景:每天有2000-5000份产品说明书、合同、营销材料需要翻译,从手动复制粘贴到集成开源OCR,中间踩了很多坑。最典型的痛点是------同步阻塞:用户上传一个200页的产品手册后,API调用超时,但任务其实在后台跑着;用户重复点击"翻译",导致同一个文件被处理多次。

本文分享一套完整的FastAPI + Celery + Redis异步任务系统架构,从API网关到任务调度,从状态查询到结果下载,全部开源、可复用。处理一份100页的PDF,平均响应时间从原来的"30秒+超时"优化到"3秒返回任务ID,后台异步完成,微信通知用户下载"。

系统架构

复制代码
                        ┌─────────────────┐
                        │   用户/前端      │
                        └────────┬────────┘
                                 │ HTTPS
                                 ▼
┌──────────────────────────────────────────────────────┐
│                   FastAPI 网关层                       │
│  POST /translate       创建翻译任务,返回任务ID        │
│  GET  /tasks/{id}      查询任务状态和进度             │
│  GET  /download/{id}   下载翻译结果                  │
│  GET  /health          健康检查                      │
└────────┬─────────────────────────────────────────────┘
         │ 入队
         ▼
┌─────────────────┐    ┌─────────────────┐
│   Redis Broker  │ <─>│  Celery Worker  │
│   (消息队列)     │    │  (任务执行)      │
└─────────────────┘    └────────┬────────┘
                                │ 任务状态
                                ▼
                        ┌─────────────────┐
                        │  Redis Backend  │
                        │  (结果存储)      │
                        └─────────────────┘
                                │
                                ▼
                        ┌─────────────────┐
                        │  PDF翻译引擎     │
                        │  (PyMuPDF/LLM)  │
                        └─────────────────┘

环境准备

bash 复制代码
# Python 3.10+
pip install fastapi uvicorn celery redis pymupdf pdfplumber requests python-multipart

需要本地启动Redis服务(Docker方式):

bash 复制代码
docker run -d -p 6379:6379 --name pdf-redis redis:7-alpine

实现步骤

Step 1: 项目结构

复制代码
pdf-translate-service/
├── app/
│   ├── __init__.py
│   ├── main.py           # FastAPI入口
│   ├── tasks.py          # Celery任务定义
│   ├── translator.py     # 翻译核心逻辑
│   ├── models.py         # Pydantic数据模型
│   └── config.py         # 配置(Redis地址等)
├── celery_worker.py      # Celery worker启动入口
├── requirements.txt
└── README.md

Step 2: 配置和Celery实例

python 复制代码
# app/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    """应用配置"""
    redis_url: str = "redis://localhost:6379/0"
    celery_broker_url: str = "redis://localhost:6379/1"
    celery_result_backend: str = "redis://localhost:6379/2"
    upload_dir: str = "./uploads"
    output_dir: str = "./outputs"
    max_file_size: int = 20 * 1024 * 1024  # 20MB
    
    class Config:
        env_file = ".env"

settings = Settings()
python 复制代码
# app/tasks.py
from celery import Celery
from app.config import settings

# 初始化Celery实例
celery_app = Celery(
    "pdf_translate",
    broker=settings.celery_broker_url,
    backend=settings.celery_result_backend,
    include=["app.tasks"]
)

# Celery配置
celery_app.conf.update(
    task_serializer="json",
    accept_content=["json"],
    result_serializer="json",
    timezone="Asia/Shanghai",
    enable_utc=True,
    task_track_started=True,  # 追踪任务开始状态
    task_time_limit=600,      # 任务超时10分钟
    task_soft_time_limit=540, # 软超时9分钟
    worker_prefetch_multiplier=1,  # 避免大任务抢占所有worker
    worker_max_tasks_per_child=50, # 每个worker处理50个任务后重启,防内存泄漏
)

Step 3: 翻译核心逻辑

python 复制代码
# app/translator.py
import fitz
from pathlib import Path
from typing import Callable, Optional

class PDFTranslator:
    """PDF翻译核心类(支持进度回调)"""
    
    def __init__(self, translate_func: Callable[[str], str]):
        """初始化
        
        Args:
            translate_func: 文本翻译函数(接收原文,返回译文)
                            可以接入任何LLM或翻译API
        """
        self.translate_func = translate_func
    
    def translate_pdf(self, 
                      input_path: str, 
                      output_path: str,
                      progress_callback: Optional[Callable[[int, int], None]] = None) -> dict:
        """翻译PDF(支持进度回调)
        
        Args:
            input_path: 输入PDF路径
            output_path: 输出PDF路径
            progress_callback: 进度回调函数(current_page, total_pages)
            
        Returns:
            {
              "total_pages": int,
              "translated_pages": int,
              "elapsed_seconds": float
            }
        """
        import time
        start_time = time.time()
        
        src_doc = fitz.open(input_path)
        out_doc = fitz.open()
        
        total_pages = len(src_doc)
        translated_count = 0
        
        for page_num, page in enumerate(src_doc, start=1):
            # 提取原文文本
            original_text = page.get_text()
            
            # 调用翻译函数
            translated_text = self.translate_func(original_text)
            
            # 创建新页面,保留原始版式
            new_page = out_doc.new_page(
                width=page.rect.width,
                height=page.rect.height
            )
            
            # 插入翻译后文字
            text_rect = fitz.Rect(50, 50, page.rect.width - 50, page.rect.height - 50)
            new_page.insert_textbox(
                text_rect,
                translated_text,
                fontsize=10,
                fontname="helv"
            )
            
            # 复制原页面的图片、表格元素(简化处理)
            for img in page.get_images(full=True):
                xref = img[0]
                try:
                    new_page.insert_image(page.rect, xref=xref)
                except:
                    pass
            
            translated_count += 1
            
            # 回调进度
            if progress_callback:
                progress_callback(page_num, total_pages)
        
        # 保存输出PDF
        out_doc.save(output_path)
        out_doc.close()
        src_doc.close()
        
        return {
            "total_pages": total_pages,
            "translated_pages": translated_count,
            "elapsed_seconds": round(time.time() - start_time, 2)
        }

Step 4: Celery异步任务定义

python 复制代码
# app/tasks.py (续)
import os
from celery import shared_task
from pathlib import Path
from app.config import settings
from app.translator import PDFTranslator

# 全局翻译器实例
_translator = None

def get_translator():
    """获取翻译器实例(懒加载)"""
    global _translator
    if _translator is None:
        # 这里接入实际的翻译API(可以是Gemini/OpenAI/本地LLM)
        import openai
        
        def llm_translate(text: str) -> str:
            """调用LLM翻译文本"""
            if not text.strip():
                return text
            
            # 调用Gemini/OpenAI API(此处以OpenAI为例)
            response = openai.ChatCompletion.create(
                model="gpt-4",
                messages=[
                    {"role": "system", "content": "你是一个专业PDF翻译助手,保持术语准确和段落结构。"},
                    {"role": "user", "content": f"请将以下英文翻译成中文,保持段落结构:\n\n{text}"}
                ],
                temperature=0.3
            )
            return response.choices[0].message.content
        
        _translator = PDFTranslator(translate_func=llm_translate)
    
    return _translator


@shared_task(bind=True, name="translate_pdf_task")
def translate_pdf_task(self, input_path: str, output_filename: str) -> dict:
    """Celery异步任务:翻译PDF
    
    Args:
        input_path: 上传的PDF路径
        output_filename: 输出文件名
        
    Returns:
        任务结果字典
    """
    import shutil
    
    output_path = Path(settings.output_dir) / output_filename
    Path(settings.output_dir).mkdir(parents=True, exist_ok=True)
    
    # 进度回调:更新Celery task meta信息
    def update_progress(current: int, total: int):
        self.update_state(
            state="PROGRESS",
            meta={
                "current": current,
                "total": total,
                "percent": round(current / total * 100, 2)
            }
        )
    
    try:
        # 执行翻译
        translator = get_translator()
        result = translator.translate_pdf(
            input_path=input_path,
            output_path=str(output_path),
            progress_callback=update_progress
        )
        
        # 清理输入文件
        if Path(input_path).exists():
            Path(input_path).unlink()
        
        return {
            "status": "success",
            "output_path": str(output_path),
            "output_filename": output_filename,
            **result
        }
    
    except Exception as e:
        # 异常处理
        return {
            "status": "failed",
            "error": str(e),
            "input_path": input_path
        }

Step 5: FastAPI网关层

python 复制代码
# app/main.py
import uuid
from pathlib import Path
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
from pydantic import BaseModel

from app.config import settings
from app.tasks import translate_pdf_task, celery_app

app = FastAPI(
    title="PDF翻译API",
    description="基于FastAPI+Celery+Redis的异步PDF翻译服务",
    version="1.0.0"
)

# 创建上传目录
Path(settings.upload_dir).mkdir(parents=True, exist_ok=True)


class TaskResponse(BaseModel):
    """任务创建响应"""
    task_id: str
    filename: str
    status: str
    message: str


class TaskStatus(BaseModel):
    """任务状态响应"""
    task_id: str
    status: str  # PENDING / PROGRESS / SUCCESS / FAILURE
    progress: dict = {}
    result: dict = {}


@app.post("/translate", response_model=TaskResponse)
async def create_translation_task(file: UploadFile = File(...)):
    """创建PDF翻译任务"""
    
    # 文件类型校验
    if not file.filename.endswith(".pdf"):
        raise HTTPException(status_code=400, detail="只支持PDF文件")
    
    # 文件大小校验
    file_bytes = await file.read()
    if len(file_bytes) > settings.max_file_size:
        raise HTTPException(status_code=413, detail=f"文件超过{settings.max_file_size // 1024 // 1024}MB限制")
    
    # 保存到本地
    file_id = str(uuid.uuid4())
    input_path = Path(settings.upload_dir) / f"{file_id}.pdf"
    output_filename = f"{file_id}_translated.pdf"
    
    with open(input_path, "wb") as f:
        f.write(file_bytes)
    
    # 提交Celery异步任务
    task = translate_pdf_task.delay(str(input_path), output_filename)
    
    return TaskResponse(
        task_id=task.id,
        filename=file.filename,
        status="PENDING",
        message=f"任务已创建,使用 /tasks/{task.id} 查询状态"
    )


@app.get("/tasks/{task_id}", response_model=TaskStatus)
async def get_task_status(task_id: str):
    """查询任务状态"""
    
    task_result = celery_app.AsyncResult(task_id)
    
    response = TaskStatus(
        task_id=task_id,
        status=task_result.status,
        progress={},
        result={}
    )
    
    if task_result.status == "PROGRESS":
        response.progress = task_result.info or {}
    elif task_result.status == "SUCCESS":
        response.result = task_result.result or {}
    elif task_result.status == "FAILURE":
        response.result = {"error": str(task_result.info)}
    
    return response


@app.get("/download/{task_id}")
async def download_result(task_id: str):
    """下载翻译结果"""
    
    task_result = celery_app.AsyncResult(task_id)
    
    if task_result.status != "SUCCESS":
        raise HTTPException(status_code=400, detail=f"任务未完成,当前状态: {task_result.status}")
    
    output_path = task_result.result.get("output_path")
    if not output_path or not Path(output_path).exists():
        raise HTTPException(status_code=404, detail="结果文件不存在")
    
    output_filename = task_result.result.get("output_filename", "translated.pdf")
    
    return FileResponse(
        path=output_path,
        media_type="application/pdf",
        filename=output_filename
    )


@app.get("/health")
async def health_check():
    """健康检查"""
    return {
        "status": "ok",
        "redis": "connected" if celery_app.control.inspect().active() else "disconnected"
    }


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000, workers=1)

Step 6: 启动服务

启动Redis(如果没有):

bash 复制代码
docker run -d -p 6379:6379 --name pdf-redis redis:7-alpine

启动Celery Worker(终端1):

bash 复制代码
celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4

启动FastAPI(终端2):

bash 复制代码
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

完整使用流程

1. 上传PDF创建任务

bash 复制代码
curl -X POST "http://localhost:8000/translate" \
  -F "file=@test_document.pdf"

响应:

json 复制代码
{
  "task_id": "8f4d2a3b-1c5e-4f7a-8d9b-2e1f3a4b5c6d",
  "filename": "test_document.pdf",
  "status": "PENDING",
  "message": "任务已创建,使用 /tasks/8f4d2a3b-... 查询状态"
}

2. 查询任务进度

bash 复制代码
curl "http://localhost:8000/tasks/8f4d2a3b-1c5e-4f7a-8d9b-2e1f3a4b5c6d"

响应:

json 复制代码
{
  "task_id": "8f4d2a3b-1c5e-4f7a-8d9b-2e1f3a4b5c6d",
  "status": "PROGRESS",
  "progress": {
    "current": 47,
    "total": 100,
    "percent": 47.0
  }
}

3. 下载翻译结果

bash 复制代码
curl "http://localhost:8000/download/8f4d2a3b-1c5e-4f7a-8d9b-2e1f3a4b5c6d" \
  -o "translated.pdf"

性能优化要点

1. Worker并发配置

bash 复制代码
# 启动4个并发worker,每个worker处理1个任务
celery -A app.tasks.celery_app worker --concurrency=4 --prefetch-multiplier=1

2. 任务优先级

python 复制代码
# 在任务定义时设置优先级
celery_app.conf.task_queue_max_priority = 10
celery_app.conf.task_default_priority = 5

# 提交任务时指定优先级
translate_pdf_task.apply_async(
    args=[input_path, output_filename],
    priority=9  # VIP用户的翻译任务优先级最高
)

3. 监控(Flower)

bash 复制代码
pip install flower
celery -A app.tasks.celery_app flower --port=5555

打开 http://localhost:5555 可以看到:

  • 实时任务列表
  • Worker状态
  • 任务执行时间统计
  • 失败任务详情

4. 数据库持久化(替代Redis存储)

如果担心Redis宕机导致任务丢失,可以用PostgreSQL作为backend:

python 复制代码
celery_app.conf.update(
    celery_result_backend="db+postgresql://user:pass@localhost/pdftasks"
)

需要安装pip install sqlalchemy psycopg2

异常处理与生产部署

常见异常

异常 原因 解决方案
Worker一直显示PROGRESS 翻译函数卡死 设置task_time_limit强制超时
Redis连接失败 Redis服务挂了 配置Redis哨兵或集群
任务丢失 Worker进程崩溃 启用acks_late=True,确保任务执行后才确认
输出文件超大 翻译后PDF膨胀 压缩输出或分章节翻译

生产部署建议

bash 复制代码
# 使用Gunicorn管理FastAPI
gunicorn app.main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --timeout 120

使用Supervisor管理Celery Worker

ini 复制代码
# /etc/supervisor/conf.d/celery.conf
[program:celery_worker]
command=celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4
directory=/app
user=www-data
autostart=true
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/celery_worker.log

总结

本文分享的FastAPI + Celery + Redis异步任务系统,完整解决了企业级PDF翻译的关键问题:

  1. 异步非阻塞:用户提交后立即返回任务ID,无需等待
  2. 进度可查:实时查询翻译进度,前端可展示进度条
  3. 结果可下载:翻译完成后自动生成可下载的PDF
  4. 并发可扩展:Worker数量可按业务量横向扩展
  5. 异常可恢复:任务失败可重试,支持acks_late

整套系统从开发到部署,一个工程师两周内可全部交付。对于中等规模(每天1000-10000份PDF)的企业场景,完全够用。

延伸阅读


标签:Python自动化、PDF翻译、FastAPI、Celery、Redis、异步任务、效率工具、AI翻译

相关推荐
霸道流氓气质2 小时前
Redis Pub/Sub — 概念、原理、场景与代码示例
数据库·redis·缓存
AC赳赳老秦2 小时前
语义采集进阶实战:利用 OpenClaw AI 语义识别自动提取网页核心信息,无需手动编写选择器
java·运维·服务器·python·信息可视化·deepseek·openclaw
Logintern092 小时前
什么时候应该用多进程什么时候用多线程呢?
开发语言·python
明月_清风2 小时前
Pi Agent 深度解析:开源极简终端 AI 编码代理的终极指南
前端·后端·ai编程
程序员cxuan2 小时前
DeepSeek Harness 必装的插件公布了!
人工智能·后端·程序员
fatcoder3 小时前
玩转Nginx 03 — location 匹配规则:让不同的路径各回各家
前端·后端·nginx
wno7043 小时前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
无凭3 小时前
字节跳动 DeerFlow:Agent Harness 怎么让大模型主动向用户提问?
人工智能·python
蜀道山老天师3 小时前
Python + Playwright 实现问卷星自动化填写
python
月才3 小时前
Spring Boot 接口参数校验从入门到精通
后端