☰
批量统计PDF页数:从页面树原理到脚本与工具实战指南
2026/10/1 5:43:27 网站建设 项目流程

简介:这是一款面向 Windows 用户的 PDF 页码计数小工具,通过指定 PDF 所在文件夹或单个文件路径,即可快速得到对应文件的页码数,适合办公文档管理、电子资料整理、打印装订前页数核对等场景,也适合不熟悉编程的普通用户直接使用,省去逐本打开 PDF 查看页数的重复操作。整个 rar 压缩包约 8.69MB,共 5 个文件:包含可直接运行的 exe 工具、Python 源码 py 文件、docx 使用说明,以及 2 个 pdf 测试样本。exe 文件便于零基础读者直接运行,py 脚本展示页码获取逻辑,docx 是图文操作说明,测试样本则可快速验证工具输出是否正确。已有 508 人下载学习。使用说明对路径指定、运行方式和注意事项做了清晰交代,测试文件也能帮助确认批量统计结果是否准确;开发者可从源码出发做二次扩展,普通用户则可把它当作免安装的日常小工具,实用性与学习价值兼备。

1. pdf页码计数工具到底解决什么问题:打印计费与文档清点场景

如果你整理过一整个项目文件夹的PDF文档,或者帮打印店统计过待打印文件的总页数,你大概体会过那种“一个个打开PDF、盯着底部看页码、再手动记到表格里”的滋味。pdf页码计数工具要解决的正是这个问题:批量读取一批PDF文件,在不需要打开阅读器的情况下,把每个文件的页数、文件大小、有时连页面尺寸一起统计出来,最后落到一张表里。它最典型的用户是设计公司前台、出版社校对、档案管理员和做知识付费的课程打包者——这些人手里动辄几百个PDF,页数关系到打印成本、翻译报价和工作量估算,数错一页就是真金白银。

常见的pdf阅读器、pdf转word工具虽然都显示页数,但“批量”二字才是这个工具的命门。好消息是,这个需求不需要什么重装备,一条命令行或一个几十行的Python脚本就能干得很稳。这篇文章会把背后的计数原理、三条可落地的实现路径、批量场景下的完整脚本,以及我踩过的几个坑一次讲完。

2. 页码藏在PDF的哪个位置:先从文件结构看计数原理

2.1 为什么不能用正则数“/Type /Page”

很多人第一次写页码统计脚本时,脑子里蹦出来的方案是:把PDF当文本文件读,搜里面出现了多少个/Type /Page,数出来不就是页数吗?这个想法在十年前还有市场,现在基本一数一个错。

PDF文件确实会包含/Type /Page这样的对象声明,但页面的组织方式不是“铺平”的,而是一棵树。文件开头是/Catalog(目录对象),它指向/Pages(页面树根节点),页面树根节点下挂着若干/Page对象或下一级/Pages节点。问题在于:一个页面可能同时出现在多个父节点的引用里,某些生成器还会把页面属性写成继承形式,导致子页面对象里根本没有完整的/Type /Page声明。更拧巴的是,有些PDF做线性化优化时,同样的页面树结构会在文件里出现两次,一份给阅读器顺序加载用,一份给随机访问用。正则一搜,页数直接翻倍。

还有个更隐蔽的坑:PDF里允许存在“孤儿对象”——被删除了但没清干净的残留声明。它们不参与任何页面树引用,但文本层面依然存在。所以用文本搜索的方式统计页码,本质上是把PDF当黑匣子猜,属于典型的玄学做法。真正稳妥的做法是解析页面树结构,顺着/Root找到/Pages,再累加各页面节点的数量。

2.2 最快的一条命令:pdfinfo 怎么读 /Count

如果你只是想快速知道一个PDF有几页,不需要写代码。安装poppler-utils后,用pdfinfo这个命令行工具就能拿到结果。它在Linux、macOS下是系统仓库里的标准包,Windows下可以通过MSYS2或直接下载poppler的Windows构建版得到。

pdfinfo document.pdf | grep "^Pages"

这条命令的背后逻辑是:pdfinfo会解析PDF的交叉引用表和页面树,最终在输出里给出Pages: 12这样一行。grep "^Pages"是只把页码这一行捞出来,避免被一堆字体、元数据信息淹没。

参数说明:pdfinfo本身支持追加-f和-l参数来指定首页和末页,但对于统计总页数,直接运行然后过滤输出就够了。如果文件带密码,需要加-upw参数传入用户密码,比如pdfinfo -upw "123456" document.pdf。这个命令我一般配合find做批量统计,后面第三章会展开。它读的是页面树里的/Count字段或通过遍历/Kids数组得到的结果,不依赖任何渲染引擎,所以速度极快,几百页的文件也是毫秒级返回。

2.3 用Python读Pages树要比数Page对象稳得多

pdfinfo虽好,但它是C语言写的二进制工具,想在脚本里拿到结构化结果还得走子进程解析。如果本身就在写Python脚本,更顺手的做法是用pypdf(老项目叫PyPDF2,2023年后主推pypdf)直接解析页面树。

from pypdf import PdfReader reader = PdfReader("document.pdf") page_count = len(reader.pages) print(f"总页数: {page_count}")

这段代码只有三行,但你不能小看len(reader.pages)这行背后的工作量。PdfReader.pages属性在首次访问时会从/Root出发,沿着/Pages树递归解析所有/Kids节点,把全部页面对象收集到列表里,然后返回列表长度。这个过程处理了间接引用跳转、继承属性合并等复杂情况,比你手写递归省心得多。

参数说明:PdfReader构造时还可以传一个strict参数,默认是True,遇到轻微结构异常会抛异常;如果你只是想统计页数,对文件内容本身不关心,可以设strict=False来容忍一些损坏的PDF,让流程不至于被单个坏文件打断。另一个常用参数是password,加密PDF用PdfReader("encrypted.pdf", password="123456")就能解开。至于为什么说len(reader.pages)比数/Type /Page可靠——因为它是按PDF规范定义的页面树语义来走的,而不是按文本碰运气,这就是“解析”和“搜索”的本质区别。

3. 三条落地路径对比:pdfinfo、PyPDF2与现成exe工具该选哪个

3.1 先亮结论:按场景选型而不是按名气

面对“批量统计PDF页数”这个需求,从业者圈子里流传的方案基本就三类:poppler提供的pdfinfo命令行、Python的pypdf库、以及网上流传的各种绿色exe小工具(也就是标题里那个rar包大概率装的东西)。很多人一上来就找exe,觉得双击就能用;也有人坚持“能写脚本绝不装软件”。我的建议是先看场景,再选工具。

对比维度pdfinfo(poppler)pypdf/PyPDF2rar里的现成exe
安装成本需下载poppler或走系统包管理器pip install pypdf解压即用,但有杀软误报风险
批量处理配合find/xargs很强灵活,可导出Excel取决于工具作者是否做了批量
加密文件支持支持-upw传密码支持password参数多数不支持
损坏文件容忍度部分容错strict=False可跳过差异大,老工具容易闪退
结果可信度按页面树解析,稳定按页面树解析,稳定个别工具走渲染路线,可能虚报
适合场景服务器、自动化脚本需要二次加工数据临时救急、对方电脑没环境

这条表里最值得注意的是最后一行和“加密文件支持”那一行。我见过太多人拿着一个从rar里解压出来的老工具去统计加密PDF,结果要么报错要么干脆没反应,最后还反过头来怀疑PDF文件有问题。其实不是文件坏了,是工具太老,不支持带权限密码的文档。

3.2 路径A:pdfinfo命令行,适合批量与脚本化

pdfinfo最大的优势是它不会说谎。它不渲染页面、不加载字体,只是纯解析页面树然后把结果打出来,所以跑几百个文件都不会卡。批量场景下,我一般这样用:

find /data/pdf_files -name "*.pdf" -type f -exec pdfinfo {} \; | grep -E "^(Pages|File):"

这里的find负责递归找出所有PDF文件,-exec pdfinfo {} \;对每个文件执行一次统计,grep -E "^(Pages|File):"把文件名和页数成对捞出来。存在的问题是输出顺序被打散了,两个字段隔着好几行,肉眼不好对齐。所以实战里我会用循环加echo的方式:

for f in /data/pdf_files/*.pdf; do pages=$(pdfinfo "$f" | awk '/^Pages/{print $2}') echo "$f: $pages 页" done

参数说明:awk '/^Pages/{print $2}'是取Pages:这一行的第二列,也就是纯数字。for循环遍历的是当前目录下的直接文件,如果有子目录需要递归,改成find /data/pdf_files -name "*.pdf" -print0 | while IFS= read -r -d '' f,这样还能避免文件名带空格导致的解析错乱。这种方式输出的是路径: 页数格式,可以直接重定向到文本文件存档,也可以交给后续脚本处理。

3.3 路径B:pypdf,适合需要导出Excel和二次处理

pdfinfo适合纯统计,但如果你还要按页数分组、算总价、过滤异常值,Python的优势就出来了。pypdf在批量场景下的正确打开方式是先收集再统一落盘:

import os from pypdf import PdfReader def count_pdf_pages(path, password=None): reader = PdfReader(path, password=password) return len(reader.pages) pdf_dir = r"D:\work\待统计PDF" results = [] for filename in os.listdir(pdf_dir): if not filename.lower().endswith(".pdf"): continue full_path = os.path.join(pdf_dir, filename) try: pages = count_pdf_pages(full_path) results.append((filename, pages)) print(f"{filename}: {pages} 页") except Exception as e: print(f"{filename}: 统计失败 -> {e}")

参数说明:password传None表示不处理加密文件;文件路径里有中文时,Python在Windows下建议用pathlib.Path替代os.path来避免编码问题。os.listdir只扫一层目录,需要递归的话换成os.walk。strict参数我在上面提过,实际写脚本时一般不会显式传,而是用try/except兜住坏文件——这样单个文件损坏不会让整个批量任务停下来,这也是Python方案比很多exe工具强的地方。

3.4 路径C:rar包里的现成exe,什么时候该用、什么时候该扔

再回来说说标题里的rar包。这类压缩包里通常装着一个几十KB或几百KB的exe,常是用易语言或Delphi写的,界面简陋,功能就是拖入PDF文件显示页数。它的定位是给非技术人员救急用的:从qq群里收到一个rar,解压双击,拖文件进去,看结果,完事。如果你只是要数三五份文件,它确实比配环境快。

但这类工具的翻车点也很集中。第一,对新版PDF标准支持差,遇到带/StructTreeRoot或对象流压缩的文件可能直接闪退;第二,统计大文件时如果走的是渲染路线,几百页的文件能卡到人心态崩;第三,因为加壳方式问题,相当一部分会被杀软报毒。我见过不止一个同事把这类工具装进公司电脑,结果被安全软件隔离后整个rar包都不能用了。所以我的习惯是:exe工具只留一份在U盘里应急,凡是超过20个文件的统计任务一律用pdfinfo或Python。你手上那个rar,先解压看看里面工具的“关于”页——如果连PDF版本都没提,建议只把它当备用方案。

4. 把批量计数做成日常脚本:递归统计、排序与Excel导出

4.1 需求先写清楚:递归目录、忽略临时文件、结果可排序

工具能数单个文件只是起步,真正省时间的做法是把“统计整个文件夹”变成一个可重复执行的脚本。做之前先定义清楚需求,不然脚本写到一半会自己跟自己打架。

第一,要递归扫描子目录,因为归档目录几乎总是带层级结构的。第二,要忽略临时文件和部分损坏文件——比如~$开头的Office临时文件、0字节的占位PDF。第三,结果要能按页数排序,并且标记“无法解析”的文件,不要静默跳过。第四,输出格式要统一,方便直接贴进Excel或者转CSV。这些需求合并起来,就是下面这个脚本的雏形。

4.2 代码:递归统计一个文件夹下所有PDF

先把核心功能做出来,不依赖pandas或openpyxl,用纯标准库加pypdf,这样在其他机器上部署成本最低。

import os from pathlib import Path from pypdf import PdfReader def scan_pdf(root_dir): """递归统计root_dir下所有PDF的页数,返回list[tuple[文件名, 页数]]""" results = [] root_path = Path(root_dir) for pdf_path in root_path.rglob("*.pdf"): # 跳过文件名以~$开头的临时文件 if pdf_path.name.startswith("~$"): continue try: reader = PdfReader(str(pdf_path), strict=False) pages = len(reader.pages) results.append((str(pdf_path), pages)) except Exception as e: results.append((str(pdf_path), -1)) # -1 表示统计失败 return results if __name__ == "__main__": base_dir = r"D:\archive" data = scan_pdf(base_dir) for path, pages in sorted(data, key=lambda x: x[1], reverse=True): status = f"{pages} 页" if pages >= 0 else "统计失败" print(f"{path}\t{status}")

逻辑说明:rglob("*.pdf")是pathlib提供的递归匹配方法,比手写os.walk简洁。strict=False让解析器对损坏文件更宽容,配合try/except可以保证一个坏文件不拖垮整个批次。sorted(..., reverse=True)按页数从高到低排序,打印时用\t分隔路径和结果,方便后续复制到Excel。

参数说明:如果你希望只处理大于某个字节数的文件,可以在循环里加一行if pdf_path.stat().st_size < 1024: continue,因为小于1KB的PDF大概率是损坏或空壳。如果想限制层级深度,把rglob换成glob只扫当前目录即可。这个脚本在几千个文件下跑完也就十几秒,瓶颈基本在硬盘IO而不是解析本身。

4.3 代码:导出Excel并标记异常文件

纯文本输出虽然方便,但交付给不写代码的同事还是需要Excel。这里我用csv做中间格式,因为Excel直接打开CSV无压力,而且不用装openpyxl。

import csv from pathlib import Path from pypdf import PdfReader def scan_pdf(root_dir): """同上,返回list[dict],便于写表""" results = [] for pdf_path in Path(root_dir).rglob("*.pdf"): if pdf_path.name.startswith("~$"): continue try: reader = PdfReader(str(pdf_path), strict=False) results.append({ "文件路径": str(pdf_path), "文件名": pdf_path.name, "页数": len(reader.pages), "状态": "正常", }) except Exception: results.append({ "文件路径": str(pdf_path), "文件名": pdf_path.name, "页数": "", "状态": "解析失败", }) return results def write_csv(data, output_path): with open(output_path, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["文件路径", "文件名", "页数", "状态"]) writer.writeheader() writer.writerows(data) if __name__ == "__main__": data = scan_pdf(r"D:\archive") write_csv(data, r"D:\archive\页数统计.csv") failed = [d for d in data if d["状态"] != "正常"] print(f"共 {len(data)} 个PDF,其中 {len(failed)} 个解析失败")

逻辑说明:encoding="utf-8-sig"是关键,不加sig用Excel打开CSV会出现中文乱码,加了这个BOM头后Excel直接识别UTF-8。DictWriter配合fieldnames可以保证列顺序固定。最后打印统计失败的数量,方便你针对这些文件单独复查。

参数说明:状态列取值为“正常/解析失败”比纯页数更直观,交付给同事时他们一眼能看出哪些文件需要人工介入。如果你要在表里加“文件大小(KB)”列,在循环里加"文件大小": pdf_path.stat().st_size // 1024即可,对估算打印成本尤其有用。这套脚本我改过三版,第一次没有加状态列,结果好几个损坏文件被当成0页,后面加了状态标记就再也不怕背锅了。

4.4 参数说明与常见改法(加白名单、限制大小、合并子目录)

脚本写完之后,实际部署中你一定会遇到“想改一改”的需求。最常见的是加文件白名单:归档目录里除了PDF,可能混着一堆同名txt说明文件,虽然不影响统计,但输出表格不够干净。另一个常见需求是合并子目录:如果每个子产品线一个文件夹,且希望统计结果按产品线汇总,可以在scan_pdf里先把pdf_path.parent.name存下来,然后用itertools.groupby按父目录分组。

这里还要提醒一个容易忽略的参数:PdfReader打开文件时会读整个交叉引用表,如果文件在网络的SMB共享盘上,速度会明显变慢。我一般建议先把共享盘映射到本地盘符,或先robocopy到本地临时目录再统计。页数超过2000的超大PDF文件,偶尔会有解析器自己算晕的情况,这时把len(reader.pages)改成用reader.pages的迭代器去数(sum(1 for _ in reader.pages))会更稳,代价是慢一点。遇到这种情况,多半是PDF的/Count字段和实际/Kids对不上了,下一章就讲这类问题。

5. 页码计数常见问题与避坑:加密文件、损坏PDF与rar解压陷阱

5.1 rar解压报错“文件头损坏”:先别重下

现象:从网盘或聊天记录里下载的pdf页码计数工具.rar,双击解压到一半就弹“文件头损坏”或CRC错误,文件不全,exe跑不起来。

原因:这类rar包在传输过程中被网页下载器断点续传损坏,或者是发布者用“伪加密”方式打包导致部分解压工具识别异常。还有一种可能是网盘服务商做了文件嗅探,把压缩包当危险文件截断。

解决:先用WinRAR自带的“修复压缩文件”功能(打开rar后按Alt+R,选“把损坏的文件保存为修复后的文件”),修复后解压成功率较高。如果修复出来的文件大小明显异常,直接用qtool或其他工具检测rar的加密标志位,伪加密可以通过修改文件头第18个字节绕过。实在不行就换一个下载来源,这类工具本来就有多个分发版本,别跟一个坏包死磕。

5.2 统计结果在不同工具间不一致:/Count缓存与真实页面数打架

现象:同一个PDF,pdfinfo统计出12页,某exe工具统计出13页,用Adobe打开看却是13页,三边对不上。

原因:PDF页面树的根节点上有一个/Count字段,它按规范必须等于所有子页面数量之和。但某些PDF生成器(特别是老版本Word转PDF插件)在写文件时/Count算错了,Adobe打开时会忽略这个字段、按实际页面对象数显示,而pdfinfo严格按规范直接读/Count,于是两边结果不一样。这属于源文件本身的“脏数据”,不是工具的问题。

解决:以Adobe或pdfinfo遍历/Kids数组的结果为准,pdfinfo可以通过-l参数测试真实页数。如果/Count明显错误,用qpdf重写一次文件就能把交叉引用表和页面树修正:“qpdf --linearize --object-streams=generate input.pdf fixed.pdf”。重写后/Count被重新计算,各工具统计结果就一致了。注意重写文件会改变文件大小和元数据,涉及正式交付材料时先备份。

5.3 加密PDF统计不到页数:需要绕过所有者密码

现象:工具打开加密PDF要么提示“需要密码”,要么显示0页,连文件大小都读不出来。

原因:PDF的加密分用户密码(打开密码)和所有者密码(权限密码)。很多归档PDF只设置了所有者密码,限制复制打印权限,但打开时不需要输密码。pdfinfo和pypdf默认按“无密码”去解析,遇到所有者密码就卡住了。

解决:先区分到底哪类加密。用pdfinfo试一次,如果提示需要密码,用-upw传空字符串不行就说明有打开密码。如果只是所有者密码,用qpdf一行命令去除后再统计:“qpdf --decrypt input.pdf output.pdf”。这个操作不解锁打开密码,只去除权限限制,在合规使用场景下是文档管理员的常规操作。统计完之后可以把output.pdf删掉,只在内存里留页数结果。

5.4 现成exe被杀软报毒:别头铁

现象:刚从rar里解压出来的工具,双击前杀软弹窗报毒,或者文件直接被隔离,整个pdf页码计数工具文件夹变成空壳。

原因:这类小工具大多用易语言编写,而易语言运行库本身常被杀软标记为风险行为;加上部分作者为了防破解会加UPX壳或VMProtect,这些壳的特征在杀软眼里跟木马几乎没区别。说句不好听的,这个生态里也确实混进去过带广告子程序甚至后门的版本。

解决:不要在一个正经生产力环境里对一个报毒工具“添加信任”。如果只是临时用,放在非联网的虚拟机里跑;如果经常要用,干脆彻底放弃exe路线,改用第三章的pdfinfo或Python方案。我自己的习惯是:U盘里长期备一个poppler压缩包,不装系统级,随用随解压,这样既不触发杀软安装拦截,也不依赖任何人的exe,心里踏实。

5.5 大文件卡死:工具在渲染而不是在计数

现象:把一个300页的扫描版PDF拖进工具,界面卡了十几秒才出结果,有时直接无响应;但pdfinfo跑同一个文件是瞬间出结果的。

原因:部分exe工具的实现方式是调用PDF渲染控件(比如Windows的GDI或第三方ActiveX)来“打开文档”从而获取页数,这个过程等于把每一页都过了一遍渲染管线。页数一多,或者页面里有高清扫描图片,内存和CPU都被吃满,自然卡死。

解决:换解析型工具,pdfinfo和pypdf走的是结构解析,不渲染页面,理论上页数和文件大小关系不大。如果一定要用某个exe,优先选在文件“关于”信息里写明了“基于PDF规范解析”的版本。另外,统计大批量文件时建议分批执行,每批不超过500个,避免工具一次加载太多文件导致内存占满。

6. 进阶:用双重校验与qpdf检查让页码统计结果可信

工具能跑通还不够,交付给别人的数据必须经得起复核。我现在的进阶做法是对统计结果做双重校验:先用pdfinfo读/Count,再用pypdf遍历页面树,两者结果一致才写出报告;不一致时走qpdf重写再做第三次确认。

for f in *.pdf; do a=$(pdfinfo "$f" | awk '/^Pages/{print $2}') b=$(python -c "from pypdf import PdfReader; print(len(PdfReader('$f').pages))") echo "$f pdfinfo=$a pypdf=$b" done

这个循环会逐文件打印两个工具的统计结果,一眼能看出哪个文件存在/Count不准确的问题。如果a和b数值长期稳定一致,说明这批PDF的文件结构是健康的;但凡出现偏差,就把文件单独拎出来跑“qpdf --check”,它会直接告诉你页面树哪里不对。qpdf的检查报告里如果有“error: Page count mismatch”之类的描述,对应就是本章5.2节的情况。

做完校验后,输出报告时我会额外加一列“校验状态”:两个结果一致标“通过”,不一致标“复核”。这个习惯救过我一次——当时给客户交付的PDF合并文件里,有三份是从旧系统导出的,/Count全部虚高一页,如果只信单一工具,打印费用就会多算三页。从那以后,凡是超过50个文件的批量统计,我都默认跑一遍双重校验,成本是每个文件多花几十毫秒,但换来的是数据可信度,值。

日常使用中,我还养成了一个习惯:脚本里的异常分支必须打印到终端,而不是静默吞掉。统计失败的文件名一旦被静默丢弃,报告里就少了一个文件,这比页数错了还隐蔽。现在所有批量PDF统计脚本跑完,我都会看一眼“共X个PDF,Y个解析失败”那行输出,Y不为零就绝不发报告。希望这篇笔记能把你在PDF页数统计上可能要走的弯路提前探平,帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询