发票 PDF 智能合并
将多张发票 PDF 拼版到同一张 A4 纸上,节省打印纸张,同时保持 PDF 文字可选中特性。
核心特性
- 保持文字可选中 — 使用 PyMuPDF 的
show_pdf_page(),不转图片 - 发票上 + 行程单下 — 固定上下布局,发票自适应缩放至半页,行程单按原始尺寸铺排
- 自动删除空白尾页 — 检测并删除仅有页码的无意义尾页
- 落单发票处理 — 只占上半张 A4,下半留空,大小与合并时一致
- 批量处理 — 自动扫描文件夹,按文件名智能配对发票和行程单
安装依赖
/usr/bin/python3 -m pip install PyMuPDF --user
注意:必须使用系统 Python 安装,WorkBuddy 管理的 Python 环境可能不同。
快速使用
核心设计原则:一张发票对应一个行程,按文件名中的共同前缀智能配对。
方式一:批量处理文件夹
将脚本放在待处理文件夹同级目录,然后运行:
/usr/bin/python3 merge_invoices.py <输入文件夹> <输出文件夹>
脚本会自动:
- 提取分组标识 — 从文件名中【】括号内的文字提取(如
【打车服务-2024年6月-市区到机场】) - 同一标识下的文件归为一组(每组 = 1个独立行程)
- 组内精确匹配「电子发票/电子普通发票」+「电子行程单/行程报销单」(用完整后缀判断,避免关键词误匹配)
- 每组输出一个 PDF:发票在上 + 行程单在下
- 无法配对的单独处理(只占上半张 A4)
方式二:合并两个指定 PDF
/usr/bin/python3 merge_invoices.py invoice1.pdf invoice2.pdf output.pdf
脚本说明
核心脚本:scripts/merge_invoices.py
主要函数
| 函数 | 用途 |
|---|---|
merge_invoice_itinerary() |
发票+行程单上下布局合并,自动删空白尾页 |
merge_two_pdfs() |
两个PDF简单2合1拼版(上下或左右) |
place_single_invoice() |
落单发票放到上半张A4 |
get_content_bottom() |
检测页面实际内容底部坐标 |
is_trivial_page() |
判断页面是否仅有页码等无意义内容 |
auto_merge_folder() |
批量扫描文件夹,按【】前缀智能分组配对 |
_extract_group_key() |
从文件名提取分组标识(【】括号或去后缀) |
关键设计决策
- 固定上下布局 — 发票在上半张、行程单在下半张,符合阅读习惯
- 行程单不压缩 — 按原始尺寸铺排,内容完整可读
- 检测内容区域 — 用
page.get_text("dict")["blocks"]获取 bbox,而非依赖页面尺寸 - 删除废页 — 输出后检查最后一页,若仅含页码(支持
"页码:1/1"、"页码: 1 / 1"等带空格格式)则删除
常见问题
Q:行程单内容太多,合并后变成多页? A:这是预期行为。行程单按原始尺寸铺排,超出部分自动续页。如果不需要多页,可以修改脚本改用左右布局。
Q:合并后的PDF文字不可选?
A:检查是否安装了正确版本的 PyMuPDF。必须使用 show_pdf_page() 方法,不能将 PDF 转为图片再合并。
Q:落单发票太大/太小? A:落单发票会使用与合并时相同的缩放逻辑,放到上半张A4的中心位置,大小一致。
输出文件命名
- 每组配对输出一个独立 PDF:
{分组标识前30字}_合并.pdf(如打车服务-2024年6月_合并.pdf) - 落单发票:保持原文件名,只占上半张 A4
配对逻辑详解
旧版(已废弃)用关键词 "发票" in f / "行程" in f 匹配,但发票文件名常含「1个行程」等文字导致误匹配。
新版采用两步法:
- 先分组:提取文件名中【】括号内的文字作为分组 key
- 再精确匹配:组内用完整后缀(
.电子发票.pdf/.电子行程单.pdf)判断类型
