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 的层级结构,是写好代码的前提:
- Document:整个 .docx 文件,所有内容的根容器
- Paragraph(段落):文档由一个个段落组成,包含文字、标题、列表项等
- Run(文字块):段落内连续样式相同的文字片段。一个段落可以有多个 Run,每个 Run 的字体、颜色、加粗状态可以独立设置
- Table(表格):由行(Row)和单元格(Cell)组成,单元格内包含段落
**关键认知:**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 模板文件里直接写标签:
{{ "{{" }}变量名{{ "}}" }}:输出变量值{{ "{{" }}% if 条件 %{{ "}}" }} ... {{ "{{" }}% endif %{{ "}}" }}:条件判断{{ "{{" }}% for item in 列表 %{{ "}}" }} ... {{ "{{" }}% endfor %{{ "}}" }}:循环
**如何在 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:{{、name、}}。直接用 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 自动化处理