前言
最近在做一个内部工具,需要把 PDFTranslator 的翻译能力集成到公司的文档管理系统中。核心需求是:用户上传PDF后,系统异步完成翻译,翻译期间用户可以关闭页面,完成后通过通知告知结果。
这本质上是一个典型的异步任务处理场景——而 Celery + Redis 是 Python 生态中最成熟的异步任务队列方案。本文分享完整的实现思路和代码。
环境准备
- Python 3.10+
- Redis 7.x(Windows可用Memurai或WSL中的Redis)
- 依赖安装:
pip install celery redis requests python-dotenv
架构设计
用户上传PDF → Web API接收 → 任务入队(Celery) → Worker消费
↓
调用PDFTranslator API翻译
↓
结果存入Redis缓存 + 文件存储
↓
用户轮询任务状态 → 下载结果
实现步骤
Step 1: 配置 Celery
# celery_config.py
from celery import Celery
app = Celery(
'pdf_translate',
broker='redis://localhost:6379/0',
backend='redis://localhost:6379/1',
broker_connection_retry_on_startup=True
)
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分钟,触发 SoftTimeLimitExceeded
worker_max_tasks_per_child=50, # 每worker处理50个任务后重启,防止内存泄漏
worker_concurrency=4, # 4个并发worker
)
Step 2: 定义翻译任务
# tasks.py
import os
import hashlib
import requests
from pathlib import Path
from celery_config import app
from celery.exceptions import SoftTimeLimitExceeded
# PDFTranslator API 配置
PDFTRANSLATOR_API = "https://pdftranslator.org/api/translate"
UPLOAD_DIR = Path("./uploads")
RESULT_DIR = Path("./results")
UPLOAD_DIR.mkdir(exist_ok=True)
RESULT_DIR.mkdir(exist_ok=True)
def get_file_hash(file_path: str) -> str:
"""计算文件MD5,用于去重和缓存键"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
@app.task(bind=True, name='translate_pdf')
def translate_pdf_task(self, file_path: str, source_lang: str, target_lang: str) -> dict:
"""
异步PDF翻译任务
Args:
file_path: 源PDF文件路径
source_lang: 源语言代码 (auto=自动检测)
target_lang: 目标语言代码
Returns:
dict: {status, result_path, message}
"""
task_id = self.request.id
file_hash = get_file_hash(file_path)
# 更新状态:开始处理
self.update_state(state='PROCESSING', meta={
'step': 'uploading',
'progress': 10,
'message': '正在上传文件...'
})
try:
# 1. 调用翻译API
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f, 'application/pdf')}
data = {
'source_lang': source_lang,
'target_lang': target_lang,
}
resp = requests.post(
PDFTRANSLATOR_API,
files=files,
data=data,
timeout=540 # 9分钟超时
)
if resp.status_code != 200:
return {
'status': 'FAILED',
'error': f'API返回错误: HTTP {resp.status_code}',
'detail': resp.text[:500]
}
# 2. 保存翻译结果
result_path = RESULT_DIR / f"{task_id}_{target_lang}.pdf"
with open(result_path, 'wb') as f:
f.write(resp.content)
self.update_state(state='PROCESSING', meta={
'step': 'completed',
'progress': 100,
'message': '翻译完成'
})
return {
'status': 'SUCCESS',
'result_path': str(result_path),
'file_size': os.path.getsize(result_path),
'source_lang': source_lang,
'target_lang': target_lang,
}
except SoftTimeLimitExceeded:
return {
'status': 'TIMEOUT',
'error': '翻译任务超时(超过9分钟)'
}
except FileNotFoundError:
return {
'status': 'FAILED',
'error': f'文件不存在: {file_path}'
}
except Exception as e:
return {
'status': 'FAILED',
'error': str(e)
}
Step 3: 构建 Web API 层
# app.py - Flask Web API
import json
import os
from pathlib import Path
from flask import Flask, request, jsonify, send_file
from werkzeug.utils import secure_filename
from tasks import translate_pdf_task, UPLOAD_DIR, RESULT_DIR
from celery.result import AsyncResult
from celery_config import app as celery_app
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 20 * 1024 * 1024 # 20MB限制
@app.route('/api/health', methods=['GET'])
def health_check():
"""健康检查"""
return jsonify({'status': 'ok', 'service': 'PDF翻译服务'})
@app.route('/api/translate', methods=['POST'])
def submit_translate():
"""
提交翻译任务
请求体:
multipart/form-data
- file: PDF文件
- source_lang: 源语言(可选,默认 auto)
- target_lang: 目标语言(必填)
返回:
{task_id, status: 'QUEUED'}
"""
if 'file' not in request.files:
return jsonify({'error': '缺少文件参数'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': '文件名为空'}), 400
if not file.filename.lower().endswith('.pdf'):
return jsonify({'error': '仅支持PDF文件'}), 400
source_lang = request.form.get('source_lang', 'auto')
target_lang = request.form.get('target_lang', 'zh')
# 保存上传文件
filename = secure_filename(file.filename)
save_path = UPLOAD_DIR / filename
file.save(str(save_path))
# 提交异步任务
task = translate_pdf_task.delay(
str(save_path),
source_lang,
target_lang
)
return jsonify({
'task_id': task.id,
'status': 'QUEUED',
'message': '翻译任务已提交,请通过 /api/status/{task_id} 查询进度'
}), 202
@app.route('/api/status/<task_id>', methods=['GET'])
def get_task_status(task_id: str):
"""查询任务状态"""
task = AsyncResult(task_id, app=celery_app)
response = {
'task_id': task_id,
'status': task.state,
}
if task.state == 'PENDING':
response['message'] = '任务等待中...'
elif task.state == 'PROCESSING':
response['meta'] = task.info
elif task.state == 'SUCCESS':
response['result'] = task.result
response['download_url'] = f'/api/download/{task_id}'
elif task.state == 'FAILURE':
response['error'] = str(task.info)
else:
response['message'] = f'未知状态: {task.state}'
return jsonify(response)
@app.route('/api/download/<task_id>', methods=['GET'])
def download_result(task_id: str):
"""下载翻译结果"""
# 查找结果文件
for pdf_file in RESULT_DIR.glob(f"{task_id}_*.pdf"):
return send_file(
str(pdf_file),
mimetype='application/pdf',
as_attachment=True,
download_name=f'translated_{task_id}.pdf'
)
return jsonify({'error': '结果文件不存在或已被清理'}), 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)
Step 4: 启动服务
需要三个终端窗口分别启动:
# 终端1: 启动 Redis(如果未运行)
redis-server
# 终端2: 启动 Celery Worker
celery -A tasks worker --loglevel=info --concurrency=4
# 终端3: 启动 Flask API
python app.py
Step 5: 客户端调用示例
# client.py - 客户端调用示例
import requests
import time
def translate_and_wait(file_path: str, target_lang: str = 'zh',
poll_interval: int = 3, timeout: int = 600):
"""
提交翻译任务并等待完成
Args:
file_path: PDF文件路径
target_lang: 目标语言
poll_interval: 轮询间隔(秒)
timeout: 总超时时间(秒)
"""
BASE_URL = "http://localhost:8080"
# 1. 提交任务
with open(file_path, 'rb') as f:
resp = requests.post(
f"{BASE_URL}/api/translate",
files={'file': f},
data={'target_lang': target_lang}
)
if resp.status_code != 202:
print(f"提交失败: {resp.json()}")
return
task_id = resp.json()['task_id']
print(f"任务已提交: {task_id}")
# 2. 轮询状态
elapsed = 0
while elapsed < timeout:
time.sleep(poll_interval)
elapsed += poll_interval
status_resp = requests.get(f"{BASE_URL}/api/status/{task_id}")
status_data = status_resp.json()
status = status_data['status']
if status == 'SUCCESS':
print(f"翻译完成!文件大小: {status_data['result']['file_size']} 字节")
# 3. 下载结果
dl_resp = requests.get(f"{BASE_URL}/api/download/{task_id}")
output_path = f"translated_{task_id}.pdf"
with open(output_path, 'wb') as f:
f.write(dl_resp.content)
print(f"结果已保存: {output_path}")
return
elif status == 'FAILURE':
print(f"翻译失败: {status_data.get('error')}")
return
else:
progress = status_data.get('meta', {}).get('progress', '?')
print(f"状态: {status} | 进度: {progress}%")
print("等待超时")
if __name__ == '__main__':
translate_and_wait('sample.pdf', target_lang='zh')
进阶优化
1. 任务去重
# 在 tasks.py 中添加任务去重装饰器
from functools import wraps
from celery.result import AsyncResult
def deduplicate_task(timeout=3600):
"""基于文件哈希的任务去重"""
def decorator(func):
@wraps(func)
def wrapper(self, file_path, *args, **kwargs):
file_hash = get_file_hash(file_path)
cache_key = f"task_dedup:{file_hash}"
# 检查是否有相同的任务正在运行或已完成
cached_task_id = app.backend.client.get(cache_key)
if cached_task_id:
return AsyncResult(cached_task_id).result
# 设置缓存并执行
app.backend.client.setex(cache_key, timeout, self.request.id)
return func(self, file_path, *args, **kwargs)
return wrapper
return decorator
2. 失败重试
@app.task(
bind=True,
name='translate_pdf',
max_retries=3,
default_retry_delay=60, # 重试间隔60秒
autoretry_for=(requests.exceptions.Timeout, requests.exceptions.ConnectionError),
)
def translate_pdf_task(self, file_path, source_lang, target_lang):
# ... 任务逻辑
pass
3. 结果文件自动清理
# cleanup.py - 定期清理旧文件
from pathlib import Path
import time
def cleanup_old_files(directory: Path, max_age_hours: int = 24):
"""清理超过指定时间的文件"""
cutoff = time.time() - max_age_hours * 3600
for f in directory.glob("*.pdf"):
if f.stat().st_mtime < cutoff:
f.unlink()
print(f"已清理: {f.name}")
# 配合 Celery Beat 定时执行
from celery.schedules import crontab
app.conf.beat_schedule = {
'cleanup-every-hour': {
'task': 'cleanup_old_results',
'schedule': crontab(minute=0),
},
}
运行效果
- 小文件(<1MB):从提交到完成约 30-60 秒
- 中等文件(1-5MB):约 1-3 分钟
- 大文件(5-20MB):约 3-8 分钟
- 队列中无任务时,任务几乎即时开始
- 4个并发worker可同时处理4个不同的翻译请求
总结
Celery + Redis 的异步任务队列方案,非常适合需要将 PDF 翻译能力集成到现有业务系统中的应用场景。核心优势:
- 解耦:Web API 只管接收请求和返回结果,实际翻译由 Worker 异步处理
- 可扩展:增加 Worker 实例即可水平扩展处理能力
- 可靠性:任务失败自动重试,Worker 异常重启不影响已有任务
- 可观测:通过 Celery Flower 或自定义 endpoint 实时监控任务状态
完整代码可在实际项目中直接复用,根据业务需求调整 API 端点和任务逻辑即可。
标签:Python、Celery、Redis、PDF翻译、异步任务
363



被折叠的 条评论
为什么被折叠?



