第 7 章

Python 操作 Word——批量生成与处理文档

第7章:Python 操作 Word——批量生成合同、报告和通知

每到年底,HR 要给全公司几百名员工发绩效通知书;法务部门要批量生成格式统一的合同;销售团队要把每个客户的报价单做成 Word 文档……这些重复劳动用 Python 做只要几分钟。本章从 python-docx 的文档结构讲起,重点讲解 docxtpl 模板引擎,教你批量生成专业 Word 文档,并涵盖图片插入、表格操作、Word 转 PDF 等进阶技巧。

python-docx 入门:文档结构与基础操作

安装库

终端

pip install python-docx docxtpl

文档结构:Document / Paragraph / Run / Table

理解 python-docx 的层级结构,是写好代码的前提:

**关键认知:**Run 是 Word 文档的最小文字单元。当你看到"Hello World",它可能是一个 Run,也可能是两个 Run("Hello "和"World"),取决于这两段文字的样式是否相同。这个概念在做搜索替换时非常重要。

创建文档并设置样式

create_basic_doc.py

from docx import Document
from docx.shared import Pt, RGBColor, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH

# 创建新文档
doc = Document()

# ── 设置文档标题 ──
heading = doc.add_heading('员工绩效通知书', level=1)
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER

# ── 添加正文段落 ──
p = doc.add_paragraph()
run = p.add_run('尊敬的张三,')
run.font.size = Pt(12)          # 字号12磅
run.font.bold = True            # 加粗
run.font.color.rgb = RGBColor(0x1F, 0x2D, 0x3D)  # 深色字体

# 添加换行后的内容
p2 = doc.add_paragraph(
    '经公司绩效委员会评定,您在2024年度考核中表现优秀,'
    '绩效评级为 A 级,特此通知。'
)
p2.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY  # 两端对齐

# ── 段落间距 ──
p2.paragraph_format.space_before = Pt(6)   # 段前6磅
p2.paragraph_format.space_after = Pt(12)   # 段后12磅
p2.paragraph_format.line_spacing = Pt(20)  # 行间距20磅

# 保存文档
doc.save('notice.docx')
print('文档已创建:notice.docx')

常用样式速查

操作 代码
加粗 run.font.bold = True
斜体 run.font.italic = True
下划线 run.font.underline = True
字号(磅) run.font.size = Pt(14)
字体颜色 run.font.color.rgb = RGBColor(255, 0, 0)
字体名称 run.font.name = '微软雅黑'
居中对齐 p.alignment = WD_ALIGN_PARAGRAPH.CENTER
右对齐 p.alignment = WD_ALIGN_PARAGRAPH.RIGHT

模板 + 数据填充:docxtpl 批量生成文档

python-docx 适合从零创建文档,但每次都要用代码控制排版非常低效。实际工作中更推荐的方式是:先在 Word 里把文档做好(含格式、公司 Logo、章),然后用 docxtpl 把变量部分替换成模板标签,再用 Python 批量填入数据。

docxtpl 模板语法

docxtpl 使用 Jinja2 模板语法,在 Word 模板文件里直接写标签:

**如何在 Word 里写模板标签:**直接在 Word 文档里输入 {{"{{"}}name{{"}}"}},Word 可能会把大括号拆成不同的 Run(自动纠错等原因)。建议关闭 Word 自动更正功能,或者用 docxtpl 提供的变量检查工具验证模板。

案例:批量生成员工通知书(100份)

假设 Word 模板文件 notice_template.docx 里含有以下标签:{{"{{"}}name{{"}}"}}(姓名)、{{"{{"}}department{{"}}"}}(部门)、{{"{{"}}grade{{"}}"}}(绩效等级)、{{"{{"}}date{{"}}"}}(通知日期)。

batch_notice.py — 从 Excel 读取数据批量生成通知书

import pandas as pd
from docxtpl import DocxTemplate
from pathlib import Path
from datetime import date

# ── 读取员工数据 ──
df = pd.read_excel('employees.xlsx')
# 预期列:姓名、部门、绩效等级

# ── 加载 Word 模板 ──
tpl = DocxTemplate('notice_template.docx')

# ── 输出目录 ──
output_dir = Path('output/notices')
output_dir.mkdir(parents=True, exist_ok=True)

today = date.today().strftime('%Y年%m月%d日')
success_count = 0

for _, row in df.iterrows():
    # 准备模板变量
    context = {
        'name':       row['姓名'],
        'department': row['部门'],
        'grade':      row['绩效等级'],
        'date':       today,
    }

    # 渲染模板
    tpl.render(context)

    # 保存为独立文件(按姓名命名)
    filename = output_dir / f"{row['姓名']}_绩效通知书.docx"
    tpl.save(filename)
    success_count += 1
    print(f'  已生成:{filename.name}')

print(f'\n完成!共生成 {success_count} 份通知书,保存至 {output_dir}')

**实测效率:**100 份通知书,Python 大约需要 3-8 秒。同样的工作人工做,熟练员工也需要至少 2-3 小时。这就是自动化的价值。

条件内容:根据数据动态选择段落

假设绩效等级 A/B/C 对应不同的奖励描述,在模板里可以这样写(直接在 Word 文档里输入):

模板内容示例(在 Word 里输入)

# 模板文件里的文本(非Python代码,展示模板语法):
# {%p if grade == 'A' %}
# 您的表现卓越,公司将授予您年度优秀员工称号,并给予额外绩效奖金。
# {%p elif grade == 'B' %}
# 您的表现良好,已达到公司标准。
# {%p else %}
# 希望您在下一年度继续努力提升。
# {%p endif %}

# Python 代码:渲染时传入 grade 变量即可
context = {
    'name': '李四',
    'grade': 'A',  # 模板会自动选择对应段落
}

复杂文档操作

插入图片(带位置和大小控制)

insert_image.py

from docx import Document
from docx.shared import Inches, Cm

doc = Document()

# 插入图片,指定宽度(高度按比例缩放)
doc.add_picture('company_logo.png', width=Inches(2))

# 指定宽度和高度(可能变形,慎用)
doc.add_picture('chart.png', width=Cm(12), height=Cm(8))

# 图片居中:图片实际是在一个段落里,对段落设置居中
from docx.enum.text import WD_ALIGN_PARAGRAPH
last_paragraph = doc.paragraphs[-1]
last_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER

doc.save('with_image.docx')

表格操作:创建、填充、设置样式

table_operations.py

from docx import Document
from docx.shared import Pt, RGBColor
from docx.oxml.ns import qn
import copy

doc = Document()
doc.add_heading('销售数据报告', level=1)

# ── 创建表格(3行4列) ──
table = doc.add_table(rows=1, cols=4)
table.style = 'Table Grid'  # 应用内置表格样式

# ── 设置表头 ──
header_cells = table.rows[0].cells
headers = ['产品名称', '销量(件)', '单价(元)', '金额(元)']
for i, text in enumerate(headers):
    cell = header_cells[i]
    cell.text = text
    # 设置表头加粗
    for run in cell.paragraphs[0].runs:
        run.font.bold = True
        run.font.size = Pt(11)

# ── 填入数据行 ──
data = [
    ('笔记本电脑', 120, 4999, 599880),
    ('无线鼠标',   350, 89,   31150),
    ('机械键盘',   200, 299,  59800),
]
for row_data in data:
    row = table.add_row()
    for i, value in enumerate(row_data):
        row.cells[i].text = str(value)

doc.save('sales_report.docx')
print('带表格的报告已生成')

页眉页脚设置

header_footer.py

from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH

doc = Document()

# ── 页眉 ──
section = doc.sections[0]
header = section.header
header_para = header.paragraphs[0]
header_para.text = '机密文件 — 仅限内部使用'
header_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
header_para.runs[0].font.size = Pt(9)
header_para.runs[0].font.color.rgb = RGBColor(128, 128, 128)

# ── 页脚(插入页码) ──
footer = section.footer
footer_para = footer.paragraphs[0]
footer_para.alignment = WD_ALIGN_PARAGRAPH.CENTER

# 添加"第 X 页"文字需要插入 Word 域代码
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

def add_page_number(paragraph):
    run = paragraph.add_run()
    fldChar1 = OxmlElement('w:fldChar')
    fldChar1.set(qn('w:fldCharType'), 'begin')
    instrText = OxmlElement('w:instrText')
    instrText.text = 'PAGE'
    fldChar2 = OxmlElement('w:fldChar')
    fldChar2.set(qn('w:fldCharType'), 'end')
    run._r.append(fldChar1)
    run._r.append(instrText)
    run._r.append(fldChar2)

footer_para.add_run('第 ')
add_page_number(footer_para)
footer_para.add_run(' 页')

doc.add_paragraph('文档正文内容……')
doc.save('with_header_footer.docx')

文档合并(多个 docx 合并为一个)

merge_docs.py

from docx import Document
from docx.oxml.ns import qn
from docx.oxml import OxmlElement

def add_page_break(doc):
    """在文档末尾添加分页符"""
    para = doc.add_paragraph()
    run = para.add_run()
    br = OxmlElement('w:br')
    br.set(qn('w:type'), 'page')
    run._r.append(br)

def merge_documents(file_list, output_path):
    """合并多个 Word 文件为一个"""
    merged = Document()
    # 清除默认空段落
    for element in merged.element.body:
        merged.element.body.remove(element)

    for i, filepath in enumerate(file_list):
        sub_doc = Document(filepath)
        # 将子文档的所有元素复制到合并文档
        for element in sub_doc.element.body:
            merged.element.body.append(element)
        # 每个文档后添加分页(最后一个除外)
        if i < len(file_list) - 1:
            add_page_break(merged)

    merged.save(output_path)
    print(f'已合并 {len(file_list)} 个文件
# 使用示例
from pathlib import Path
doc_files = sorted(Path('output/notices').glob('*.docx'))
merge_documents(doc_files, 'all_notices_merged.docx')

搜索与替换

Run 断点问题:为什么简单替换会失败

这是 Word 自动化中最常见的坑。当你在 Word 文档里写了 {{"{{"}}name{{"}}"}},Word 内部可能把它存储为三个 Run:&#123;&#123;name&#125;&#125;。直接用 paragraph.text.replace() 看不到这三个 Run 拼在一起,所以替换失败。

safe_replace.py — 处理 Run 断点的安全替换函数

from docx import Document
import re

def replace_in_paragraph(paragraph, old_text, new_text):
    """
    在段落内跨 Run 安全替换文本。
    先把所有 Run 合并成一个字符串查找,找到后重写第一个 Run,清空其余。
    """
    full_text = ''.join(run.text for run in paragraph.runs)
    if old_text not in full_text:
        return

    new_full = full_text.replace(old_text, new_text)
    # 把新文本写入第一个 Run,清空其余 Run
    if paragraph.runs:
        paragraph.runs[0].text = new_full
        for run in paragraph.runs[1:]:
            run.text = ''

def replace_in_document(doc, replacements: dict):
    """在整个文档中批量替换关键词"""
    # 替换正文段落
    for paragraph in doc.paragraphs:
        for old, new in replacements.items():
            replace_in_paragraph(paragraph, old, new)

    # 替换表格中的文本
    for table in doc.tables:
        for row in table.rows:
            for cell in row.cells:
                for paragraph in cell.paragraphs:
                    for old, new in replacements.items():
                        replace_in_paragraph(paragraph, old, new)

    # 替换页眉页脚
    for section in doc.sections:
        for paragraph in section.header.paragraphs:
            for old, new in replacements.items():
                replace_in_paragraph(paragraph, old, new)

# 使用示例
doc = Document('contract_template.docx')
replace_in_document(doc, {
    '{{"{{"}}company_name{{"}}"}}': '北京示例科技有限公司',
    '{{"{{"}}client_name{{"}}"}}':  '上海采购方有限公司',
    '{{"{{"}}amount{{"}}"}}':       '128,000',
    '{{"{{"}}start_date{{"}}"}}':   '2025年1月1日',
})
doc.save('contract_filled.docx')

**推荐优先使用 docxtpl:**手写替换函数容易遗漏边界情况(如页眉页脚、文本框内的文本)。docxtpl 内部已处理好 Run 断点问题,适合批量场景。只有在无法修改模板文件格式时,才考虑手写替换函数。

Word 转 PDF

不同操作系统有不同的最优方案:

Windows:win32com.client(最可靠,格式保真度最高)

word_to_pdf_windows.py

import win32com.client
from pathlib import Path

def word_to_pdf_windows(docx_path, pdf_path=None):
    """使用 Word COM 接口转换,格式与 Word 打印完全一致"""
    docx_path = Path(docx_path).resolve()
    if pdf_path is None:
        pdf_path = docx_path.with_suffix('.pdf')
    pdf_path = Path(pdf_path).resolve()

    word = win32com.client.Dispatch('Word.Application')
    word.Visible = False
    try:
        doc = word.Documents.Open(str(docx_path))
        doc.SaveAs(str(pdf_path), FileFormat=17)  # 17 = wdFormatPDF
        doc.Close()
        print(f'转换成功:{pdf_path.name}')
    finally:
        word.Quit()

# 批量转换
from pathlib import Path
for docx_file in Path('output/notices').glob('*.docx'):
    word_to_pdf_windows(docx_file)

Mac / Linux:LibreOffice 命令行

word_to_pdf_linux_mac.py

import subprocess
from pathlib import Path

def word_to_pdf_libreoffice(docx_path, output_dir=None):
    """
    使用 LibreOffice 命令行转换(需提前安装 LibreOffice)
    Mac 安装:brew install --cask libreoffice
    Ubuntu:sudo apt install libreoffice
    """
    docx_path = Path(docx_path).resolve()
    if output_dir is None:
        output_dir = docx_path.parent

    cmd = [
        'libreoffice',
        '--headless',           # 不打开 GUI
        '--convert-to', 'pdf',  # 转换格式
        '--outdir', str(output_dir),
        str(docx_path),
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode == 0:
        print(f'转换成功:{docx_path.stem}.pdf')
    else:
        print(f'转换失败:{result.stderr}')

# 使用
word_to_pdf_libreoffice('contract_filled.docx')

跨平台:docx2pdf 库

终端 + word_to_pdf_cross.py

# 安装
# pip install docx2pdf

from docx2pdf import convert
from pathlib import Path

# 单文件转换
convert('contract_filled.docx', 'contract_filled.pdf')

# 批量转换整个目录
convert('output/notices/')  # 自动将目录下所有 .docx 转为 .pdf

**docx2pdf 依赖:**在 Windows 上它调用 Word COM 接口(需安装 Microsoft Word);在 Mac 上调用 Word for Mac;在 Linux 上需要 LibreOffice。没有对应软件则会报错。无论哪种方法,格式保真度都与本地安装的 Word/LibreOffice 版本有关。

实战项目:合同批量生成系统

场景:公司需要给 50 个客户分别生成定制合同,Excel 里存有客户信息,Word 模板已做好,要求批量生成后按客户名归档,同时生成 PDF 副本。

contract_batch_system.py — 完整合同批量生成系统(约80行)

"""
合同批量生成系统
依赖:pip install python-docx docxtpl pandas openpyxl docx2pdf
"""
import pandas as pd
from docxtpl import DocxTemplate
from pathlib import Path
from datetime import date
import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

# ── 配置 ──
TEMPLATE_PATH = 'templates/contract_template.docx'
DATA_PATH     = 'data/clients.xlsx'
OUTPUT_ROOT   = Path('output/contracts')
CONVERT_PDF   = True  # 是否同时生成 PDF

def load_client_data(excel_path: str) -> pd.DataFrame:
    """读取客户数据,确保必要列存在"""
    df = pd.read_excel(excel_path)
    required_cols = ['客户名称', '联系人', '合同金额', '服务期限', '签署日期']
    missing = [c for c in required_cols if c not in df.columns]
    if missing:
        raise ValueError(f'Excel 缺少以下列:{missing}')
    logger.info(f'读取到 {len(df)} 条客户数据')
    return df

def generate_contract(tpl: DocxTemplate, row: pd.Series, output_dir: Path):
    """为单个客户生成合同"""
    client_name = row['客户名称']

    # 每个客户独立目录
    client_dir = output_dir / client_name
    client_dir.mkdir(parents=True, exist_ok=True)

    context = {
        'client_name':   client_name,
        'contact':       row['联系人'],
        'amount':        f"{row['合同金额']:,.2f}",
        'service_term':  row['服务期限'],
        'sign_date':     str(row['签署日期'])[:10],
        'today':         date.today().strftime('%Y年%m月%d日'),
        'contract_no':   f"HT-{date.today().year}-{row.name+1:04d}",
    }

    # 渲染并保存 Word
    tpl.render(context)
    docx_path = client_dir / f"{client_name}_服务合同.docx"
    tpl.save(docx_path)

    # 转换 PDF
    if CONVERT_PDF:
        try:
            from docx2pdf import convert
            pdf_path = client_dir / f"{client_name}_服务合同.pdf"
            convert(str(docx_path), str(pdf_path))
            logger.info(f'  [OK] {client_name} — Word + PDF')
        except Exception as e:
            logger.warning(f'  [PDF失败] {client_name}: {e}')
    else:
        logger.info(f'  [OK] {client_name} — Word')

def main():
    OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)

    # 加载数据和模板
    df = load_client_data(DATA_PATH)
    tpl = DocxTemplate(TEMPLATE_PATH)

    # 批量生成
    errors = []
    for _, row in df.iterrows():
        try:
            generate_contract(tpl, row, OUTPUT_ROOT)
        except Exception as e:
            errors.append((row.get('客户名称', '未知'), str(e)))
            logger.error(f'  [错误] {row.get("客户名称")}: {e}')

    # 汇报结果
    success = len(df) - len(errors)
    logger.info(f'\n生成完成:成功 {success} 份,失败 {len(errors)} 份')
    logger.info(f'文件保存至:{OUTPUT_ROOT.resolve()}')
    if errors:
        logger.warning('失败列表:')
        for name, err in errors:
            logger.warning(f'  {name}: {err}')

if __name__ == '__main__':
    main()

系统亮点: 模板与数据完全分离,非技术人员可以独立维护 Word 模板 每个客户独立目录,归档清晰,便于后期查找 自动生成合同编号(年份 + 序号) 完善的错误处理——单个客户失败不影响其他客户 PDF 转换失败不阻断整体流程,仅记录警告

上一章

下一章
第8章:PDF 自动化处理
本章评分
4.6  / 5  (44 评分)

💬 留言讨论