PDF 合并别再用在线工具:12 份文件浏览器里合完,实测不上传
12 个 PDF、240 页,拖进去点一下,33 毫秒合完,输出 3.2 KB。
整个过程没有一次上传。我把下载回来的文件用 pypdf 数了一遍:240 页,文件头还是 %PDF-1.7。
换成 50 个文件、1000 页,耗时 104 毫秒,页面记录到的网络请求仍然是 0 次。

在线案例与源码:https://atomgit.com/deli007/demo_park/tree/main/codearts-pdf-merge-tool
生成物只有一个index.html,合并和拆分都在浏览器内存里完成,文件不出本机。
一、为什么值得自己搭一个:在线工具的麻烦在「先上传」
搜 PDF 合并,前几页的工具流程几乎一样:选文件、等进度条、下载。
进度条那几秒里,你的发票、合同、扫描件已经在别人的服务器上落过盘了。
带客户名单的对账单、身份证扫描件、签了保密协议的附件,很多人根本不敢往这些站点传。
我要的只有一件事:文件在本地算完,页面不负责传。
二、准备环境:进入码道 Web,把需求写成一段话
浏览器打开 https://devcloud.cn-north-4.huaweicloud.com/chat/home ,登录后把需求写进对话框,就能拿到一个能跑的单文件应用。
不用装软件,不用配环境,本文这个工具就是这么来的。
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
需求我写了 10 条,事后看最值钱的是这三条:
- 页面上要有网络请求计数,从打开页面到合并结束,这个数必须一直是 0;
- 遇到损坏文件和加密文件,要在那一行和顶部状态栏写清原因,不许只打控制台日志;
- 页面里要有自检面板,每条断言把期望值和实测值都渲染出来。
第三条是这次没翻车的直接原因,后面细说。
三、实测:12 个文件、240 页、33 毫秒
我拿 12 个各 20 页的 PDF 拖进页面,列表里 12 行,页数一栏自动读出每份 20 页。
点一下「合并全部」,右侧输出面板给出三个数:240 页、3.2 KB、33 毫秒。

页数是 12 乘 20,对得上;体积比单个文件还小,因为输出里没有重复的字体和页对象,只有页面树。
这 33 毫秒不是估算,是页面里 performance.now() 卡出来的两端差值。
四、文件越多越慢吗:3 到 50 个文件的对照
我把同样的测试跑成了四档,输入文件都是 20 页一份。
| 输入文件数 | 输入总页数 | 输出页数 | 输出体积 | 合并耗时 |
|---|---|---|---|---|
| 3(页面内置示例) | 12 | 12 | 5.2 KB | 1.5 毫秒 |
| 12 | 240 | 240 | 3.2 KB | 33 毫秒 |
| 20 | 400 | 400 | 5.3 KB | 45.7 毫秒 |
| 50 | 1000 | 1000 | 12.4 KB | 104.3 毫秒 |
从 240 页到 1000 页,页数翻了 4 倍多,耗时从 33 毫秒涨到 104 毫秒,是 3 倍出头,接近线性。
真正撑住速度的是页数少:合并只是在页面树上搬引用,不重新渲染内容。
所以这组数字要带着前提看:我的测试文件是空白页,一页不到 3 KB;换成扫描件,体积和耗时都会明显上去。
五、把合并结果下载回来,用 pypdf 数页数
页面自己说 240 页,我不太想直接信,就把下载按钮产出的那份文件截下来,交给 Python 侧的 pypdf 复核。
三次复核的结果:12 页那次是 5333 字节,240 页那次是 3300 字节,1000 页那次是 12729 字节。
三次读出来的页数分别是 12、240、1000,和页面报的数字一致,文件头都是 %PDF-1.7。
这也是我判断「它真的合成了 PDF」而不是「它在页面上画了个数字」的办法。
六、拆分:抽 5 到 9 页和第 12 页,出来 6 页
选中一个 20 页的文件,在页码框里写 5-9,12,点「拆分 / 提取」。
输出面板报 6 页、0.6 KB、2.5 毫秒,我用 pypdf 数这份新文件,确实是 6 页。

页码写错它也认得出:填 9-2,状态栏直接给「起始页 9 大于结束页 2」,不生成文件。
序号不连续的写法是逗号隔开,1-3,5 这种能一次抽两段出来。
七、三种坏文件,页面各说各的
失败路径我专门造了三个文件试:一个把正常 PDF 拦腰截断,一个用密码加密过,一个干脆是文本文件改名成 .pdf。
三个都在列表里单独标红,各给自己的一句话:
| 输入 | 页面给出的原因 |
|---|---|
| 被截断的 PDF | 无法解析:文件损坏或不是有效 PDF |
| 密码保护的 PDF | 无法解析:PDF 已加密或受密码保护 |
| 文本文件改名为 .pdf | 无法解析:文件损坏或不是有效 PDF |
加密那个文件的判别是页内的,pdf-lib 解密时报的关键词里带 password,页面据此给出加密提示,不是按文件名猜的。

顶部的「合并全部」会跟着置灰,不让带着坏文件往下走。
八、页面自带 5 条断言,页数对不上就会红
底部有一块自检面板,点「运行自检」现场生成示例文件,再跑合并和拆分,把每一步的期望值和实测值并排打出来。
5 条断言,当前是 5/5 通过,自检耗时 7.5 毫秒,末尾一行还写着自检全程外发网络请求 0 次。

我把这 5 条抄下来:示例文件页数合计 12;合并后 12 页;提取第 2 页得到 1 页;提取 1-3 页得到 3 页;非法页码 9-2 被拦下。
这 5 条的共同点是把数字写死在期望值里,实测值来自真实解析结果,错了就红,藏不住。
九、说点不足
输出的 PDF 不做压缩,扫描件合出来的体积基本等于输入之和。
加密文件只能报错,不给输密码解锁,pdf-lib 本身不带解密能力。
文件顺序要手动拖,页面不会按文件名或者修改时间自动排。
而且它不联网就是残缺的:pdf-lib 从 CDN 加载,断网打开只会看到「加载失败」,按钮全灰。
我故意没让它在这种情况下假装还能用。
十、实测数据汇总
| 项目 | 实测值 | 说明 |
|---|---|---|
| 单个 HTML 文件体积 | 30219 字节 | 30 KB 左右,无构建步骤 |
| 12 个文件合并 | 33 毫秒 / 240 页 | 输出 3.2 KB |
| 50 个文件合并 | 104.3 毫秒 / 1000 页 | 输出 12.4 KB |
| 拆分 5-9,12 页 | 2.5 毫秒 / 6 页 | 输出 624 字节 |
| 合并结果复核 | 12 / 240 / 1000 页 | 下载后用 pypdf 数出来的 |
| 自检断言 | 5 / 5 通过 | 自检耗时 7.5 毫秒 |
| 页面资源请求 | 1 条 | 只有 pdf-lib 的 CDN 脚本 |
| 携带文件内容的请求 | 0 次 | 页面自记计数与资源列表都能对上 |
十一、本地复现
生成物只有一个 index.html,双击就能打开,第一次打开需要联网拉 pdf-lib。
也可以起个静态服务看:python -m http.server 8000,然后访问 http://localhost:8000 。
想自己造测试文件,用 pypdf 几句就能生成一叠空白页 PDF,我这轮的输入文件就是这么来的。
下载回来的成品想复核页数,也交给 pypdf:读一遍 len(reader.pages),和页面报的数对一下。
想看它有没有偷偷上传,按 F12 打开网络面板,再点一次合并,里面不会多出任何请求。
十二、用码道做这个小工具的体会
这次最值钱的一条需求是「页面里要有自检面板,把期望值和实测值都渲染出来」。
收到页面我第一件事就是点运行自检,5 条断言全绿,示例文件的页数、合并后的页数、非法页码的拦截都在里面。
但这只能证明页面自己算得对,能不能信还得看外面:所以我把合并结果下载回来,用 pypdf 再数一遍页数。
第三条心得是需求里把验收数字写死,比如直接写着「50 个文件、1000 页要能合完」,交回来的东西好不好判断,一眼就有答案。
失败路径也一样,我在需求里写明「库加载失败就置灰按钮、挂红条」,验证时把 CDN 地址换成连不上的域名再打开,红条、按钮全灰,和需求对得上。
不足的地方也说一句:这个工具只覆盖合并和按页提取,页面里没有压缩、没有水印、也没有加密解密,这几类需求它一个都接不住。
- 点赞
- 收藏
- 关注作者
评论(0)