最直接方案是用 Jinja2 渲染 HTML 模板再经 WeasyPrint 转 PDF;避免 docx 二进制操作、pdfkit/reportlab;动态计算逻辑放 Python 层注册为过滤器;模板结构锁死,仅通过表单输入业务字段。

用 jinja2 填充合同模板最直接,别碰 docx 二进制操作
硬改 Word 文件(比如用 python-docx 插文本)容易错位、丢样式、崩表格。实际项目里,90% 的合同生成需求用纯文本模板 + jinja2 就够了——导出为 PDF 或打印前转成 HTML,稳定又可控。
实操建议:
- 把合同正文存成
contract_template.html,用{{ party_a_name }}、{% if is_confidential %}保密条款生效{% endif %}这类语法标记变量和逻辑 - 用
jinja2.Environment(loader=jinja2.FileSystemLoader("templates"))加载,避免路径错误 - 渲染前务必调用
template.render(**data_dict),别漏掉双星号解包,否则变量全为空 - 敏感字段(如金额、身份证号)在传入
data_dict前做校验,jinja2不负责数据合法性
PDF 导出选 weasyprint,不是 pdfkit 或 reportlab
pdfkit 依赖系统级 wkhtmltopdf,部署时容易因版本/字体/权限挂掉;reportlab 要手写布局,改个段落间距就得调半天。而 weasyprint 直接吃 HTML/CSS,支持 @page、中文断行、页眉页脚,且纯 Python 实现,pip install weasyprint 后开箱即用。
注意点:
- CSS 中用
@font-face指定中文字体(如 Noto Sans CJK),否则 PDF 里一堆方框 - 避免用 Flex/Grid 布局,
weasyprint对现代 CSS 支持有限,老老实实用float或表格 - 页码用
@page { @bottom-center { content: "第 " counter(page) " 页"; } },别试图用 JS 注入 - 生成失败时看终端报错——常见是字体路径不对或 HTML 有未闭合标签
动态条款要靠 Python 函数注入,不是全塞在模板里
比如「违约金按日 0.05% 计算,上限不超过合同总额 20%」这种带计算的逻辑,硬写进 Jinja 模板会变面条代码,难测难调。正确做法是在 Python 层写函数,再注册进 Jinja 环境。
示例:
def calc_liquidated_damages(amount, days):
base = amount * 0.0005 * days
return min(base, amount * 0.2)
<p>env = jinja2.Environment(...)
env.filters['liquidated'] = calc_liquidated_damages</p><h1>模板里写:{{ contract_amount | liquidated(contract_days) | round(2) }}</h1><p></p>
这样做的好处:
- 计算逻辑可单元测试,模板只管展示
- 不同合同类型(买卖/服务/劳务)可注册不同函数,模板保持复用
- 后续加审计日志、四舍五入规则、税率适配,改 Python 函数就行,不动模板
千万别让业务方直接改 HTML 模板
给销售或法务一个 .html 文件让他们“自己填空”,不出三天就会出现 套三层 div、CSS 写成 style="font-size:14px; font-size:16px;" 的现场。合同是法律文件,格式失效可能影响效力。
必须加一层管控:
- 提供 Web 表单(哪怕只是 Flask 的几个
),只暴露业务字段,模板结构锁死 - 所有变量名用下划线分隔(如
signing_date),禁止驼峰,减少前端拼错 - 上线前用固定测试数据跑一遍生成流程,检查 PDF 页数、签名栏位置、金额小数位——人工扫一眼比写自动化断言更快
- 留个
debug_mode=True开关,开启时在 PDF 页脚打上「测试版-20240521」水印
合同生成真正的难点不在技术,而在字段定义是否穷尽、边界条件是否对齐法务意见、历史版本如何归档。工具只是执行者,别让它替人做判断。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











