Python Word模板渲染数据批量生成报告
用python-docx做Word模板占位符替换,从Excel/数据库读数据批量生成个性化合同、报告、通知书,模板一次做好,N份数据自动出N份Word。
场景痛点
每月要给客户发对账单、给员工发工资条通知、给代理商发授权书,格式都一样,只是姓名、金额、日期不同。手动复制模板改数据,几十上百份眼睛都看花。正确做法是先在Word里做好一份模板,占位符用 {{姓名}} 这种标记,然后Python读数据批量替换,一次出几十份。
用到的库
pip install python-docx pandas openpyxl
完整代码
# -*- coding: utf-8 -*-
"""
Word 模板占位符批量渲染
模板里用 {{字段名}} 作为占位符,代码读数据字典批量替换
"""
from pathlib import Path
import copy
import pandas as pd
from docx import Document
from docx.oxml.ns import qn
def replace_in_runs(paragraph, mapping):
"""
替换段落里的占位符。
一个占位符可能跨多个 run,所以先拼整段文本替换,再重写 runs。
保留第一个 run 的格式。
"""
if not paragraph.runs:
return
full_text = paragraph.text
if "{{" not in full_text:
return
new_text = full_text
for key, value in mapping.items():
new_text = new_text.replace("{{" + key + "}}", str(value))
if new_text == full_text:
return
# 保留第一个 run 的格式
first_run = paragraph.runs[0]
base_font = {
"name": first_run.font.name,
"size": first_run.font.size,
"bold": first_run.font.bold,
"color": first_run.font.color.rgb if first_run.font.color and first_run.font.color.type else None,
}
# 清空所有 run
for run in list(paragraph.runs):
run._element.getparent().remove(run._element)
# 新建一个 run 写入新文本
new_run = paragraph.add_run(new_text)
if base_font["name"]:
new_run.font.name = base_font["name"]
new_run._element.rPr.rFonts.set(qn('w:eastAsia'), base_font["name"])
if base_font["size"]:
new_run.font.size = base_font["size"]
if base_font["bold"] is not None:
new_run.font.bold = base_font["bold"]
if base_font["color"]:
new_run.font.color.rgb = base_font["color"]
def render_template(template_path: Path, output_path: Path, mapping: dict):
"""用 mapping 字典渲染模板,输出到 output_path"""
doc = Document(str(template_path))
# 正文段落
for para in doc.paragraphs:
replace_in_runs(para, mapping)
# 表格单元格
for table in doc.tables:
for row in table.rows:
for cell in row.cells:
for para in cell.paragraphs:
replace_in_runs(para, mapping)
# 页眉页脚
for section in doc.sections:
for hf in (section.header, section.footer):
for para in hf.paragraphs:
replace_in_runs(para, mapping)
doc.save(str(output_path))
def make_template(path):
"""造一份演示模板"""
doc = Document()
doc.add_heading("{{公司名称}} 对账单", level=0)
doc.add_paragraph("致:{{客户名称}} 先生/女士")
doc.add_paragraph("感谢您与我司合作。截至 {{对账日期}},贵司本期往来明细如下:")
table = doc.add_table(rows=4, cols=2)
table.style = "Light Grid Accent 1"
table.cell(0, 0).text = "合同编号"
table.cell(0, 1).text = "{{合同编号}}"
table.cell(1, 0).text = "本期应收金额"
table.cell(1, 1).text = "{{应收金额}} 元"
table.cell(2, 0).text = "已收款金额"
table.cell(2, 1).text = "{{已收金额}} 元"
table.cell(3, 0).text = "应收余额"
table.cell(3, 1).text = "{{余额}} 元"
doc.add_paragraph("请于 {{付款截止日}} 前完成付款,否则将按日万分之五计收违约金。")
doc.add_paragraph("联系人:{{联系人}} {{联系电话}}")
doc.save(str(path))
def batch_render(excel_path, template_path, output_dir):
df = pd.read_excel(excel_path)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"共 {len(df)} 条数据,模板:{template_path.name}")
for _, row in df.iterrows():
mapping = row.to_dict()
# 输出文件名用客户名称
filename = f"对账单_{mapping.get('客户名称', '未命名')}.docx"
out = output_dir / filename
try:
render_template(Path(template_path), out, mapping)
print(f"✓ {filename}")
except Exception as e:
print(f"✗ 失败:{e}")
if __name__ == "__main__":
# 1. 先造模板(实际使用时在Word里做好模板,占位符写成 {{字段名}})
make_template("./template_duizhang.docx")
# 2. 造演示数据
demo = pd.DataFrame([
{"公司名称": "宝鸡卓亚技术服务部", "客户名称": "西安XX贸易公司",
"对账日期": "2026-09-30", "合同编号": "HT-2026-001",
"应收金额": "58000", "已收金额": "20000", "余额": "38000",
"付款截止日": "2026-10-15", "联系人": "黄文康", "联系电话": "13800000001"},
{"公司名称": "宝鸡卓亚技术服务部", "客户名称": "咸阳YY建材公司",
"对账日期": "2026-09-30", "合同编号": "HT-2026-002",
"应收金额": "120000", "已收金额": "60000", "余额": "60000",
"付款截止日": "2026-10-20", "联系人": "黄文康", "联系电话": "13800000002"},
])
demo.to_excel("./duizhang_data.xlsx", index=False)
# 3. 批量渲染
batch_render(
excel_path="./duizhang_data.xlsx",
template_path="./template_duizhang.docx",
output_dir="./duizhang_output",
)
代码讲解
模板里的占位符统一用 {{字段名}} 格式。replace_in_runs() 先把段落整段文本拼起来做字符串替换,再重写run。这样解决了"占位符跨多个run"的问题(Word经常把一个词拆成多个run,直接在run里replace会漏)。
替换范围覆盖三处:正文段落、表格单元格段落、页眉页脚。对账单这种场景,页眉公司名、页脚页码都可能要渲染。
batch_render() 用pandas读Excel,每一行就是一份渲染数据,逐行调用 render_template(),输出文件名用客户名称区分。
运行结果
duizhang_output 目录里出现 对账单_西安XX贸易公司.docx、对账单_咸阳YY建材公司.docx。打开看,模板里所有 {{xxx}} 都被替换成了Excel里的实际值,表格、标题、联系方式都正确。控制台打印每个文件生成成功或失败。
注意事项
- 模板里的占位符
{{字段名}}必须和Excel列名完全一致,包括大小写和空格。建议用英文列名,在Excel里加一行中文注释。 - 本脚本的替换会丢失段落内的局部格式(因为重写了run),整段统一格式没问题,段落内有"部分加粗"的占位符会被拉平。要保留局部格式得用更复杂的run级替换。
- 想做"循环行"(比如对账单里N条明细)这种重复表格行,本脚本不支持,需要写"行模板复制"逻辑。
- 替换后保存到新文件,模板原文件不动,安全。
- 字段值里如果包含
{{或}}字符串会被误替换,业务数据里尽量避免。

更新时间:2026-09-14 21:43:17
上一篇:Python Word批量转PDF:一键把docx转成PDF