PDF 修改器(pdf-editor)
面向"要交付给别人"的 PDF 处理场景:改完要像原文件,转完不能丢数据,脱完不能还原。
使用原则(违反必翻车)
- 先体检后动手:任何操作前先跑
pdf_probe.py。不知道有没有文本层、字体是否嵌入、有没有隐藏文字,就动手改=蒙眼开车。 - 不盖黑块,只删对象:脱敏必须是"移除内容对象",不是"画个黑矩形"。黑块下面的文字复制粘贴就能还原(已实测验证,见下)。
- 转换不是终点,校验才是:转 Word 后必须比对表格行列数与文本量,否则丢了几行你也不知道。
- 交付前必验收:用
pdf_verify.py反向搜索关键词,确认"真的没了"而不是"看起来没了"。 - 原件只读:所有脚本只写新文件,绝不覆盖输入 PDF。
环境与依赖
1. 依赖清单
| 用途 | 包 | 必需 |
|---|---|---|
| PDF 读写 / 改写 / 脱敏 / 标签 | pymupdf (>=1.24) |
✅ |
| 结构树、JS/附件检测、XMP 清理 | pikepdf |
✅ |
| PDF → Word | pdf2docx + python-docx |
转 Word 时需要 |
| 造测试样本 | reportlab |
可选 |
安装:
pip install pymupdf pikepdf pdf2docx python-docx reportlab
2. OCR 后端(仅扫描件场景需要)
按 rapidocr → winocr → tesseract 顺序自动探测,无需手动指定。
先用 pdf_ocr.py <file> --check 看本机哪个可用。
| 后端 | 安装 | 平台 |
|---|---|---|
rapidocr |
pip install rapidocr-onnxruntime |
跨平台,中文好,带置信度 |
winocr |
pip install winrt-Windows.Media.Ocr winrt-Windows.Graphics.Imaging winrt-Windows.Storage winrt-Windows.Globalization winrt-Windows.Storage.Streams |
仅 Windows 10/11,需装中文识别包 |
tesseract |
装系统 tesseract + pip install pytesseract + 中文语言包 |
跨平台 |
3. 路径约定(重要)
下文所有命令中:
$PY= 你的 Python 解释器(Windows 下注意 venv 里是Scripts/python.exe而非bin/python)$S= 本技能包内的scripts/目录
路径一律用绝对路径。Windows Git Bash 下 ~ 展开可能出错;WinRT 相关接口还要求反斜杠路径。
> 本机已验证环境(作者机器,仅供参考,不代表通用)
> Python:C:\Users\karl8\.workbuddy\binaries\python\envs\pdfenv\Scripts\python.exe
> OCR 依赖隔离在 D:/pdfeditor-ocr-deps,运行时需 export PYTHONPATH="D:/pdfeditor-ocr-deps"。
> 该系统盘长期 100% 满,所有 pip 安装都需把 TMP/TEMP/cache/target 指向其他盘。
模块路由
| 需求 | 脚本 | 关键参数 |
|---|---|---|
| 动手前体检(强制) | pdf_probe.py |
-o report.json |
| 改文字(保字体不改版) | pdf_edit_text.py |
-r "旧=>新" --font-file |
| 转 Word | pdf_to_docx.py |
--report diff.json |
| 扫描件→可搜索 | pdf_ocr.py |
--check 自检,--dpi |
| 标签/阅读顺序 | pdf_tag.py |
--check / --reading-order / -o |
| 真·脱敏 | pdf_redact.py |
--pattern --deep |
| 交付前验收 | pdf_verify.py |
--keywords --detect-fake-redaction |
| 造测试样本 | make_sample.py |
-o sample.pdf |
| 支付宝 AI 按量付费 | alipay_metering.py |
probe / pay / complete / ack |
一、保真文字改写
坑:直接贴一块白底再写字,会因为字体缺失、字号不匹配、旋转角丢失而"字体错乱、版面变形"。
做法:读回原 span 的字体/字号/颜色/旋转角/基线 → redaction 移除原对象 → 原位原样回写 → 超长时按原框宽自适应缩放。
# 改日期、金额
"$PY" "$S/pdf_edit_text.py" in.pdf -o out.pdf \
-r "2026年12月31日=>2027年03月31日" -r "USD 12,345.67=>USD 99,999.00" \
--report edit.json
# 中文视觉 100% 一致(推荐:指定原字体文件)
"$PY" "$S/pdf_edit_text.py" in.pdf -o out.pdf \
-r "甲方=>乙方" --font-file "C:/Windows/Fonts/msyh.ttc"
# 先预览不落盘
"$PY" "$S/pdf_edit_text.py" in.pdf -o out.pdf --dry-run -r "张三=>李四"
字体策略(脚本自动判断并写进报告):
- 原字体是 CJK 字体 → 等价替换为内置 china-s(Droid Sans Fallback),不改样式;
- 原字体无 CJK 字形 → 回退 china-s 并如实标记;
- 纯拉丁文本 → 沿用原字体;
- 要绝对一致 → 用 --font-file。
批量编辑用 --json edits.json([{page, old, new, regex}],page 省略=全文)。
二、转 Word(含差异校验)
坑:页眉页脚错位、表格数据丢失、复杂排版错乱。pdf2docx 是常见解,但它会把无法定位的文字块包成 1×1 表格框。
"$PY" "$S/pdf_to_docx.py" in.pdf -o out.docx --report diff.json
校验项:表格数量与行列数、文本量差异率(>15% 告警)、图片丢失、页眉页脚提醒。
判定 NEED_FIX 时按报告逐项人工处理,不要直接交付。
已知行为:多出的 1×1 表格框属 MED 级排版降级(非数据丢失),在 Word 中选中该表 → 表格工具 → 转换为文本 即可。
三、扫描件 OCR → 双层可检索
坑:扫描 PDF 本质是图片,没有文本层 → 不能选中、复制、搜索。
做法:OCR 后写成不可见文本层(渲染模式 3,绘制但不显示),视觉上一个像素不动,但可选可搜可被索引。
"$PY" "$S/pdf_ocr.py" scan.pdf -o searchable.pdf --dpi 200
"$PY" "$S/pdf_ocr.py" scan.pdf --check # 后端可用性自检
"$PY" "$S/pdf_ocr.py" scan.pdf -o text.txt --text-only
后端自动探测顺序:rapidocr → winocr(Windows 内置 OCR)→ tesseract。
本机实测:rapidocr 不可用(onnxruntime DLL 加载报 1114),
实际生效的是 Windows 内置 OCR(winrt 绑定 + 简体中文包)。实测效果:
| 指标 | 结果 |
|---|---|
| 双层化前可提取字符 | 0(扫描件,无文本层) |
| 双层化后可提取字符 | 211 |
| 关键词搜索 | 「星海」「营业收入」「净利润」等均命中 |
| 视觉一致性 | 290 万像素中 0 个像素不同(外观完全不变) |
Windows OCR 不返回置信度,且中文会拆成单字/带空格(如 1 2 5 0),
含数字的识别结果必须人工校对。
> 装 winrt 有个坑:pip --target 会因命名空间包冲突漏装文件
> (winrt/windows/storage/streams/ 缺失)。解法见 references/03-ocr-and-tagging.md。
四、标签(Tagging)与阅读顺序
坑:无标签 PDF 没有阅读顺序与语义结构,屏幕阅读器按绘制顺序乱读,复制粘贴出来顺序错乱,归档可用性要求过不了。
"$PY" "$S/pdf_tag.py" in.pdf --check # 是否已 tagged
"$PY" "$S/pdf_tag.py" in.pdf --reading-order -o order.txt # 输出顺序供校对
"$PY" "$S/pdf_tag.py" in.pdf -o tagged.pdf --lang zh-CN --alt "1:收入趋势图"
产出:/MarkInfo << /Marked true >>、/Lang、/StructTreeRoot
(Document → Sect → H1~H6 / P / Table / Figure),页面内容流注入 MCID 并挂 /ParentTree。
分类规则:页边+小字号 → Artifact(页眉页脚);近白色/极小字 → Artifact(隐藏文字);
≥22pt → H1;加粗或 ≥18pt → H2;匹配"一、""1.2" → H3;表格整体作为一个元素(表内单元格去重)。
五、真·脱敏(重点)
坑:画黑块遮盖后,底层文字对象仍在,Ctrl+C 就能还原。实测对照(用 make_sample.py 造的样本):
| 检查项 | 画黑块(假脱敏) | 本技能真脱敏 |
|---|---|---|
| 身份证号 440304199003072118 | 残留,可复制 | 已清除 |
| 手机号 13800138000 | 残留,可复制 | 已清除 |
| 元数据 / 嵌入附件 / JS / XMP | 全部保留 | 全部清除 |
| 隐藏白色文字 | 保留 | 清除 |
# 按敏感类型脱敏 + 深度清理(推荐)
"$PY" "$S/pdf_redact.py" in.pdf -o out.pdf --pattern idcard,phone,bankcard --deep
# 按关键词 / 正则 / 区域
"$PY" "$S/pdf_redact.py" in.pdf -o out.pdf --text "张三" --text "13800138000" --deep
"$PY" "$S/pdf_redact.py" in.pdf -o out.pdf --area "2:72,700,300,720" --deep
# 脱敏后必做验收
"$PY" "$S/pdf_verify.py" out.pdf --keywords "张三,13800138000" --detect-fake-redaction
内置类型:idcard phone bankcard email creditcode ip date_cn
--deep 会额外清理:隐藏文字(白字/极小字/渲染模式3)、嵌入附件、JavaScript、
注释与表单域、Info 与 XMP 元数据。外发前务必带 --deep。
脱敏后脚本会重开文件反向验证,残留命中数必须为 0 才判 PASS。
泄漏面清单(脱敏时逐项确认)
- [ ] 正文文本(含隐藏白字、渲染模式 3 的不可见文字)
- [ ] 图片里的敏感信息(需像素级处理,不能只盖矩形)
- [ ] 文档元数据(作者/标题/主题/关键字)
- [ ] XMP 元数据
- [ ] 嵌入附件
- [ ] 批注、表单域、文档级 JavaScript
- [ ] 书签/大纲中的敏感词
- [ ] 修订历史(PDF 无原生修订记录,但若有注释型批注需清)
六、交付前验收
"$PY" "$S/pdf_verify.py" out.pdf --compare in.pdf \
--keywords "张三,13800138000" --detect-fake-redaction --expect-tagged
--detect-fake-redaction 会找出深色填充矩形并检查其下方是否仍有文字——
这是识破"假脱敏"的核心手段。
七、支付宝 AI 按量付费(probe / pay / complete / ack)
按支付宝 AI 按量付费协议实现计费链路,与 PDF 业务脚本无缝串联:
先按资源(PDF 路径)下发账单,客户端付款后用 Payment-Proof 头重试,验资通过后执行业务,最后落订单并返回 ack。重复请求用相同 order_id 直接命中旧记录(幂等)。
协议状态机
| 阶段 | 协议要求 | 本技能实现 | 状态/头 |
|---|---|---|---|
| 1. probe 账单下发 | 客户端调起 → 服务端按资源预算 | probe(resource, action, hint_units) |
402 + Payment-Needed: true |
| 2. pay 携带凭证重试 | 客户端付款后用 Payment-Proof: <hmac> 头重试 |
pay(invoice, payment_proof) 验签 |
200 / 401 / 410 / 404 |
| 3. verify 验资 | 服务端校验金额、订单号、HMAC | 同 pay() 内置 |
200 / 402 |
| 4. complete 履约确认 | 验资通过后执行业务 → 落订单 | complete(order_id, result) |
200 + ack: true |
| 5. ack 幂等查询 | 同 order_id 重复请求返回一致结果 |
ack(order_id) 与 4 内置幂等 |
200 / 404 |
命令示例
# 1) 预算 + 下发账单
"$PY" "$S/alipay_metering.py" probe --resource in.pdf
# → 402 + Payment-Needed:true + order_id + amount_cny
# 2) 客户端付款后重试(带 Payment-Proof 头)
"$PY" "$S/alipay_metering.py" pay --order-id ORD... --proof <hmac>
# → 200 + ok:true + Payment-Proof-Verified:true
# 3) 验资通过后跑业务
"$PY" "$S/pdf_edit_text.py" in.pdf -o out.pdf -r "甲=>乙"
"$PY" "$S/alipay_metering.py" complete --order-id ORD... --result "ok"
# → 200 + ack:true
# 4) 任意时刻查订单状态
"$PY" "$S/alipay_metering.py" ack --order-id ORD...
# → 200 + ack:true (重复调用结果一致 = 幂等)
与业务脚本串联(一行接入)
import alipay_metering
import pdf_redact
result = alipay_metering.run_with_metering(
resource="in.pdf",
runner=lambda p: {"redacted": 3, "leakage": 0},
action="pdf.redact",
)
# 内部自动:probe → pay → 业务 → complete
订单持久化与幂等
- 存储位置:
~/.pdf-editor/orders.json(原子写入,跨重启保留;生产可换 Redis/MySQL) - 幂等机制:
pay()对已paid订单直接返回idempotent: true,不重复扣款complete()对已completed订单直接返回idempotent: true,不重复履约ack()任意时刻可查,重复查询结果一致- 过期:账单 15 分钟内未付款返回
410 invoice_expired,需重新 probe
真实接入支付宝
把 alipay_metering.py 里的两处换成 alipay-sdk-python 即可:
pay()内的 HMAC 本地校验 → 调 alipay 验签 + 查单接口probe()内的UNIT_PRICE_CNY→ 调 alipay 询价接口或本地价格表
pip install alipay-sdk-python
> 安全提醒:真实接入请把 _mock_proof 与默认 secret 替换为服务端签名(不要在客户端硬编码密钥)。
> 演示模式默认 secret 为 demo-secret-do-not-use-in-prod,仅用于本地联调与冒烟测试。
已知限制(诚实摊牌)
- OCR 识别质量:后端能力因机器而异(
--check可查)。Windows 内置 OCR 不返回置信度,
且中文会拆成单字/带空格(如1 2 5 0)——金额、编号、日期这类数字必须人工校对后再交付。 - 字体一致性:PDF 未嵌入字体时,改写只能用内置字体近似;要 100% 一致必须提供原字体文件(
--font-file)。 - MCID 与语义的绑定:按"内容流文本指令顺序 ↔ PyMuPDF span 顺序"配对。对结构极其复杂、内容流被打乱的 PDF,标签可能不精确,需人工校对阅读顺序。
- 转 Word 不是无损:pdf2docx 对复杂版面(多栏、浮动图、旋转文字)会降级,必须看差异报告。
- 图片内文字不在脱敏范围:脚本处理的是文本对象。身份证号若印在扫描图里,需按区域(图片)像素级处理或人工确认。
- 磁盘空间:依赖安装若报空间不足,把 pip 的
TMP/TEMP/cache/target指向有空间的盘再装。
参考文件
references/01-routing-and-pitfalls.md— 五大痛点原理与决策路由references/02-text-and-word.md— 文字保真改写与转 Word 细节references/03-ocr-and-tagging.md— OCR 双层化与标签化(含本机环境排障)references/04-redaction-and-verify.md— 真脱敏、泄漏面清单与验收