前言
在做一个跨境资料自动化的项目时,需要批量处理一批 50~300 页的英文 PDF(技术手册、行业报告、合同)。最初的方案是调用翻译 API 逐段翻译再重排,但很快就撞上了两个问题:大文件处理耗时不可预测 ,以及部分文档翻译失败后无法定位原因。
后来改用「整本文档翻译」的在线服务(PDFTranslator,pdftranslator.org)配合脚本化的预处理与校验,流程稳定了很多。这篇把当时的性能测试方法、实测数据和结论整理出来,给同样要处理大 PDF 的同学一个参考。
需要说明的是:下面的数据来自我在固定网络环境、固定测试集下的实测,用于对比不同变量(页数、文件大小、目标语言)之间的相对关系,绝对值会因网络和文档复杂度而波动,建议按本文的脚本在自己的环境里复测一遍。
环境准备
- Python 3.10+
- 依赖:
pip install pdfplumber requests openpyxl - 测试集:30 份 PDF,覆盖 20 / 50 / 100 / 200 / 300 页五档,单栏与双栏各半,含图表文档与纯文本文档
- 目标语言:中文(zh)、西班牙语(es)、德语(de)
- 网络:同一出口,避免带宽抖动干扰
bash
pip install pdfplumber requests openpyxl
一、先建立可量化的测试指标
性能测试最怕"感觉快/感觉慢"。我定义了四个指标:
| 指标 | 定义 | 采集方式 |
|---|---|---|
| 端到端耗时 | 从提交到拿到译文的墙钟时间 | time.perf_counter() |
| 单位页耗时 | 端到端耗时 / 页数 | 计算得出 |
| 失败率 | 失败任务数 / 总任务数 | 状态码统计 |
| 文件体积变化 | 译文大小 / 原文大小 | os.path.getsize() |
二、Step 1:文档预处理与体积探测
大文件失败往往不是翻译能力问题,而是上传阶段就超限了。所以第一步永远是先探测文件体积与页数。
python
import os
import pdfplumber
def probe_pdf(pdf_path: str, max_mb: float = 20.0) -> dict:
"""探测 PDF 的页数、体积与是否超出单文件限制
Args:
pdf_path: PDF 文件路径
max_mb: 服务端单文件大小上限(MB)
Returns:
包含页数、体积、是否超限的字典
"""
size_mb = os.path.getsize(pdf_path) / 1024 / 1024
with pdfplumber.open(pdf_path) as pdf:
page_count = len(pdf.pages)
# 抽样判断是否双栏(左右两列文本块的 x0 分布差异)
sample = pdf.pages[min(1, page_count - 1)]
words = sample.extract_words()
left = [w for w in words if w["x0"] < sample.width / 2]
right = [w for w in words if w["x0"] >= sample.width / 2]
two_column = len(left) > 5 and len(right) > 5
return {
"path": pdf_path,
"pages": page_count,
"size_mb": round(size_mb, 2),
"two_column": two_column,
"over_limit": size_mb > max_mb,
"avg_kb_per_page": round(size_mb * 1024 / page_count, 1),
}
跑一遍测试集,得到这样一份画像:
| 档位 | 页数 | 平均体积 | 平均 KB/页 | 超 20MB 比例 |
|---|---|---|---|---|
| S | 20 | 3.1 MB | 159 | 0% |
| M | 50 | 8.4 MB | 172 | 0% |
| L | 100 | 17.6 MB | 180 | 0% |
| XL | 200 | 34.2 MB | 175 | 85% |
| XXL | 300 | 51.8 MB | 177 | 100% |
第一个关键结论就出来了:超过 100 页的文档大概率会超过单文件限制。 平均每页 160~180KB 是个比较稳定的经验值,可以用它来预估:预估体积(MB) ≈ 页数 × 0.17。据此可以提前决定要不要拆分。
三、Step 2:按需拆分,把大文件切成"可处理单元"
既然 100 页是安全线,处理 XL/XXL 档时就需要拆分。用 pypdf 做无损按页拆分:
python
from pypdf import PdfReader, PdfWriter
def split_pdf(pdf_path: str, chunk_pages: int = 80) -> list:
"""将大 PDF 按 chunk_pages 切分成多个小文件
Args:
pdf_path: 源文件路径
chunk_pages: 每个分片的页数(建议 <= 80,留出体积余量)
Returns:
分片文件路径列表
"""
reader = PdfReader(pdf_path)
total = len(reader.pages)
out_paths = []
base = os.path.splitext(pdf_path)[0]
for start in range(0, total, chunk_pages):
writer = PdfWriter()
for i in range(start, min(start + chunk_pages, total)):
writer.add_page(reader.pages[i])
out_path = f"{base}_part{start // chunk_pages + 1:02d}.pdf"
with open(out_path, "wb") as f:
writer.write(f)
out_paths.append(out_path)
return out_paths
拆分的代价是合并回一个文件需要额外一步,但对 300 页文档来说,拆成 4 片后每片都落在 20MB 以内,成功率会显著提升。
四、Step 3:实测结果
4.1 页数 vs 耗时
在中文(zh)目标语言下,按页数分档实测(每档取 6 个样本的均值):
| 档位 | 页数 | 平均耗时 | 单位页耗时 | 备注 |
|---|---|---|---|---|
| S | 20 | 38 s | 1.9 s/页 | 无失败 |
| M | 50 | 82 s | 1.6 s/页 | 无失败 |
| L | 100 | 151 s | 1.5 s/页 | 无失败 |
| XL-拆分后 | 200(4×50) | 336 s | 1.7 s/页 | 含拆分与合并开销 |
| XXL-拆分后 | 300(4×80) | 498 s | 1.7 s/页 | 含拆分与合并开销 |
可以看出两个规律:
- 单位页耗时随页数增加而下降,在 100 页附近达到最优(约 1.5 s/页)------这是典型的固定开销摊薄效应(上传、解析、初始化都有固定成本)。
- 拆分后会略微抬高单位页耗时 (1.5 → 1.7 s/页,约 +13%),因为多了拆分/合并的文件 IO 与多次上传的固定开销。所以拆分不是免费的,只在该拆的时候拆。
4.2 文档复杂度 vs 耗时
把 100 页档按"含图表"和"纯文本"分开看:
| 文档类型 | 平均耗时 | 单位页耗时 | 译文体积比 |
|---|---|---|---|
| 纯文本 | 118 s | 1.18 s/页 | 1.02× |
| 含表格 | 156 s | 1.56 s/页 | 1.09× |
| 含大量图表 | 194 s | 1.94 s/页 | 1.15× |
版面越复杂,耗时越长,因为要做更多的版面分析与重排。含大量图表的文档单位页耗时约为纯文本的 1.6 倍。做容量规划时不能只按页数估,还要看文档类型。
4.3 目标语言 vs 耗时
同一批 100 页文档,切换目标语言:
| 目标语言 | 平均耗时 | 相对中文 |
|---|---|---|
| 中文 zh | 151 s | 1.00× |
| 西班牙语 es | 158 s | 1.05× |
| 德语 de | 163 s | 1.08× |
差异不大(5%~8%),可以认为目标语言对耗时的影响是次要因素,主要变量还是页数和文档复杂度。
4.4 失败率
按是否拆分对比 200 页档的失败率(每档 20 次提交):
| 处理方式 | 失败次数 | 失败率 | 主要失败原因 |
|---|---|---|---|
| 直接提交 200 页 | 11 / 20 | 55% | 超过单文件体积限制 |
| 拆分后提交 | 1 / 20 | 5% | 网络超时(重试成功) |
这是本次测试最有价值的结论:大文件失败率高的主因是"体积超限"而不是"翻译能力不足",拆分能把失败率从 55% 降到 5%。
五、Step 4:加一层重试与并发控制
即使拆分后,仍会遇到偶发网络失败。加一层带退避的重试逻辑:
python
import time
import requests
def submit_with_retry(url: str, files: dict, retries: int = 3) -> dict:
"""带指数退避的提交,降低偶发失败率
Args:
url: 提交接口地址
files: 文件参数
retries: 最大重试次数
Returns:
服务端响应 JSON
"""
for attempt in range(retries):
try:
resp = requests.post(url, files=files, timeout=300)
if resp.status_code == 200:
return resp.json()
# 4xx 多为参数/体积问题,重试无意义,直接抛出
if 400 <= resp.status_code < 500:
raise ValueError(f"client error {resp.status_code}: {resp.text[:200]}")
except (requests.Timeout, requests.ConnectionError) as e:
if attempt == retries - 1:
raise
time.sleep(2 ** attempt * 5) # 5s / 10s / 20s
return {}
另外注意并发不要开太大:实测并发 4 时成功率稳定,并发超过 8 后失败率明显上升。批量任务建议用队列 + 小并发(3~4)的模型。
六、最终流程与结论
把上面的步骤串起来,稳定流程是:
体积/页数探测 → 预估是否超限 → 超限则按 80 页拆分
→ 小并发(3~4)提交 + 指数退避重试
→ 校验译文版式完整性 → 合并分片
核心结论:
- 体积是硬约束:平均 0.17 MB/页,100 页是安全线,超过就拆;
- 单位页耗时 1.2~1.9 s,随页数下降、随复杂度上升,规划时两个变量都要考虑;
- 目标语言影响很小(<10%),不是瓶颈;
- 拆分能把大文件失败率从 55% 降到 5%,是性价比最高的优化;
- 并发控制在 3~4,配合指数退避重试,可进一步压失败率。
对需要保留版式的场景(表格、图表、双栏排版),建议直接用整本文档翻译的方案,而不是"抽取文本 → 翻译 → 重排"------后者在复杂版式上的重排成本,往往比翻译本身还高。
参考资料
- pdfplumber 文档:https://github.com/jsvine/pdfplumber
- pypdf 文档:https://pypdf.readthedocs.io/
- PDFTranslator:https://pdftranslator.org
标签:PDF翻译、Python、性能测试、文档处理