心疼你的一切头像
Python批量处理Word和PDF实战封面图

Python批量处理Word和PDF实战

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 等)

壁纸分享

在这里插入图片描述

八、总结

  1. Word 批量生成的公式:模板 + {{占位符}} + 整段替换 + 回读验证;
  2. PDF 批量操作的公式:pymupdf.open() 收集 + insert_pdf 合并 + get_text 提取;
  3. 两个必记细节:run 拆分坑(整段替换才能根治)和 fontname="china-s"(PDF 中文);
  4. 工程习惯:先小批量验证再跑全量,assert 断言占位符替换干净。

再配合第 5 篇的自动发邮件,"批量生成通知书 → 逐个发给对应员工"就是几行代码的事,自动化闭环又扩大了一圈。

如果本文对你有帮助,点赞 + 收藏 + 关注三连支持一下~ 你要批量处理的是什么文档?评论区聊聊。下一篇我们写《用 FastAPI 10 分钟写一个能跑的接口服务》。


转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_42603590/article/details/169548319

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--