Python 批量处理 Word/PDF 实战:python-docx + PyMuPDF
摘要: 领导说"给这 200 个员工每人发一份结业通知书",你不会要改 200 次模板吧?财务说"把这 36 个月报合成一个 PDF",你不会要拖 36 次鼠标吧?本文用 python-docx 从模板批量生成 Word 文档,用 PyMuPDF 批量合并 PDF、提取文本,并把两个最容易翻车的坑讲透:Word 占位符被拆进多个 run 导致替换失效、PDF 默认字体写不了中文。全文代码实测通过,含回读验证。
标题
一、两个经典场景
凡是"内容 90% 相同、只有几个字段不同"的 Word 文档,都属于本文 A 部分要解决的场景:
给每个结业员工发一份《培训结业通知书》,姓名、部门、日期不同,其余完全一样。
凡是"一堆散装 PDF 要合成一个/要从中捞内容"的操作,都属于 B 部分:
把 7、8、9 三个月的月报 PDF 合并成《Q3 月报合集》,并顺手提取每页标题做目录。
两个场景共用一个工程原则:程序写完先做小批量验证(回读检查),确认无误再放去跑全量。
二、环境准备
pip install python-docx pymupdf
| 库 | 作用 |
|---|---|
| python-docx | 读写 .docx,只能处理 docx,不支持老 .doc(后者先用 Word 另存为 docx) |
| pymupdf | 读写 PDF 的全能选手:合并、拆分、提取文本/图片、加水印、加密 |
注意:PyMuPDF 新版本请写
import pymupdf,老的import fitz写法已标记弃用(运行时会看到 deprecation warning)。
三、A 部分:python-docx 批量生成 Word
3.1 做一个带占位符的模板
模板就是一份普通 docx,把"变化的部分"写成 {{占位符}}(和 Word 自带的邮件合并域一个思路,但更直观):
from docx import Document
def make_template(path):
path.parent.mkdir(parents=True, exist_ok=True)
doc = Document()
doc.add_heading("培训结业通知书", level=1)
doc.add_paragraph("{{姓名}} 同学:")
doc.add_paragraph(" 你已完成《Python 办公自动化》全部课程学习,"
"经考核成绩合格,准予结业,特发此证,以资鼓励。")
doc.add_paragraph("部门:{{部门}}")
doc.add_paragraph("发证日期:{{日期}}")
doc.save(path)
实际项目中,直接用设计好的正式模板文件(红头、落款、公章位都排好版),不需要代码来生成模板。
3.2 替换逻辑:90% 的人会踩的 run 拆分坑
网上流传的一行流写法是:
p.text = p.text.replace("{{姓名}}", "张伟") # 有坑的写法!
它能跑,但会把整个段落的格式拍平(加粗的字头、特殊字体全丢)。而更隐蔽的是另一种流行写法的失败场景——在 run 里做替换:
for run in p.runs:
run.text = run.text.replace("{{姓名}}", "张伟") # 会漏!
docx 的底层是 XML,一个段落被切成若干个 run。 你在 Word 里输入 {{姓名}} 时,只要中途切换过输入法、改过字、动过格式,Word 就可能把它拆进两个 run,变成 {{姓 + 名}}——单 run 替换永远匹配不上,占位符原样留在文档里。
正确姿势:在"段落"层面拼全文做替换,再把结果写回第一个 run(顺带保留它的格式):
def fill_template(tpl: Path, out: Path, mapping: dict):
"""按映射表替换模板里的 {{占位符}},另存为新文档"""
doc = Document(tpl)
for p in doc.paragraphs:
if "{{" not in p.text:
continue
# 按整段替换,避免占位符被 Word 拆进多个 run 导致漏替换
new_text = p.text
for key, val in mapping.items():
new_text = new_text.replace(key, val)
# 写回:清空其余 run,整段新文字放进第一个 run(沿用它的格式)
for run in p.runs[1:]:
run.text = ""
if p.runs:
p.runs[0].text = new_text
else:
p.add_run(new_text)
doc.save(out)
补充:表格里的占位符要遍历
doc.tables→table.rows→row.cells→cell.paragraphs,替换逻辑与此完全相同。
3.3 批量生成 + 回读验证
名单在实际项目中从 Excel 读取(第 1 篇的 pandas 一行就能读),本文为便于复现直接写在代码里:
EMPLOYEES = [("张伟", "销售部"), ("李娜", "市场部"), ("王强", "技术部"),
("刘洋", "人事部"), ("陈静", "财务部")]
def batch_make_docs(tpl, folder):
folder.mkdir(exist_ok=True)
for name, dept in EMPLOYEES:
out = folder / f"培训结业通知书_{name}.docx"
fill_template(tpl, out, {"{{姓名}}": name, "{{部门}}": dept,
"{{日期}}": "2026年10月29日"})
回读验证是批量文档脚本的标配:生成完抽样读回一份,断言占位符已替换干净:
sample = Document(folder / "培训结业通知书_张伟.docx")
joined = "\n".join(p.text for p in sample.paragraphs)
assert "{{" not in joined and "张伟" in joined
四、B 部分:PyMuPDF 合并 PDF、提取文本
4.1 合并多个 PDF
核心 API 只有一个:merged.insert_pdf(src)。glob 排序后逐个并入,文件名里用 2026-07 这种补零格式,排序即时间序:
import pymupdf
def merge_pdfs(folder: Path, out: Path):
"""把文件夹里所有 PDF 按文件名顺序合并成一个"""
merged = pymupdf.open()
for pdf_path in sorted(folder.glob("*.pdf")):
with pymupdf.open(pdf_path) as src:
merged.insert_pdf(src) # 把整份文档追加到末尾
print(f"并入 {pdf_path.name}({src.page_count} 页)")
merged.save(out)
print(f"合并完成:{out.name} 共 {merged.page_count} 页")
merged.close()
拆分也简单:insert_pdf(src, from_page=0, to_page=4) 支持按页区间抽取,可以做出"只要每份月报的前 2 页"这种合集。
4.2 生成 PDF 时的中文支持
如果流程里还要用代码生成 PDF,注意:PyMuPDF 默认的西文字体写不了中文,必须用内置的 CJK 字体 china-s(简体):
doc = pymupdf.open()
page = doc.new_page()
page.insert_text((72, 120), "2026 年 9 月销售月报",
fontsize=20, fontname="china-s") # 不加 fontname 中文会丢失
doc.save("2026-09销售月报.pdf")
4.3 提取文本
提取文本是 page.get_text(),逐页读回:
def extract_text(pdf: Path):
"""逐页提取文本(可直接交给 pandas/正则做进一步分析)"""
with pymupdf.open(pdf) as doc:
for no, page in enumerate(doc, start=1):
text = page.get_text().strip()
first_line = text.splitlines()[0] if text else "(空白页)"
print(f"第 {no} 页:{first_line}")
注意:扫描件(图片型 PDF)提取不出文本,需要上 OCR(如 Tesseract),那属于另一个话题。
五、完整代码
保存为 doc_pdf_batch.py:
# -*- coding: utf-8 -*-
"""
批量处理 Word/PDF 实战:python-docx + PyMuPDF
依赖:pip install python-docx pymupdf
用法:python doc_pdf_batch.py
"""
from pathlib import Path
import pymupdf # 新版用 import pymupdf,旧的 import fitz 已弃用
from docx import Document
OUT_DIR = Path(__file__).parent / "输出文件"
# ============ A 部分:批量生成 Word 通知书 ============
EMPLOYEES = [
("张伟", "销售部"), ("李娜", "市场部"), ("王强", "技术部"),
("刘洋", "人事部"), ("陈静", "财务部"),
]
def make_template(path: Path):
"""生成带占位符的模板文档(实际项目中直接用现成的模板文件即可)"""
path.parent.mkdir(parents=True, exist_ok=True)
doc = Document()
doc.add_heading("培训结业通知书", level=1)
doc.add_paragraph("{{姓名}} 同学:")
doc.add_paragraph(" 你已完成《Python 办公自动化》全部课程学习,"
"经考核成绩合格,准予结业,特发此证,以资鼓励。")
doc.add_paragraph("部门:{{部门}}")
doc.add_paragraph("发证日期:{{日期}}")
doc.save(path)
print(f"模板已生成:{path.name}")
def fill_template(tpl: Path, out: Path, mapping: dict):
"""按映射表替换模板里的 {{占位符}},另存为新文档"""
doc = Document(tpl)
for p in doc.paragraphs:
if "{{" not in p.text:
continue
# 按整段替换,避免占位符被 Word 拆进多个 run 导致漏替换
new_text = p.text
for key, val in mapping.items():
new_text = new_text.replace(key, val)
for run in p.runs[1:]:
run.text = ""
if p.runs:
p.runs[0].text = new_text
else:
p.add_run(new_text)
doc.save(out)
def batch_make_docs(tpl: Path, folder: Path):
"""按名单逐个生成通知书"""
folder.mkdir(exist_ok=True)
for name, dept in EMPLOYEES:
out = folder / f"培训结业通知书_{name}.docx"
fill_template(tpl, out, {"{{姓名}}": name, "{{部门}}": dept,
"{{日期}}": "2026年10月29日"})
print(f"已生成:{out.name}")
print(f"共生成 {len(EMPLOYEES)} 份通知书\n")
# ============ B 部分:合并 PDF + 提取文本 ============
def make_sample_pdfs(folder: Path):
"""生成 3 个模拟月报 PDF(实际项目中换成你自己的 PDF)"""
folder.mkdir(exist_ok=True)
for i in (7, 8, 9):
doc = pymupdf.open()
page = doc.new_page()
# fontname="china-s" 是 PyMuPDF 内置的简体中文字体
page.insert_text((72, 120), f"2026 年 {i} 月销售月报",
fontsize=20, fontname="china-s")
page.insert_text((72, 170), f"这是第 {i} 期月报的正文内容。",
fontsize=12, fontname="china-s")
doc.save(folder / f"2026-{i:02d}销售月报.pdf")
doc.close()
print(f"已生成 2026-{i:02d}销售月报.pdf")
def merge_pdfs(folder: Path, out: Path):
"""把文件夹里所有 PDF 按文件名顺序合并成一个"""
merged = pymupdf.open()
for pdf_path in sorted(folder.glob("*.pdf")):
with pymupdf.open(pdf_path) as src:
merged.insert_pdf(src)
print(f"并入 {pdf_path.name}({src.page_count} 页)")
merged.save(out)
print(f"合并完成:{out.name} 共 {merged.page_count} 页\n")
merged.close()
def extract_text(pdf: Path):
"""逐页提取文本"""
with pymupdf.open(pdf) as doc:
for no, page in enumerate(doc, start=1):
text = page.get_text().strip()
first_line = text.splitlines()[0] if text else "(空白页)"
print(f"第 {no} 页:{first_line}")
if __name__ == "__main__":
# ---- A 部分:批量生成 Word ----
tpl = OUT_DIR / "通知书模板.docx"
make_template(tpl)
batch_make_docs(tpl, OUT_DIR / "通知书")
# 回读验证:占位符必须全部替换干净
sample = Document(OUT_DIR / "通知书" / "培训结业通知书_张伟.docx")
joined = "\n".join(p.text for p in sample.paragraphs)
assert "{{" not in joined and "张伟" in joined and "销售部" in joined
print("Word 回读验证通过:占位符已全部替换\n")
# ---- B 部分:合并 PDF ----
pdf_dir = OUT_DIR / "月报PDF"
make_sample_pdfs(pdf_dir)
merged_pdf = OUT_DIR / "2026年Q3销售月报合集.pdf"
merge_pdfs(pdf_dir, merged_pdf)
extract_text(merged_pdf)
with pymupdf.open(merged_pdf) as doc:
all_text = "".join(page.get_text() for page in doc)
assert "9 月销售月报" in all_text and doc.page_count == 3
print("PDF 回读验证通过:合并后 3 页,文本可正常提取")
六、运行结果
实测输出:
模板已生成:通知书模板.docx
已生成:培训结业通知书_张伟.docx
已生成:培训结业通知书_李娜.docx
已生成:培训结业通知书_王强.docx
已生成:培训结业通知书_刘洋.docx
已生成:培训结业通知书_陈静.docx
共生成 5 份通知书
Word 回读验证通过:占位符已全部替换
已生成 2026-07销售月报.pdf
已生成 2026-08销售月报.pdf
已生成 2026-09销售月报.pdf
并入 2026-07销售月报.pdf(1 页)
并入 2026-08销售月报.pdf(1 页)
并入 2026-09销售月报.pdf(1 页)
合并完成:2026年Q3销售月报合集.pdf 共 3 页
第 1 页:2026 年 7 月销售月报
第 2 页:2026 年 8 月销售月报
第 3 页:2026 年 9 月销售月报
PDF 回读验证通过:合并后 3 页,文本可正常提取
生成的目录结构:
输出文件/
├── 通知书模板.docx
├── 通知书/
│ └── 培训结业通知书_张伟.docx ...共 5 份
├── 月报PDF/
│ └── 2026-07销售月报.pdf ...共 3 份
└── 2026年Q3销售月报合集.pdf
5 份通知书秒出,36 个月报合一个 PDF 也只是一次 for 循环的事。
七、常见坑速查表
| 现象 | 原因 | 解法 |
|---|---|---|
| 占位符替换后还在文档里 | 占位符被 Word 拆进多个 run | 按 3.2 节"整段替换再写回第一个 run" |
| 替换后段落格式全丢 | 用 p.text = ... 整体赋值 | 只写回 p.runs[0].text,其余 run 清空 |
PackageNotFoundError | 文件其实是老 .doc 或已损坏 | 先用 Word 另存为 .docx |
保存时报 PermissionError | 文档正被 Word/WPS 打开 | 关掉再跑 |
| PDF 中文丢失/乱码 | 默认西文字体无中文字形 | insert_text(..., fontname="china-s") |
import fitz 报弃用警告 | 新版 PyMuPDF 更名 | 改用 import pymupdf |
| 扫描件提取不出文本 | 内容是图片不是文字 | 需要 OCR(Tesseract 等) |
壁纸分享

八、总结
- Word 批量生成的公式:模板 +
{{占位符}}+ 整段替换 + 回读验证; - PDF 批量操作的公式:
pymupdf.open()收集 +insert_pdf合并 +get_text提取; - 两个必记细节:run 拆分坑(整段替换才能根治)和
fontname="china-s"(PDF 中文); - 工程习惯:先小批量验证再跑全量,
assert断言占位符替换干净。
再配合第 5 篇的自动发邮件,"批量生成通知书 → 逐个发给对应员工"就是几行代码的事,自动化闭环又扩大了一圈。
如果本文对你有帮助,点赞 + 收藏 + 关注三连支持一下~ 你要批量处理的是什么文档?评论区聊聊。下一篇我们写《用 FastAPI 10 分钟写一个能跑的接口服务》。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_42603590/article/details/169548319




