背景
做文档翻译的工程方案选型时,绕不开一个问题:本地跑还是丢给在线服务?
支持本地的理由很硬:数据不出内网、没有调用额度、可以随便改链路。支持在线的理由也很硬:不用维护模型环境、不用管显存、格式还原通常做得更成熟。
但这两边的讨论经常停留在"感觉",缺少可量化的对比。这篇文章我用一台普通开发机,把两条路线都实测一遍,把时间、资源、隐私三个维度的成本摊开来看,最后给一套选择判据。
测试对象:30 页 / 50 页结构化 PDF(正文 + 12 行 × 3 列表格),含文本层,非扫描件。实测环境:Windows 11,Python 3.13.12,PyMuPDF 1.28.2,16 GB 内存。
评测维度设计
先说清楚测什么、为什么这样测。
本地方案的成本结构是:链路每一段都要自己付。取文本、分析结构、栅格化、识别/翻译、重组输出,每一步都消耗时间和内存,所以本地必须分段计时。
在线方案的成本结构是:你只付上传和等待。但代价在别处——文件体积上限、额度限制,以及最关键的,文件离开了你的机器。
所以我设计了四个观测点:
- 本地链路分段耗时:打开、取文、结构分析、栅格化、回写
- 本地链路内存占用:各阶段 RSS 增量与峰值
- 体积约束触发条件:多大概率会超过在线服务的单文件上限
- 隐私边界:哪些数据实际离开了本机
本地链路实测:分阶段耗时
先测最理想的情况——纯文本路径,不涉及 OCR:
import time, os, gc
import psutil
import pymupdf
import pypdf
PDF = "testpdf/sample_50p.pdf"
P = psutil.Process(os.getpid())
def timed(fn):
"""计时包装"""
t0 = time.perf_counter()
r = fn()
return round(time.perf_counter() - t0, 3), r
doc = pymupdf.open(PDF)
gc.collect()
base = P.memory_info().rss / 1024 / 1024
# 1 打开文档(惰性加载,只解析交叉引用表)
t_open, _ = timed(lambda: pymupdf.open(PDF))
# 2 全文提取
t_text, text = timed(lambda: "".join(p.get_text() for p in doc))
# 3 结构分析(blocks 坐标,用于阅读顺序重建)
t_blocks, blk = timed(lambda: [len(p.get_text("blocks")) for p in doc])
# 4 页面栅格化(扫描件 OCR 链路必经步骤)
def raster150():
n = 0
for p in doc:
pm = p.get_pixmap(dpi=150)
n += len(pm.samples)
del pm
return n
t_raster, rbytes = timed(raster150)
# 5 回写输出 PDF
def writeback():
nd = pymupdf.open()
nd.insert_pdf(doc)
b = nd.tobytes()
nd.close()
return len(b)
t_write, outbytes = timed(writeback)
gc.collect()
delta = P.memory_info().rss / 1024 / 1024 - base
print(f"打开 {t_open}s | 取文 {t_text}s | 结构 {t_blocks}s | "
f"栅格化 {t_raster}s | 回写 {t_write}s")
print(f"内存增量 {delta:.1f} MB, 栅格字节 {rbytes/1024/1024:.1f} MB, "
f"输出 {outbytes/1024:.1f} KB")
实测输出(50 页样本):
打开 0.001s | 取文 0.025s | 结构 0.014s | 栅格化 0.182s | 回写 0.009s
内存增量 1.9 MB, 栅格字节 311.4 MB, 输出 45.5 KB
换算到 30 页:
| 阶段 | 耗时(30 页折算) | 说明 |
|---|---|---|
| 打开文档 | < 0.001s | 惰性加载,只读交叉引用表 |
| 全文文本提取 | 0.015s | PyMuPDF 是 C 层实现,纯文本路径极快 |
| 结构分析(blocks) | 0.008s | 取坐标用于分栏与阅读顺序 |
| 页面栅格化 150 dpi | 0.109s | 扫描件才需要,电子稿可跳过 |
| 回写输出 PDF | 0.005s | 复用原内容流 |
| 合计 | ≈ 0.14s | 不含翻译本身 |
这个数字很关键:本地处理 30 页文档的"机械耗时"只有 0.14 秒。 也就是说,本地路线的耗时几乎全部来自你要自己搭的那部分——模型加载、推理、术语后处理。纯 PDF 操作在本地是完全不成问题的。
其中栅格化是唯一明显的开销(占 78%),因为要把矢量页面渲染成 311 MB 的位图数据。但它是扫描件链路才需要的:电子稿 PDF 有文本层,直接取文即可,跳过这一步总耗时降到 0.03 秒。
内存实测:本地链路的真实成本
C 层库的内存行为和 Python 对象完全不同。50 页文档栅格化时,Python 堆只涨了不到 2 MB,但 C 层一次性分配了 311 MB 位图缓冲。这部分的占用必须在内存预算里预留:
import gc, psutil, pymupdf
P = psutil.Process()
doc = pymupdf.open("testpdf/sample_50p.pdf")
gc.collect(); base = P.memory_info().rss / 1024 / 1024
peak = base
for pg in doc: # 流式栅格化
pm = pg.get_pixmap(dpi=300)
_ = len(pm.samples)
del pm
peak = max(peak, P.memory_info().rss / 1024 / 1024)
gc.collect()
print(f"基线 {base:.1f} MB -> 峰值 {peak:.1f} MB (增量 {peak-base:.1f} MB)")
# 基线 50.9 MB -> 峰值 75.8 MB (增量 24.9 MB)
300 dpi 单页位图 24.9 MB,峰值增量正好等于一页的位图大小——说明流式写法确实做到了"只驻留一页"。但如果你需要并发处理(比如 4 个线程同时渲染),峰值就是 24.9 × 4 ≈ 100 MB;如果还要在内存里同时持有多个页面图给 OCR 用,这个数字会继续乘上去。
所以本地路线的资源账是这样的(30 页,300 dpi,单线程):
| 项目 | 占用 |
|---|---|
| Python 进程基线 | ≈ 51 MB |
| 栅格化峰值增量 | +25 MB |
| 4 线程并发峰值增量 | +100 MB |
| 磁盘中间产物(300 dpi PNG × 30) | ≈ 3.2 MB |
单线程跑,一台 4 GB 的机器绰绰有余。这也说明:"本地部署需要很强硬件"这个说法,对 PDF 预处理环节并不成立——真正的硬件门槛在模型推理,不在文件处理。
体积约束:在线服务最容易被踩的门槛
在线服务通常有单文件体积上限。我用 reportlab 生成的测试文档模拟了"扫描件"的体积膨胀——把页面栅格化后重新拼成图片型 PDF:
import pymupdf
doc = pymupdf.open("testpdf/sample_50p.pdf")
for dpi in (150, 300, 600):
nd = pymupdf.open()
for pg in doc:
pm = pg.get_pixmap(dpi=dpi)
np_ = nd.new_page(width=pm.width, height=pm.height)
np_.insert_image(np_.rect, pixmap=pm)
del pm
b = nd.tobytes(deflate=True, garbage=4) # 压缩后
print(f"{dpi} dpi 扫描版: {len(b)/1024/1024:.2f} MB")
nd.close()
实测结果:
| 输出形态 | 50 页体积 | 折算 30 页 | 相对原始文本 PDF |
|---|---|---|---|
| 原文件(文本 PDF) | 48.1 KB | 28.9 KB | 1× |
| 150 dpi 图片型 | 2.51 MB | 1.51 MB | 53× |
| 300 dpi 图片型 | 5.25 MB | 3.15 MB | 112× |
| 600 dpi 图片型 | 11.97 MB | 7.18 MB | 255× |
关键结论:体积膨胀跟页数无关、跟渲染精度强相关。
30 页在 300 dpi 下是 3.15 MB,距离常见的 20 MB 上限还很宽裕。同样的精度下,页数要涨到 190 页左右才会顶到上限。但如果原始件是 600 dpi 高清扫描件、又赶上 200 页以上,就会撞墙。
这里有个反直觉的点:真正容易超限的不是"大文档",而是"高精度扫描件 + 中等页数"的组合。 一份 150 页的 600 dpi 产品手册,比一份 300 页的文本型行业报告更容易被平台拦下来。所以判据不是页数,而是先算一下:页数 × 单页位图字节 ÷ 压缩比。
隐私边界:两种方案的本质差异
前两个维度本地和在线各有胜负,但隐私这个维度是结构性的、无法用参数调优弥补的。
本地方案的隐私边界很清楚:
- 文件全程不离开本机,中间产物(位图、临时文本)都在自己的磁盘上
- 唯一的风险点是中间文件是否清理干净
- 如果链路里调用了外部 API 做翻译,那文件内容还是出去了——“本地部署"不等于"数据不出门”,要看你把翻译这一步放在哪
在线方案的边界也很清楚:
- 文件必须完整上传到对方服务器,翻译在对方环境完成
- 你能控制的只有:是否 SSL 传输、服务方承诺多久删除、是否要求注册(注册意味着身份和文件绑定)
- 你无法验证"24 小时后删除"是否真的执行,只能选择相信服务方的承诺
我给自己定的规则是按文档敏感度分流:
- 内部资料、公开报告:走在线,省事优先
- 客户合同、财务文件、未发布的商业计划:优先本地,或者选择明确承诺不注册、不留存、定期自动清除的在线服务
- 任何情况下都不要:把带个人身份信息或涉密标记的文档传给需要注册才能用的免费工具
第三条不是谨慎过度。免费工具的商业模式决定了它需要留存数据来做产品分析,注册账号又让"这份文件属于谁"变得可追溯。两个因素叠加,风险是实打实存在的。
综合对比与选择判据
把三个维度放在一起:
| 维度 | 本地部署 | 在线服务 |
|---|---|---|
| PDF 机械处理耗时 | 0.14s / 30 页(电子稿 0.03s) | 上传 + 排队,通常 30s–2min |
| 资源占用 | 基线 51 MB + 峰值 25 MB(4 线程 ×100 MB) | 本机几乎零占用 |
| 环境维护 | 需装依赖、管模型文件 | 零维护 |
| 体积约束 | 无 | 受单文件上限约束,高精度扫描件易超限 |
| 并发/批量 | 受本机 CPU 内存限制,可无限次 | 受额度限制 |
| 隐私 | 文件不出内网(除非外调 API) | 文件必然离开本机 |
| 格式还原能力 | 需自己实现,是整条链路的难点 | 通常是产品核心能力,更成熟 |
从这张表能看出一个明确的分工:
适合本地的场景——文档量大且一次性处理(比如把 500 份归档文件批量转成中文)、文档敏感不能外传、链路需要深度定制(自定义术语库、特殊结构解析)。
适合在线的场景——需求零散(一次几页到几十页)、需要高性能的格式还原而不想自己实现排版重排、涉及小语种(自己配模型成本太高)、临时用不想装环境。
结论
回到最初的问题:30 页文档该怎么处理?
如果你的文档是电子稿、需求量小、对排版还原有要求——在线方案的实际总成本更低。本地的 0.14 秒机械耗时看着很漂亮,但那只是链路里最简单的一段;真正难的格式保留和排版重排,在线上已经是被打磨过的产品能力,而本地你要从零实现。
如果你的文档是敏感数据、或者量级到了几百份的批量处理——本地更合适。资源上完全跑得动(预处理的硬件门槛远比想象中低),而且没有额度和隐私方面的约束。
一个折中方案是我目前在用的:预处理在本地做,格式保持和翻译交给在线服务。 本地先判断文本层、拆章节、把高精度扫描件降采样压到合理体积,再上传。这样既避开了体积上限和无效上传,也不用自己实现排版重排。
最后提醒一个容易被忽略的细节:无论选哪条路线,先确认文档有没有文本层。如果是纯图片扫描件,两条路线都会拿到空白结果,必须先做 OCR 回填。这一步的判断成本只有 1 分钟,但能省掉整条链路的返工。
参考资料
- PyMuPDF 文档 - Document / Page / Pixmap:https://pymupdf.readthedocs.io/
- psutil 文档 - Process memory info:https://psutil.readthedocs.io/en/latest/
- pypdf 文档:https://pypdf.readthedocs.io/
标签:Python、PDF、技术选型、性能测试、PyMuPDF
453



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



