上次把一份 PDF 按页码拆开发给两组人,第二天就被退回来了。对方说:你发我这册,第一页上的编号对不上。我打开一看,问题确实存在:我按阅读器里的页码切分,但文档右上角那串流水号从封二起就跟实际页码错开了一位。封面没编号,第二页才开始印“001”,而我切的是物理第 1 页。差一位,后面全错。
这种事遇上两回,我就把流程倒过来:按页码拆开只是动作,真正决定成败的是拆之前的核对。而且顺序不能反——先核对文件页,把 PDF 的物理页序确定下来,再去核对文档里的编号。这篇就聊这件事,从坐标系怎么建、对照表怎么填、两种拆法各自的核对逻辑,到命令、脚本、拆完怎么验、分发怎么命名。做过重复性文档分发的人应该都用得上,手上有几十页到几百页 PDF、要按组分头发的场景尤其合适。
1. 页码对不上,根子是两个坐标系被混为一谈
1.1 拆开分发的动机决定了容错空间有多小
先想清楚为什么要拆。大多数情况下不是因为文件太大发不动,而是因为两组人只应该看到自己那部分内容。比如一份汇总表里,A 组看前段、B 组看后段,互相不该看到对方的数据;再比如一份名单,前半是已确认的、后半是待补充的,发给不同的人跟进。这类场景的共同点是:拆分本身就是一种信息边界,边界一旦切错,不是排版难看的问题,而是发错了内容。
这决定了容错空间。如果只是自己看的资料切错了,重新切一次就行;但发出去的东西撤不回来,接收方可能已经把文件转发、打印、归档。所以核对不是“最好做一下”的加分项,是流程里必须有的一环。我现在的习惯是:拆分只要涉及对外发送,核对环节的时间投入不低于拆分本身。听起来夸张,但一次返工的成本远高于多花二十分钟对数。
1.2 文件页是客观事实,文档编号是人为约定
这是整件事的核心认知。两者听起来都叫“页”,但性质完全不同。
文件页是 PDF 内部的物理页序,第 1 页到第 N 页,阅读器页码框里显示的那个数字。它由文件结构决定,客观、可枚举、不会骗人。你用任何工具读一遍,得到的 N 都一样。
文档编号是印在页面内容上的标识——右上角的流水号、表格里的序号列、合同编号、工号、座位号、房间号。它是人写进去的,可能漏、可能重、可能和实际内容不符,也可能压根没印。
麻烦在于日常说话时这两个词经常混用。同事说“第 15 页那份”,你以为是物理第 15 页,他指的是编号 015 那一页,而因为封面没编号,编号 015 其实在物理第 16 页。一句含糊的话,能让你切错一整册。
判断标准很简单:这个数字能不能用程序从文件结构里直接读出来?能,就是文件页;得靠 OCR 或者人眼看内容才能拿到,就是文档编号。
1.3 先把物理坐标确定下来,再把业务标识对应上去
为什么顺序必须是“先文件页、后编号”,反过来不行?
因为文件页是稳定参照系,编号是易变业务数据。先锁参照系,再把业务标识对应上去,出错时你能立刻判断是“页找错了”还是“编号本身有问题”。反过来先按编号整理,一旦编号有缺漏或重号,你连基准都没有,后面全部推倒重来,而且没法回溯到底哪一步错的。
打个数据库的比方:文件页相当于自增主键,永远唯一、永远有序;文档编号相当于业务单号,可能重复、可能为空。没人会拿业务单号当主键去 join,同理,拆分时的切分依据必须是文件页,编号只用来验证切得对不对。
我第一次做这件事时顺序搞反了:先按台账里的编号排出“谁在第几页”,再照着这个表去切。结果台账里有三个人编号写重了,映射表直接错位,切出来的两份文件互相串了内容,还是接收方发现的。从那以后我就认死理:切分只认文件页,编号只做校验。
2. 拆之前先建一张页号对照表
2.1 表里到底要放哪几列
不用复杂的工具,一张表格就够,但字段要定死。我现在固定用这几列:
| 列名 | 含义 | 示例 |
|---|---|---|
| 物理页 | PDF 里的第几页,从 1 开始 | 16 |
| 印刷页码 | 页面底部印的页码,可能没有 | 15 |
| 文档编号 | 页面内容里的业务标识 | A-015 |
| 归属组 | 这份内容该发给谁 | 甲组 |
| 内容摘要 | 一句话说明这页是什么 | 学员登记表 |
| 备注 | 空白页、跨页表、盖章页等 | 骑缝章起始页 |
前三列是核心。物理页和印刷页码经常不一致,把差异显式记下来,比在心里算偏移量可靠得多。文档编号这一列如果页面没印,就填“无”,别留空——留空和“没印”在后续统计里是两种含义,混在一起会误导判断。
内容摘要这一列看着多余,实际很有用。拆分后抽查时,你没法逐页比对全文,但可以快速翻到某一页看它是不是“学员登记表”,一眼就能确认切没切错。
2.2 三种最常见的错位来源
错位不是偶然,是有固定套路的。我遇到过的基本归为三类:
封面和前言不参与编号。最典型。封面、目录、说明页往往不印页码,或者用罗马数字 i、ii 单独编。正文从“1”开始,此时物理页 3 对应印刷页 1,偏移量是 2。如果文档有 2 页封面加 3 页目录,偏移量就是 5。
跨页表格被拆成两页。一张表本来设计成一页,内容多了自动流到下一页,第二页只有表头和两三行。这时候物理页多了一页,但业务上它属于同一条记录。拆分时如果正好切在这两页中间,接收方拿到的是半张表。
扫描件里的空白页和重复页。双面扫描时背面空白页会被扫进来,进纸不顺还会出现重复页。这些页在物理页计数里是实打实的一页,但在业务逻辑里不该存在。我见过一次 40 页的扫描件里夹了 6 页空白,按物理页切分完全没问题,但两组人拿到手都以为漏了东西。
2.3 填完表之后的三项自检
表填完不算完,得跑三个检查:
- 重号检查:文档编号这一列有没有重复值。用表格软件的条件格式标记重复项,几秒钟的事。重号意味着要么是录入错误,要么是真的有两页共用一个编号,两种情况处理方式完全不同。
- 跳号检查:编号是否连续。如果编号是按 001、002、003 递增的,中间断了就是有页缺失或者编号规则本身不连续(比如按部门分段编号)。这一步不能想当然,规则得先问清楚。
- 行数对账:表格行数是不是等于 PDF 总页数。不等就说明有页没被登记,直接回头找。
这三项做完,你手上才有一份可信的映射关系。很多人跳过这一步直接切,切完发现不对再回来查,等于把核对成本翻倍——因为此时你面对的是两份已拆分的文件,定位问题比在原始文件上难得多。
3. 核对文件页:把物理页码确定下来
3.1 先确认总页数和起始基准
第一步永远是看总页数。别信“大概三十来页”这种描述,打开文件看页码框,或者用命令行读。
qpdf --show-npages input.pdf输出一个数字,这就是 N。后面所有的页码范围都必须落在 1 到 N 之间,任何越界都说明映射表有问题。
接着确认一件事:这份 PDF 的第一页,是不是你理解的那个“第一页”。有些文件前面挂了封面、封二、空白页、审批单,业务上的“第一页”其实是物理第 4 页。这个判断只能靠翻,不能靠猜。我的做法是把前 5 页和后 3 页都翻一遍,确认内容的起止边界在哪。
顺便检查一个容易被忽略的点:文件里有没有页被旋转过。旋转 90 度的页在缩略图里看着正常,但提取文本或者打印时会歪。发现旋转页先处理掉再拆,否则拆出来的文件在别人机器上打开是横的,又是一轮沟通。
3.2 缩略图扫读:比逐页翻快十倍的办法
几十页的文件逐页翻还行,上百页就熬人了。我的办法是打缩略图:把多页拼到一张纸上,一屏看十几二十页。
大多数桌面 PDF 编辑器都有“每张纸打印多页”的功能,选 6×4 或者 4×4 的版式,打印成 PDF 而不是真打印。生成的缩略图文件一屏能看 16 到 24 页,靠页眉、表格标题、段落起止位置就能判断每页是什么内容。一百页的文件,三四屏扫完。
扫读的目标不是看懂内容,是找结构断点:哪里是新章节开头、哪里表格结束、哪里出现空白页、哪里开始重复。这些断点就是拆分的天然候选位置。找到断点之后,再回到原文件精确定位到具体页码。
这个办法的好处是把“逐页精读”降级成“整体扫读 + 局部精读”,时间成本能压到原来的三分之一。我现在的习惯是:任何超过 30 页的拆分任务,先出缩略图,扫完再动刀。
3.3 扫描件、空白页、旋转页的处理顺序
如果这份 PDF 是扫描件,情况会更麻烦,因为提取不到文本,核对只能靠眼睛。这时候处理顺序是:
- 先去除空白页。用编辑器的“删除空白页”功能,或者脚本判断页面文本长度和图像占比。空白页留着会打乱页码范围,也让接收方困惑。
- 再统一页面方向。把旋转页转到正确方向,避免拆完后两边显示不一致。
- 最后做 OCR(如果需要)。如果后续要靠编号做校验,扫描件必须先 OCR 出文本,否则编号核对只能人工。OCR 有识别错误的风险,编号里的 0 和 O、1 和 I 经常混淆,所以 OCR 出来的编号必须抽样人工复核,不能全信。
处理完这三步,扫描件才具备和电子文档一样的核对条件。跳过任何一步,后面都会以另一种形式找回来。
4. 核对文档编号:和外部台账做双向比对
4.1 编号连续性是第一道筛子
文件页锁死之后,开始核对编号。第一步看连续性:编号是不是按规则递增,中间有没有断。
如果编号是 001 到 060 连续排列,实际只找到 58 个,那就缺 2 个。缺的这两个可能是印刷时漏了,也可能是页面本身缺失。这个信息必须先搞清楚,因为它直接决定拆分后的两份文件是否完整。
反向也要查:如果编号最大到 060,但实际有 62 页,说明有 2 页重复编号或多编了号。重复编号在分组时会导致同一页被分到两组,或者两组都不认领。
这一步用表格软件的条件格式加计数就能搞定,不需要写代码。数据量大的话用脚本更快:
import re # 从对照表里读到的编号序列 doc_ids = ["A-001", "A-002", "A-003", "A-005"] # 示例,实际从表格读取 nums = [int(x.split("-")[1]) for x in doc_ids] full = set(range(min(nums), max(nums) + 1)) missing = sorted(full - set(nums)) dup = sorted({x for x in doc_ids if doc_ids.count(x) > 1}) print("缺号:", missing) print("重号:", dup)输出的缺号和重号,就是你必须先解决的疑点。
4.2 和外部台账做双向差集
文档里的编号只是一个侧面,还得和外部台账对。台账可能是报名表、工号表、房间分配表,总之是“应该有谁”的权威清单。
比对要做两个方向,只做一个方向会漏:
- 有档无人:文档里出现了某个编号,但台账里没有这个人。可能是台账漏登,也可能是文档里混进了旧版本的页。
- 有人无档:台账里有这个人,但文档里找不到对应页。可能是漏印、漏扫,或者这一页压根没生成。
doc_ids = {"A-001", "A-002", "A-003"} roster_ids = {"A-001", "A-002", "A-004"} print("有档无人:", sorted(doc_ids - roster_ids)) print("有人无档:", sorted(roster_ids - doc_ids))两个差集都为空,说明文档和台账完全对齐,可以放心拆。任意一个非空,就得先查清楚原因。我遇到过“有档无人”其实是台账里那个人退出了但没删行,也遇到过“有人无档”是打印机卡纸漏了一页。两种情况处理方式天差地别,不查清楚就拆,等于把问题打包发给别人。
4.3 一页多号和一号多页怎么处理
现实里不是所有文档都规整。两种特殊结构必须先定处理规则:
一页多号:一页上印了三个人的信息。这种情况不能按页切,必须整页复制给相关组,或者拆成单独的三份。选哪种取决于接收方需求:如果两组人都需要这页的完整信息,就复制;如果各自只要自己那部分,就得裁切内容。
一号多页:一个人的材料占了三页,比如登记表加两张附件。这三页必须整体归属同一组,绝不能切在中间。映射表里要把这三页标成同一个分组单元,切分时按单元切而不是按页切。
这两种情况如果没在表里显式标出来,切分脚本会当成普通页处理,结果就是半份材料发给了一个人。这是最容易被忽略、也最容易被投诉的错误。
5. 两种拆法对应两套核对逻辑
5.1 连续区间切分:一刀两段
最常见的情况是:前一段给甲组,后一段给乙组。切分点是一个页码,比如第 12 页之前归甲组,第 13 页起归乙组。
这类拆法的核对重点是切分点本身。要做三件事:
第一,确认切分点前后两页的归属没有歧义。翻到第 12 页和第 13 页,看内容是否真的分属两组。有些文档的章节边界在页中间,前半页属于上一章、后半页属于下一章,这种页无论归哪边都不完美,得提前和接收方确认怎么处理。
第二,确认没有跨组共用页。如果某一页两组都需要,这一页就必须复制到两份文件里,而不是二选一。复制意味着总数会多一页,后面做页数守恒校验时要把这个因素算进去。
第三,确认切分点处的连续性。第 12 页如果是半张表,第 13 页是这半张表的续表,切在这里等于把表劈开。这种情况要么前移切分点,要么后移,不能切在表格中间。
5.2 交错抽取:一页一个人,按组打包
另一种情况是:一人一页,人员按组分,需要把属于甲组的页抽出来拼成一份,乙组的拼成另一份。这时候页码不是连续的,是跳跃的。
核对逻辑完全不同。你不能看区间,只能看名单和页的对应关系是否完整覆盖。判断标准是:
- 每个被抽出来的页,都对应名单里的一个人,没有多抽
- 名单里的每个人,都在某一组里出现了一次,没有遗漏
- 两组的页数之和等于总页数(不考虑共用页的情况下)
这个场景下映射表的价值最大,因为页码完全不规律,靠脑子记根本不可能。抽页顺序也会影响结果:如果按名单顺序抽,出来的文件是按名单排的;如果按页码顺序抽,出来的文件是按原文档排的。这两种顺序对接收方的阅读体验差别很大,得提前定。
我一般按页码顺序抽。原因很简单:接收方如果拿着这份文件去和原始文档对照,页码顺序一致才好核对;按名单顺序排的话,对方翻到第 5 页,对应原文档第 37 页,沟通成本高。
5.3 工具选型:三种路径的取舍
| 路径 | 适合场景 | 优点 | 局限 |
|---|---|---|---|
| 图形界面编辑器 | 一两次、页数少、需要目视确认 | 直观,能边看边切 | 重复操作累,容易点错,无审计记录 |
| 命令行工具 | 固定区间、批量、可写进脚本 | 快,可复现,参数明确 | 需要记语法,出错提示不友好 |
| 脚本(Python) | 交错抽取、复杂规则、需要校验 | 逻辑可定制,能顺带做校验 | 要写代码,环境依赖 |
选择标准不是哪个高级,是这件事会不会重复做。只做一次,图形界面最快,别折腾。每周都要做,写脚本,第三次之后就开始省时间。我现在的分界线是:同一份文档结构如果一个月内出现两次以上,直接写脚本。
6. 拆分执行与拆后自查
6.1 命令行三条路,选一条用熟
固定区间切分,命令行最快。三种工具都能干:
# qpdf:. 表示沿用输入文件,后面跟页码范围 qpdf input.pdf --pages . 1-12 -- part-a.pdf qpdf input.pdf --pages . 13-40 -- part-b.pdf# pdftk:支持 end 关键字,不用先查总页数 pdftk input.pdf cat 1-12 output part-a.pdf pdftk input.pdf cat 13-end output part-b.pdf# mutool:poppler 系的写法 mutool merge -o part-a.pdf input.pdf 1-12三条命令效果一样,选你机器上已经装了的那个。qpdf 的--pages语法稍绕,但它对损坏文件的容错最好;pdftk 的end关键字很实用,省去先查页数;mutool 参数最简洁。
切完立刻看一眼文件大小和页数,异常小的文件通常是切错了页码范围:
qpdf --show-npages part-a.pdf qpdf --show-npages part-b.pdf两个数字加起来应该等于原文件页数(有共用页时大于原页数,这个要记在备注里)。
6.2 交错抽取用脚本,顺带把校验写进去
按名单抽页的场景,写个小脚本比手工点可靠得多:
from pypdf import PdfReader, PdfWriter def split_pick(src, dst, pages): """pages 是 1 起始的物理页码列表""" reader = PdfReader(src) total = len(reader.pages) writer = PdfWriter() for n in pages: if not 1 <= n <= total: raise ValueError(f"页码 {n} 超出范围 1-{total}") writer.add_page(reader.pages[n - 1]) with open(dst, "wb") as f: writer.write(f) return len(pages) n = split_pick("input.pdf", "part-a.pdf", [1, 3, 5, 7, 9]) print(f"甲组输出 {n} 页")脚本里那个范围校验别省。页码越界是交错抽取最常见的错误来源,通常是名单里多了一行或者编号从 0 开始,一眼看不出来,跑完才发现少页。让脚本直接抛异常,比事后对账省事。
6.3 拆完必须验:页数守恒、指纹比对、首尾抽样
切完不等于做完,三步验证:
页数守恒。拆分后各份页数之和,等于原文件页数,或者等于原页数加共用页数。不相等就说明有页漏切或重复切。
文本指纹比对。提取每页前 40 个字做指纹,检查拆分文件的指纹序列是不是原文件指纹序列的子序列,且顺序一致。这一步能抓出页码错位一页这种隐蔽错误。
from pypdf import PdfReader def fingerprints(path): r = PdfReader(path) out = [] for p in r.pages: t = " ".join((p.extract_text() or "").split()) out.append(t[:40]) return out src = fingerprints("input.pdf") a = fingerprints("part-a.pdf"). # 注意这里不加点 # 正确写法 a = fingerprints("part-a.pdf") # 检查 a 是不是 src 的连续子序列 def is_subseq(sub, full): for i in range(len(full) - len(sub) + 1): if full[i:i + len(sub)] == sub: return i + 1 # 返回起始物理页 return 0 pos = is_subseq(a, src) print("甲组对应原文件起始页:", pos if pos else "不匹配")如果pos返回 0,说明拆出来的内容和原文件对不上,可能是切错了页码,也可能是原文本身被改过。这时候必须停下来查,不能带着疑问发出去。
人工抽样看首尾。程序校验覆盖不了版式问题。每份文件翻三页:第一页、最后一页、中间随便一页。看什么?看页眉编号是否连续、看表格有没有被切断、看落款和盖章页在不在对应那份里。盖章页尤其容易切丢——如果章盖在两组文档的接缝处,切完两边都不完整。
7. 分发前的命名、体积与权限处理
7.1 文件名就是最省事的说明书
接收方拿到文件,第一眼看的是文件名。名字起得含糊,对方就得打开确认,一来一回浪费时间。我的命名规则是三段式:
组名_对应原文件页码范围_版本日期.pdf比如甲组_原1-12页_20240612.pdf。三个信息缺一不可:谁该看、对应原文哪几页、什么时候的版本。
第三段日期容易被忽略,但它是版本管理的核心。同一份文档可能改过好几版,两组人手上拿的版本不一致,对起数来能吵半天。带上日期,至少能确认双方是不是同一批。
如果文档里有“详见第 X 页”这类交叉引用,拆分后页码位移会导致引用失效。补救办法是在文件名或者首页加一行说明:“本册对应原文件第 1-12 页”,让对方换算时有依据。
7.2 体积控制:能不压就不压
拆分本身会减小体积,但扫描件即使拆开也可能超出附件限制。这时候要压,但压缩有代价。
扫描件的体积主要来自图片分辨率。300 dpi 压到 150 dpi,体积能降一半以上,文字还能看清;压到 100 dpi 以下,小号字就开始糊了。所以压缩前先确认接收方是打印还是屏幕看:打印就别压太狠,屏幕看可以适度降。
另外注意,压缩工具的不同选项对文字层的影响不一样。有些压缩会丢掉文本层,只留图片,导致对方搜不到关键词。如果这份文档后续还要被检索,压缩后必须验证文本层还在。
7.3 只读权限和留痕
拆开发给两组人,本质是控制信息的可见范围。除了切分本身,还可以叠加一层权限设置:设置为只读,禁止编辑和批注,防止接收方改动后二次转发时内容已经变了样。
设置权限这件事有个前提——接收方得能打开。加了打开密码的文件,对方打不开就会来问你要密码,反而增加沟通成本。我的做法是只设权限密码(限制编辑和打印),不设打开密码,这样对方能直接看,但改不了。
另外建议留一份分发记录:什么时候、发给了谁、发的是哪个页码范围、用的什么版本。不需要多正式,一个表格几行字就够。真出问题时,这份记录能帮你快速定位是哪一环出的错,而不是从头回忆。
8. 我反复踩到的几个坑
阅读器显示的页码不等于物理页。有些阅读器会把封面排除在页码计数外,显示“第 1 页”其实是物理第 2 页。判断依据永远是工具的页数查询,不是界面上那个数字。我第一次被退回就是因为这个,界面上写着 1,实际切的是物理 1,但文档编号从 2 开始。
拆完后书签和内部链接会错位。原文件如果有目录书签、内部跳转链接,拆分后这些引用不会自动重映射,点进去要么跳错页,要么直接失效。如果文档依赖目录导航,拆完得手工重建,或者干脆在文件名里注明“目录仅供参考”。
电子签章的文档拆完可能验签失败。如果原文件带电子签章或数字签名,任何页面的增删都会导致签名失效。这种文档要么整份发,要么在拆分前确认接收方不要求验签。扫描件上盖的实体章没这个问题,但电子签章一定有。
共用页只复制一次等于漏发。两组都需要的那一页,必须出现在两份文件里。如果按区间切,这一页只能落在其中一份,另一份就缺了。这个错误在核对页数守恒时看不出来——总数是对得上的,但有一组少了一页。所以共用页必须在映射表里单独标记,切的时候显式复制。
页码范围写成“起始页加页数”最容易差一页。工具的参数有的是“起-止”,有的是“起始页+数量”,混用必错。qpdf 和 pdftk 都是起止页码,13-40是第 13 页到第 40 页,共 28 页,不是 40 页。写命令前先在纸上算一遍页数,和工具输出对一下。
中文文件名在命令行下的编码问题。用命令行工具处理中文名的文件,某些环境下会报找不到文件。稳妥的做法是先把文件重命名成英文或数字,处理完再改回来,或者全程用脚本处理,让程序自己处理编码。
踩过这些之后,我现在的固定流程是:先出缩略图扫一遍确定结构断点,建映射表并跑重号跳号差集三项检查,按物理页切分,用指纹比对验一遍,抽样看首尾页,最后按三段式命名分发。整套下来,一份几十页的文档大概二十分钟,比返工一次划算得多。真要说有什么体会,就是那句老话——切分是动作,核对才是本事。页码这种东西看着最不起眼,恰恰是最容易出错的地方。