简介面向经常需要整理PDF合同的办公人员、处理电子书和学习资料的学生以及需要批量管理文档的行政人员这份工具包提供PDFPatcher 0.3.4.101的完整可用版本核心解决多个PDF合并和自动生成书签两大高频需求。合并时能够保留各原始文件中的书签层级并自动整合避免合并后目录丢失对于没有书签的长篇文档还可手动添加或编辑书签使几十上百页的资料也能快速跳转。包内共10个文件以exe主程序为主体配套dll运行库确保功能完整txt文件为更新历史和授权说明xml/xsd提供配置模板另附PDF版使用手册整体压缩仅6.96MB便于随手存放和拷贝使用。目前已有3938人学习下载适合作为常备PDF辅助工具。借助这套资源用户无需联网即可完成PDF合并、书签整理甚至后续再拆分、转换等衍生操作配合手册与配置说明能较快上手并建立适合自身的文档管理流程。1. 每次都要手工加几百个书签PDF合并和自动生成书签为什么值得自己搞一套做开发这些年「合并几个PDF」可能是被问得最多的办公类需求。真正让我下决心自己写一套工具的是有一次接到五份文档加起来四百多页要求合成一份带目录的完整资料。手动合并倒不难难的是给这四百页逐个加书签每个章节标题要对应到页码还要分出一级、二级的层级关系坐在电脑前一页一页翻了一个多小时眼睛都快花了。当时的体会是市面上的PDF合并工具并不少但能支持「合并后自动生成书签」的要么是付费模块要么生成的书签只有扁平的一层直接把文件名当成书签标题完全没法区分章和节。后来我把目光转向 Python 生态发现几行代码配合规范的文件命名就能同时解决 PDF 合并、书签层级、页码定位这三件事。这篇笔记就把这套方案的选型思路、具体代码和踩过的坑完整写出来给需要批量处理 PDF 的开发者、文档工程师和经常整理资料的从业者一个能直接复用的落地方案。这个方向的核心价值在于只要输入文件命名有规律合并结果就能自动带有结构化书签以后打开文档想跳到哪里只需点一下书签不用再靠记忆和滚动条找位置。2. PDF合并为何不能直接拼接文件对象树、交叉引用表与书签的真实结构2.1 PDF 的对象模型合并的本质是重建交叉引用在动手写代码之前先把 PDF 的文件结构讲清楚。很多人第一次尝试合并 PDF会直接想到把两个文件按二进制拼在一起用cat a.pdf b.pdf c.pdf之类的命令处理。结果打开一看第二个文件完全不可读甚至整个文件都损坏了。这不是巧合而是因为 PDF 的存储结构根本不允许这样做。一个 PDF 文件由若干对象组成包括目录对象/Root、页面树对象/Pages、页面对象/Page、内容流、字体资源、图片资源等等。这些对象分散在文件的不同位置文件尾部有一个交叉引用表xref table记录每个对象的偏移量还有一份尾部字典trailer指明根目录在哪。阅读器打开 PDF 时先读交叉引用表再顺着偏移找到各个对象最后按页面树的顺序渲染页面。单纯的字节拼接会带来两个致命问题第一交叉引用表中记录的偏移量是基于原文件位置的拼接后第二个文件的偏移量整体失效第二两个文件都各自声明了一个根目录阅读器不知道该以谁为准。所以正确的合并方式是把两个文件的页面对象导入到一个全新的页面树里并重新生成整个交叉引用表。这就是为什么我们绕不开 pypdf、pikepdf 这类库本质上它们是在做对象迁移和数据重写而不是「把文件头尾相接」。另外还有一层关键信息藏在文件的 trailer 里。每个 PDF 都有自己的文件标识符/ID部分对象还被标记了唯一编号。合并时如果两边的标识符冲突有些阅读器会认为文件被改动过而弹窗提示。pypdf 在写入新文件时通常会重新生成标识符这能避免大部分问题但也意味着合并后的文件去掉了原文件的指纹信息。如果业务流程需要保留原始文件来源的元数据就得额外做一层处理。2.2 pypdf 与 pikepdf 怎么选一个合并需求该用谁选型阶段主要比较的是 pypdf 和 pikepdf 这两个库这也是目前处理 PDF 合并加书签最常用的两条路。pypdf 的前身是 PyPDF2后来由于维护问题分裂重组为 pypdf特点是纯 Python 实现安装简单API 容易上手书签操作有现成的add_outline_item方法合并几十个文件轻轻松松。如果只是做「合并 加书签」这种常规任务pypdf 是首选。pikepdf 则基于 C 库 qpdf处理复杂 PDF 的能力更强对损坏文件的修复能力、对加密文件的支持都要好一些。但它的 API 风格更偏底层操作书签时要自己处理 PDF 对象树不适合快速写脚本的场景。对比项pypdfpikepdf依赖环境纯 Python跨平台需编译安装但安装包通常齐全合并多个 PDF直接支持 append需手动操作页面对象添加书签 APIadd_outline_item简单直接底层操作 Outlines 对象树处理加密 PDF支持但操作稍慢支持性能更优中文书签兼容新版已稳定稳定适用场景快速脚本、批量合并、自动书签复杂 PDF 修复、精细对象级操作我的建议很明确绝大部分「合并 PDF 并生成书签」的需求pypdf 就够用了。只有当单个 PDF 文件特别大比如超过 500MB或者文件本身有轻微结构损坏导致 pypdf 读取失败时再考虑 pikepdf 做前置修复。这样做既避免了过度设计也降低了脚本的依赖复杂度。2.3 书签的存储结构Outline 树与「自动生成」能借助的规律书签在 PDF 内部叫大纲Outline是一棵挂在文档目录下的树形结构。树根是一个 /Outlines 对象下面挂若干大纲条目每个条目都可以有自己的下一级条目形成层级关系。每个条目包含标题文本/Title和一个指向目标页面的信息比如 /Dest 直接指定页面或者通过 /A 执行跳转动作。理解这个结构对「自动生成书签」很重要因为这意味着 PDF 能表达的书签层级可以很丰富。一级条目对应章节二级条目对应小节三级对应更细的分段阅读器展开大纲时就是一棵完整的目录树。手动用 Adobe 或 WPS 给 PDF 加书签本质上也是在编辑这棵大纲树。既然底层结构是这样那自动生成书签的落地思路也就清晰了我们在合并多个 PDF 的时候并不需要一个一个去手工定位页码而是按照每个输入文件在合并结果中的首页位置来创建书签条目。每个输入文件对应一级书签文件名就是书签标题如果文件名本身带有编号或层级信息甚至可以解析出二级、三级结构让最终文档的目录树更接近原书。举例来说如果输入文件夹里有这几个文件01_第1章_背景.pdf、02_第2章_方案.pdf、02_第2章_第1节_设计.pdf。我就可以通过文件名中的「章」「节」等关键词把书签组织成「第2章」下挂着「设计」的层级结构。这就是自动生成书签最实用的做法不在 PDF 内部做全文语义识别而是把文件命名规则作为一种约定靠正则表达式解析出结构来。3. 用 pypdf 实现 PDF 合并最小脚本与三个关键参数3.1 合并并自动生成书签的最小脚本先给出一段可直接运行的最小脚本。这段代码做的事是读取 input 目录下所有 PDF按文件名排序后逐个追加到新文件每追加一个文件就在输出文件中加一条书签书签标题取自文件名定位到该文件在合并结果中的首页。把这个脚本保存为merge_pdfs.py在input目录放好 PDF 文件后运行python merge_pdfs.py输出就是merged.pdf打开后左侧书签已经生成好了。from pypdf import PdfWriter, PdfReader import glob import os import re pdf_files sorted(glob.glob(input/*.pdf)) if not pdf_files: raise SystemExit(input 目录下没有找到 PDF 文件) writer PdfWriter() page_offset 0 # 当前合并结果的总页数也是下一个文件首页的页码 for f in pdf_files: reader PdfReader(f) # import_outlineFalse 表示不把原 PDF 自带书签带进来 writer.append(reader, import_outlineFalse) base os.path.splitext(os.path.basename(f))[0] # 去掉文件名前缀的序号例如 01-需求文档.pdf - 需求文档 title re.sub(r^\d[_\-\s], , base).strip() if title: writer.add_outline_item(title, page_offset) page_offset len(reader.pages) with open(merged.pdf, wb) as fout: writer.write(fout) print(f合并完成共 {page_offset} 页书签 {len(writer.get_outline() or [])} 条)这段代码里有几个点需要展开说明。page_offset是整个脚本最核心的状态变量它从 0 开始每处理完一个文件就累加该文件的页数。这样当下一个文件追加进来时page_offset正好指向它首页的索引也就是书签应该指向的位置。这里用「页索引」而不是「页码」是 pypdf 的 API 约定第 5 章会专门讲这个坑。import_outlineFalse这个参数容易被忽略但很重要。pypdf 的append方法默认会把源文件自带的书签一并导入输出。如果输入文件本身有书签最终就会出现「原书签 新书签」混在一起的情况目录变得又长又乱。我在批量合并电子书时吃过一次亏当时输出的文档有一千多条书签几乎全是重复的章节名。writer.get_outline()拿到的是大纲树的列表表示用来统计书签数量很直观。如果输出结果是空列表说明书签没有写进去这时候需要检查add_outline_item是否被调用、标题字符串是否为空。调试时可以在循环里加print(title, page_offset)就能很快定位是哪一步出了问题。3.2 append 参数、页面顺序与元数据取舍pypdf 的append方法并不只是「把页面加进去」这么简单它还提供了两个对合并体验影响极大的参数。第一个是import_outline刚才提到过控制是否带入源书签第二个是outline_item可以在追加时顺手为这个文件添加一个书签条目。outline_item的用法是writer.append(reader, outline_item需求文档)效果等同于追加之后调用一次add_outline_item。但它的局限在于只能创建一级书签无法处理嵌套层级而且每次追加都要重新构造这个参数。对于「每个文件对应一条顶层书签」的场景用outline_item写起来更简洁但一旦要生成多级书签还是得回到手动调用add_outline_item的方式因为那里可以传parent参数。页面顺序方面append按调用顺序依次把页面放进同一个页面树顺序完全可控。要注意的是PdfWriter也可以先创建空白写入器再用add_page手动加页面这种用法适合需要穿插空白页或调整顺序的复杂场景但日常合并用不到那么细。关于元数据pypdf 写入新文件时会自动带上写入器的元数据通常是空白的。如果希望输出文件保留标题、作者、主题等信息需要把这些信息从第一个输入文件或指定文件里提取出来赋值给writer.metadata。打开 PDF 的文档属性里面通常能看到这些字段writer.metadata reader.metadata # 把最后一个文件的元数据作为输出元数据这段代码要放在for循环之后因为循环结束时reader指向的是最后一个文件。如果想让元数据来自第一个文件就得单独保存一份第一文件的reader引用。实际工作中文档属性里的「作者」「标题」对阅读器默认显示名有影响值得保留一份。3.3 批量合并目录下的所有 PDF命名策略决定书签质量自动书签的质量很大程度上取决于输入文件的命名是否规律。如果输入文件叫扫描件.pdf、文档最终版2.pdf、未命名.pdf那自动生成的书签就是一堆毫无意义的标题用户拿到手还是要手动改。我一般会在交付脚本时附上一份命名建议让使用者按固定格式准备文件。常用格式是序号 分隔符 章节标题例如01_开题报告.pdf、02_需求分析.pdf。sorted()对这个格式排序最稳定即使文件名里混合了汉字和数字只要前缀序号位数一致排序就不会乱。如果不加数字前缀按字符串排序时第10章会排在第2章前面因为字母序比较的是字符而不是数值这是「按文件名排序」最常见的坑。glob 模式input/*.pdf不会匹配子目录里的文件。如果输入文件在不同子目录下比如每个章节一个文件夹更稳妥的做法是用glob.glob(input/**/*.pdf, recursiveTrue)。但要注意recursiveTrue模式返回的路径包含目录名此时书签标题应基于文件名排序可以借助sorted()的参数按完整路径排序。如果文件夹很多又必须保持目录层级可以把子目录名作为书签的父级标题先把同一目录下的 PDF 合为一组再给整个目录加一条父书签。这种做法能让最终目录树跟原来的文件夹结构保持一致适合整理课程讲义、项目交付文档这类场景。4. 自动生成 PDF 书签从文件名单到章节层次4.1 文件名解析正则提取章节标题与层级树既然自动生成书签的核心是把文件名转成书签标题那文件名解析规则就值得单独拿出一节来讲清楚。我的做法是定义一个解析函数输入文件名输出书签标题和层级深度两级信息。层级深度的作用是判断这条书签挂在树的哪一层一级是章二级是节。先把输入文件名做三件事去掉扩展名、去掉序号前缀、按分隔符切分。分隔符常见的有_、-、空格偶尔还有全角字符。为兼容各种来源的命名习惯我会用正则先把这些分隔符统一替换为_再按_拆分。import re def parse_filename_to_bookmark(filename): name os.path.splitext(filename)[0] # 统一分隔符把 - 和空格替换为 _ name re.sub(r[\-\s], _, name) parts [p for p in name.split(_) if p.strip()] if len(parts) 2: # 第一个通常是序号后续部分是标题 seq parts[0] title .join(parts[1:]) # 判断标题里是否含“章”“节”等层级关键词 if re.search(r第[一二三四五六七八九十0-9]章, title): level 0 elif re.search(r第[一二三四五六七八九十0-9]节, title): level 1 else: level 0 return seq, title, level # 退化情况文件名叫“附录A.pdf” return , name, 0解析规则中有一个关键点层级判断不能只看标题里有没有「章」字。实际会遇到的文件命名五花八门比如02_第2章_方案设计_3.1节_细节.pdf这里面既有「章」又有「节」如果只匹配第一个关键词就会错误地把它当成一级书签。更稳妥的思路是按最后一个关键词判断或者直接约定标题里出现「节」「条」「点」就是二级否则是一级。这套解析规则的边界在于它只能处理用户已经按规范命名的文件。如果文件名完全没有结构比如就叫part2.pdf那解析后的标题就是part2层级是 0。对于这种文件我通常会在脚本运行前打印出解析结果让用户确认书签是否符合预期毕竟拼写错误的标题比没有书签更让人头疼。4.2 从 PDF 文本内容生成书签正文识别逻辑与代码节选跳开文件名另一种「自动生成书签」的思路是直接分析 PDF 正文内容。比如一本电子书本身没有书签但每章首页有大号字体写着「第 1 章 xxx」我们就可以提取页面文本识别出这些标题再在对应页生成书签。文本识别的基本流程是用 pypdf 的extract_text()提取每一页的文本逐行找符合章节标题特征的行。这个方案只对原生文本型 PDF 有效扫描件没有文本层提取结果是空字符串必须先做 OCR 识别而 OCR 的速度和准确率又是另一套坑。from pypdf import PdfReader import re def detect_chapter_pages(pdf_path): reader PdfReader(pdf_path) hits [] for idx, page in enumerate(reader.pages): text page.extract_text() or # 按行切分避免同一行里混入正文内容 lines [ln.strip() for ln in text.splitlines() if ln.strip()] for line in lines[:5]: # 只看页面前几行降低误匹配概率 if re.match(r^第[一二三四五六七八九十百0-9][章卷], line): hits.append((idx, line)) break return hits局限要提前说清楚。extract_text()的文本顺序并不总等同于视觉顺序取决于 PDF 的内容流如何组织。有的 PDF 在排版时把页眉、页脚、正文分成了多个文本块extract_text()返回的顺序可能是页眉在正文之后这时候按行匹配会漏掉真正的章节标题。缓解办法是把匹配窗口扩大到前 10 行或者用小字号过滤条件排除标题字体极小的情况。识别到章节页之后生成书签的逻辑就和文件名方案统一了用识别到的标题文本作为书签标题用该页在 reader 中的索引作为页码。这里又回归到最核心的公式——add_outline_item(title, page_index)。文本识别方案的优点是输出书签和正文完全对应缺点是对扫描版 PDF 无能为力且不同 PDF 的排版差异可能导致识别结果不稳定。我的经验是对于排版规整的电子书正文识别效果很好对于公司内部扫描签批的文档老老实实靠文件名更可靠。4.3 多级书签的构建先建父节点再挂子节点自动生成书签很容易被人忽略的一点是层级问题。实际业务中一份交付文档可能有「第 1 章」「第 1 章 1.1」「第 1 章 1.2」这样的结构。把所有标题平铺成一条条一级书签虽然跳转功能能用但阅读器左侧的大纲展开就是一条长列表可读性极差。多级书签的构建逻辑并不复杂核心是先创建父级书签拿到它返回的父节点引用再把它传给子级书签。在 pypdf 里add_outline_item的返回值就是该条目的引用而parent参数正好接收这个引用。这里用一个简化示例说明结构。假设要生成两级书签章级和节级章是父节是子。chapter_item writer.add_outline_item(第1章 概述, 0) # 返回父节点 writer.add_outline_item(1.1 动机, 1, parentchapter_item) writer.add_outline_item(1.2 目标, 3, parentchapter_item)代码里值得注意的执行顺序问题父节点必须先创建子节点才能引用到它。如果把顺序写反parent参数拿不到有效引用子书签会被挂到大纲树的顶层层级关系就乱了。更完整的做法是维护一个「当前章节」变量每解析到一条章级书签就更新它之后解析到的节级书签都用这个变量作为 parent。文件名方案和正文识别方案都适用这套逻辑先按文件顺序循环构建一级书签再根据文件名里的编号规则动态处理二级书签。此外多级书签的页码设置有一个隐含约束父书签的页码应当是子书签所在范围的起始页。比如第 1 章从第 0 页开始那么子书签的页码不能小于 0且应该落在这一章页码范围之内。脚本里通常不会做这个校验但人工验收时要注意这里如果发现子书签跳到了别的章节回去检查页码计算逻辑八成是页码偏移没有累加。5. 避坑合并与书签生成最常见的 5 个坑5.1 书签页码偏移add_outline_item 的页索引从 0 开始现象合并生成的 PDF打开后第一本书签指向的不是首页而是封面页或者第二页有的甚至指向了上一章内容的最后一页。原因add_outline_item的page_number参数用的是相对于整个文档的页索引索引从 0 开始计数。而你在阅读器里看到的页码是从 1 开始的。如果脚本里直接把「第 3 页」这样的概念当成参数传进去就会产生一页的系统性偏移。解决写代码时统一用页索引不要用「页码」。在前面最小脚本里page_offset累加的是len(reader.pages)天然就是索引。想验证结果对不对可以读回输出文件的大纲树打印出每条书签的页码再从阅读器对比看是不是偏移了一页。如果整体差一页就在加书签时把页码减 1如果是逐条偏移说明是累加逻辑出了问题需要检查page_offset len(reader.pages)是否写在了正确的位置。5.2 加密 PDF 导致合并失败先解密还是先报错现象脚本在跑第二个 PDF 时抛出FileNotDecryptedError程序直接终止排查后发现那个 PDF 设置了打开密码。原因pypdf 的PdfReader默认不自动解密。任何一个被加密的 PDF 都含有加密字典未解密时页面树不可读取len(reader.pages)就报错。有的 PDF 只设置了权限密码打开时不提示但限制复制打印这种情况下 pypdf 不报错因为对象仍可读取。解决判断逻辑放在PdfReader创建之后。先看reader.is_encrypted为真时调用decrypt。空密码可以解开仅含权限密码的 PDF打开密码则需要用户输入。reader PdfReader(f) if reader.is_encrypted: st reader.decrypt(password) if st 0: print(f无法解密{f}请先单独处理再合并) continuedecrypt返回状态码0 表示失败1 表示用打开密码解密成功2 表示用权限密码解密成功。注意权限密码解密只能读取不能修改合并时写入新文件可能仍保留加密属性。应对办法是合并前在专业软件里手工去除密码保护或者要求输入文件提前做解密处理。5.3 中文书签乱码编码问题导致标题显示成乱码现象书签能跳转但标题显示为「测试」或一堆黑框方块看起来完全不可读。原因PDF 内部对文本的编码处理比普通文本复杂得多。如果生成的代码用的是旧版 PyPDF2 或 PyPDF 早期版本中文字符串没有按 PDF 标准的 Unicode 编码写入阅读器就会按系统默认编码强行解析渲染出乱码。另一种情况是 PDF 文件本身缺少对应字体阅读器无法显示罕见字符。解决优先使用 pypdf 的最新版本它在 Python 3 下生成书签时会把字符串编码为 PDF 内部的标准字符串类型。关键是确保传入add_outline_item的标题是普通 Pythonstr不要提前自己编码成 bytes 或 latin-1。如果升级后还乱码把标题转成 UTF-8 字符串再写入title title.encode(utf-8).decode(utf-8)这条代码看起来没用但能提醒我们排查方向检查传参的字符串类型。另一个排查点是输入文件名本身可能就是乱码比如从某些压缩包解压出来的文件名是 GBK 被错误解码了这时书签当然也是乱码。先把文件名打印出来看一眼比在 PDF 侧排查更快。5.4 合并后文件体积异常增大对象没有被完全复用现象合并三个原本总共 20MB 的 PDF输出文件变得到了 60MB甚至更大。原因PDF 合并时每个页面的对象被复制到新文档字体、图片、资源字典等共享对象并不一定能自动去重。三个 PDF 里的同一款字体在合并结果里可能出现三份副本。如果文件以图片为主这种情况会让体积成倍暴涨。解决在可接受范围内先判断原 PDF 中是否已经有大量冗余资源。若文档是扫描件图片本身就是每页独立的合并后体积增加属于正常。想压缩体积常见做法是把合并结果再走一遍压缩工具比如用命令行的 Ghostscript 做瘦身/ebook或/screen预设能大幅减小体积代价是图片清晰度下降。如果合并后不涉及印刷要求用/ebook预设通常是性价比最高的方式。体积问题还有一个隐蔽来源是每次打开写入器都有残留对象。我建议在循环开始前初始化 writer循环内不反复创建新的PdfWriter合并结束后只调用一次write这样能减少一部分重复对象的写入。5.5 页面旋转和尺寸不一致合并后排版变乱的真相现象合并后的文档有些页面是横版有些是竖版阅读器里看起来方向不一致打印出来更乱。原因每个 PDF 的页面对象可能带有/Rotate属性值为 0、90、180、270 度。阅读器在渲染时会根据这个属性自动旋转页面。合并操作本身不会统一这个属性所以混有不同旋转属性的页面在最终文档里就表现为方向错乱。解决合并前先统一页面方向。pypdf 的页面对象读取/Rotate字段对该页处理角度差。如果业务要求强制统一为竖版则横向页面要旋转为纵向反之亦然。注意这里的旋转不会改变页面内容坐标只是改旋转属性page reader.pages[i] rotate page.get(/Rotate, 0) if rotate not in (0, 90): page.rotate(-rotate) # 按需调整在合并循环里按需加上这段后再writer.append。页面尺寸不一致的问题则没法在合并时自动解决只能靠阅读器打印设置调整或者统一导出为固定纸张大小再合并。如果是内部资料不要求打印这一步只要能接受阅读器适应页面即可不用过度处理。6. 进阶把合并脚本做成命令行工具并在输出前用代码自检书签脚本迭代到第三版之后我把它封装成了一个带命令行参数的小工具方便非技术同事直接使用也让自己在重复执行时少敲两行命令。import argparse parser argparse.ArgumentParser(description合并 PDF 并自动生成书签) parser.add_argument(--input, defaultinput, help读取哪个目录下的 PDF) parser.add_argument(--output, defaultmerged.pdf, help输出文件名) parser.add_argument(--sort, defaultname, choices[name, mtime], help排序方式按文件名或修改时间) args parser.parse_args()这里--sort用修改时间排序适合某些业务场景要求按文件生成顺序合并而不是文件名顺序。比如扫描仪的导出文件名往往是数字序号但真正的章节顺序和文件名顺序不一致此时按修改时间排就能还原扫描顺序。做完合并和书签我会习惯性地再读一次输出文件把所有书签标题和页码打印出来做自检。先看多少条书签再抽几条核对是否落在正确页面。from pypdf import PdfReader reader PdfReader(merged.pdf) outline reader.get_outline() for node in outline: if isinstance(node, list): print(子书签列表展开项数, len(node)) else: page_no reader.get_destination_page_number(node) 1 print(f{node.title} - 第 {page_no} 页)这段自检脚本能帮我发现三种问题书签总数与输入文件数不一致、页码明显偏移、层级嵌套丢失。每次执行合并前我也会顺带打印输入文件列表和解析出来的书签标题确认命名解析没有出错。这个习惯帮我少踩了很多坑尤其是文件名里偶发出现的全角括号、特殊空格都可能让正则匹配落空。这套方案的最终形态是一个 Python 脚本 一份命名规范说明。同事按规范命名文件脚本自动合并、自动生成层级书签输出前自检书签完整性。整个过程唯一需要人参与的就是检查输出文件的书签树是否符合预期。至于那些没有规范命名、需要人工整理的特殊文件我会单独建一个「手动处理」目录不混进自动流程。希望这篇笔记能帮你把「合并 PDF 加书签」这件小事彻底自动化省下来的时间去干更有价值的事。本文还有配套的精品资源点击获取