本文最后更新于:2025年11月9日 晚上

在需要根据特定模板生成定制化文档的场景中,Word 模板是一种直观可靠的实现方式,本文通过将 json 中的配置信息以表格的形式展示在Word的案例,介绍如何利用docxtplJinja2这些Python库来实现基于现有的Word模板生成个性化的文档。

简介

我们的目标是构建一个带有多种格式配置、可编辑占位符的 Word 模板后,通过 Json 格式的信息将 Word 模板丰富成一个真正有意义的文档文件。

为此我们需要以下工作步骤:

  1. 构建通用、灵活、完备的 Word 模板
  2. 为模板组织相应的数据
  3. 调用 Python 脚本,将数据插入到模板中生成 Word 文件
  4. 将 Word 文件转换为 PDF

Jinja2

Jinja2 是一个现代的,设计者友好的,仿照 Django 模板的 Python 模板语言。 它速度快,被广泛使用,并且提供了可选的沙箱模板执行环境保证安全:

我们需要在 Word 模板中加入 Jinja2 的代码。

docxtpl

正常的Jinja2语法只有%的普通标签,而docxtpl的类语法包含%p,%tr,%tc,%r:

1
2
3
4
{%p jinja2_tag %} for paragraphs 段落,对应docx.text.paragraph.Paragraph对象
{%tr jinja2_tag %} for table rows 表格中的一行,对应docx.table._Row对象
{%tc jinja2_tag %} for table columns 表格中的一列,对应docx.table._Column对象
{%r jinja2_tag %} for runs 段落中的一个片段,对应docx.text.run.Run对象

通过使用这些标记,python-docx-template将真正的Jinja2标记放入文档的XML源代码中的正确位置。

PS:这四种标签,起始标签不能在同一行,必须在不同的行上面,否则无法正确渲染。

例如:

1
{%p if display_paragraph %}Here is my paragraph {%p endif %}

需改写成:

1
2
3
{%p if display_paragraph %}
Here is my paragraph
{%p endif %}
  • pip 安装:
1
pip install docxtpl
  • conda 安装:
1
conda install docxtpl --channel conda-forge
  • 使用:
1
2
3
4
5
6
from docxtpl import DocxTemplate

doc = DocxTemplate("my_word_template.docx")
context = { 'company_name' : "World company" }
doc.render(context)
doc.save("generated_doc.docx")

模板构建、使用流程

合并单元格

  • 水平合并单元格

在for循环中要合并的单元格内容前面补充:

1
{% hm %}
  • 垂直合并单元格

在for循环中要合并的单元格内容前面补充:

1
{% vm %}

Word文档模板

我们需要创建一个包含占位符的Word文档模板 (docs 格式)。这些占位符将在后续的文档生成过程中被实际内容替换。使用 Jinja2 的模板语法,我们可以定义占位符和可替换的内容。例如,可以使用 {{ name }} 表示一个占位符。

这里我给出一个我随便生成的一个模板

是在 Linux 下 LibreOffice Writer 生成的 docx 文件,windows 打开可能会有乱码。

示例模板

数据准备

需要准备根据模板中的字段信息匹配的 Json 数据(字典)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
context = {
'title': '项目报告',
'user': {
'name': '张三',
'email': 'zhangsan@example.com'
},
'tasks': [
{'description': '需求分析', 'deadline': '2023-12-31'},
{'description': '代码开发', 'deadline': '2024-01-15'}
],
'special_note': '请尽快完成初稿评审',
'name': '项目经理',
'date': '2025-08-04',
'employees': [
{'name': '张三', 'department': '研发部', 'hire_date': '2020-01-15', 'salary': 15000},
{'name': '李四', 'department': '市场部', 'hire_date': '2019-05-20', 'salary': 12000},
{'name': '王五', 'department': '财务部', 'hire_date': '2021-03-10', 'salary': 18000},
],
"node_config": {
"ip1": {
"check_hostnamectl": {
"hostname": "node01",
"operating_system": "Tencent tlinux 2.6",
"kernel": "Linux 5.4.119-1-tlinux4-0010",
"architecture": "x86-64"
},
"check_cpu_metrics": {
"cpu_num": "4C",
"model_name": "Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz"
},
"check_physical_cpu": {
"physical_cpu": "4C"
},
"check_physical_mem": {
"physical_mem": "8G"
},
"check_nvme": {
"nvme_size": "1.80TB*4"
}
},
"ip2": {
"check_hostnamectl": {
"hostname": "node02",
"operating_system": "Tencent tlinux 2.6",
"kernel": "Linux 5.4.119-1-tlinux4-0010",
"architecture": "x86-64"
},
"check_cpu_metrics": {
"cpu_num": "4C",
"model_name": "Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz"
},
"check_physical_cpu": {
"physical_cpu": "4C"
},
"check_physical_mem": {
"physical_mem": "8G"
},
"check_nvme": {
"nvme_size": "1.80TB*4"
}
},
"ip3": {
"check_hostnamectl": {
"hostname": "node03",
"operating_system": "Tencent tlinux 2.6",
"kernel": "Linux 5.4.119-1-tlinux4-0010",
"architecture": "x86-64"
},
"check_cpu_metrics": {
"cpu_num": "4C",
"model_name": "Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz"
},
"check_physical_cpu": {
"physical_cpu": "4C"
},
"check_physical_mem": {
"physical_mem": "8G"
},
"check_nvme": {
"nvme_size": "1.80TB*4"
}
}
},
"test_vm": "AAA"
}

渲染文档

使用 docxtplJinja2 来将数据填充到文档模板中,并生成最终的文档。

首先,我们需要加载模板文件并创建一个DocxTemplate对象。然后,我们将数据传递给模板对象,使用render方法渲染文档。最后,可以选择将文档保存到本地文件或直接进行下载。

针对上文中的模板文件,提供以下示例代码:

需要准备一张 test.png 图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from docxtpl import DocxTemplate, InlineImage
from docx.shared import Pt
import os


# 数据准备
context = {
'title': '项目报告',
'user': {
'name': '张三',
'email': 'zhangsan@example.com'
},
'tasks': [
{'description': '需求分析', 'deadline': '2023-12-31'},
{'description': '代码开发', 'deadline': '2024-01-15'}
],
'special_note': '请尽快完成初稿评审',
'name': '项目经理',
'date': '2025-08-04',
'employees': [
{'name': '张三', 'department': '研发部', 'hire_date': '2020-01-15', 'salary': 15000},
{'name': '李四', 'department': '市场部', 'hire_date': '2019-05-20', 'salary': 12000},
{'name': '王五', 'department': '财务部', 'hire_date': '2021-03-10', 'salary': 18000},
],
"node_config": {
"ip1": {
"check_hostnamectl": {
"hostname": "node01",
"operating_system": "Tencent tlinux 2.6",
"kernel": "Linux 5.4.119-1-tlinux4-0010",
"architecture": "x86-64"
},
"check_cpu_metrics": {
"cpu_num": "4C",
"model_name": "Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz"
},
"check_physical_cpu": {
"physical_cpu": "4C"
},
"check_physical_mem": {
"physical_mem": "8G"
},
"check_nvme": {
"nvme_size": "1.80TB*4"
}
},
"ip2": {
"check_hostnamectl": {
"hostname": "node02",
"operating_system": "Tencent tlinux 2.6",
"kernel": "Linux 5.4.119-1-tlinux4-0010",
"architecture": "x86-64"
},
"check_cpu_metrics": {
"cpu_num": "4C",
"model_name": "Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz"
},
"check_physical_cpu": {
"physical_cpu": "4C"
},
"check_physical_mem": {
"physical_mem": "8G"
},
"check_nvme": {
"nvme_size": "1.80TB*4"
}
},
"ip3": {
"check_hostnamectl": {
"hostname": "node03",
"operating_system": "Tencent tlinux 2.6",
"kernel": "Linux 5.4.119-1-tlinux4-0010",
"architecture": "x86-64"
},
"check_cpu_metrics": {
"cpu_num": "4C",
"model_name": "Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz"
},
"check_physical_cpu": {
"physical_cpu": "4C"
},
"check_physical_mem": {
"physical_mem": "8G"
},
"check_nvme": {
"nvme_size": "1.80TB*4"
}
}
},
"test_vm": "AAA"
}

# 加载模板
doc = DocxTemplate("template.docx")

# 创建InlineImage对象并添加到context
context['my_image'] = InlineImage(doc, 'test.png', width=Pt(100)) # Pt(100) 表示一个 100 磅 的长度。
# 在排版和印刷领域,1 磅 (Pt) = 1/72 英寸 ≈ 0.0353 厘米 ≈ 0.3527 毫米 1 英寸 = 2.54 厘米
context['my_image_2'] = InlineImage(doc, 'test.png', width=Pt(300))

# 渲染Word文档
doc.render(context)
output_docx = "output.docx"
doc.save(output_docx)

# 转换为PDF
def convert_to_pdf(docx_path):
pdf_path = docx_path.replace('.docx', '.pdf')
cmd = f"libreoffice --headless --convert-to pdf {docx_path} --outdir ."
os.system(cmd)
return pdf_path

pdf_output = convert_to_pdf(output_docx)
print(f"PDF生成成功: {pdf_output}")

效果展示

上述流程会生成一个 Word 文件 和一个 PDF 文件

多模板拼接

若最终生成的 Word 是由多个.docx模板拼接而成,可以使用下述代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import json  
import os
from docxtpl import DocxTemplate
from docx import Document
from docxcompose.composer import Composer


def generate_word(input_path, output_path):
# 定义模板文件路径列表
path_lst = ["/path/to/template-1.docx",
"/path/to/template-2.docx",
"/path/to/template-3.docx"]

# 将模板文件路径列表加载为 DocxTemplate 对象列表
doc_lst = [DocxTemplate(i) for i in path_lst]
# 定义一个存储临时文件路径的列表
rm_lst = []
# 读取输入数据文件
with open(input_path, "r") as f:
input_data = json.load(f)
# 定义一个文档组合器对象
composer = None
# 遍历模板对象列表
for index, docx in enumerate(doc_lst):
# 指定临时文档路径
docx_path = "{}-test.docx".format(index)
# 将临时文档路径添加到删除列表中
rm_lst.append(docx_path)
# 渲染模板文档
docx.render(input_data)
# 保存渲染后的文档
docx.save(docx_path)

# 加载临时文档作为 Document 对象
docx_context = Document(docx_path)
# 判断是否为第一个文档,如果是则直接赋值给组合器,否则追加到组合器中
if index == 0:
composer = Composer(docx_context)
else:
composer.append(docx_context)
# 保存组合后的文档
composer.save(output_path)
# 删除临时文件
for path in rm_lst:
os.remove(path)


if __name__ == "__main__":
input_path = "/path/to/config.json"
out_path = "/path/to/config.docx"

try:
generate_word(input_path, out_path)
print("生成 Word 文件成功")
except Exception as e:
print("生成 Word 文件失败: {}".format(e))

进阶用法

数学运算

模板中支持如下运算:

算术运算

  • +:加法 {{ a + b }}
  • -:减法 {{ a - b }}
  • *:乘法 {{ a * b }}
  • /:除法 {{ a / b }} (结果是浮点数)
  • //:整除 {{ a // b }} (结果是整数)
  • %:取模 {{ a % b }}
  • **:幂运算 {{ a ** 2 }}

比较运算 (常用于 if 语句)

  • ==:等于
  • !=:不等于
  • >:大于
  • >=:大于等于
  • <:小于
  • <=:小于等于

逻辑运算 (常用于 if 语句)

  • and
  • or
  • not
  • ( ):用于改变优先级

其他常用操作

  • 字符串连接: {{ "Hello " + name }}
  • 列表/元组索引: {{ my_list[0] }}
  • 字典键值访问:
    • {{ my_dict['key'] }}
    • {{ my_dict.key }} (如果键是有效的标识符)
  • 获取属性: {{ obj.attribute }}
  • 函数调用 (需在上下文中传入函数): {{ get_full_name(first, last) }}
  • 三元表达式: {{ "Yes" if condition else "No" }}
1
a = {{a}}, b = {{b}}

数据填入 a b 即可计算出 a/b

虽然 Jinja2 支持这些运算,但模板的主要职责是展示,复杂的业务逻辑最好在 Python 代码中处理完毕,然后将结果通过 context 传入模板,以保持模板的简洁和可读性。

自定义过滤器

模板渲染过程中可以自定义函数来处理数据,大大增加模板灵活性。

函数为 Python 中定义:

1
2
3
def filter(value, arg):
return_value = value + ' ' + arg
return return_value

模板的用法:

1
{{value | filter(arg)}}

过滤器的第一个参数为 | 左边的变量,如果需要输入其他参数,可以在函数后面追加其他变量。

  • 示例:
1
2
3
4
The float value is {{ base_value_float }}
The string value is {{ base_value_string }}
The filter modified float value is {{ base_value_float | my_filterB(2)}}
The filter modified string value is {{ base_value_string | my_filterA(‘Deric’)}}

过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from docxtpl import DocxTemplate
import jinja2

jinja_env = jinja2.Environment()

# to create new filters, first create functions that accept the value to filter
# as first argument, and filter parameters as next arguments
def my_filterA(value, my_string_arg):
return_value = value + ' ' + my_string_arg
return return_value

def my_filterB(value, my_float_arg):
return_value = value + my_float_arg
return return_value

# Then, declare them to jinja like this :
jinja_env.filters['my_filterA'] = my_filterA
jinja_env.filters['my_filterB'] = my_filterB


context = {'base_value_string': ' Hello', 'base_value_float': 1.5}

tpl = DocxTemplate('templates/custom_jinja_filters_tpl.docx')
tpl.render(context, jinja_env)
tpl.save('output/custom_jinja_filters.docx')
# “|”作为管道符,将左侧的值传到到右侧函数,作为右侧函数的第一个参数

多元素操作

这里以求和操作为例,列举多种多元素操作方式。

内置过滤器

jinja2 拥有 sum 基础过滤器,可以用于多元素处理

  • 模板代码
1
2
数字列表的总和是:{{ numbers | sum }}
总支出:{{ expenses | sum(attribute='amount') }}
  • Python 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from docxtpl import DocxTemplate
import jinja2

# 准备上下文数据
context = {
'numbers': [10, 20, 30, 40, 50],
'expenses': [
{'amount': 100, 'category': 'food'},
{'amount': 200, 'category': 'transport'},
{'amount': 150, 'category': 'entertainment'},
]
}

# 创建文档并渲染
doc = DocxTemplate("your_template.docx")
doc.render(context)
doc.save("output.docx")
for 循环

如果需要更复杂的求和逻辑,可以使用循环和自定义变量。

  • 模板代码

    在 Jinja2 中,{% set %} 语句在循环内部创建的是局部变量,而不是修改外部作用域的变量

    需要通过创建一个命名空间对象,确保我们在修改的是同一个变量。

1
2
3
4
5
6
{% set ns = namespace(total=0) %}
{% for expense in expenses %}
{% set ns.total = ns.total + expense.amount %}
当前累计支出:{{ ns.total }}
{% endfor %}
总支出:{{ ns.total }}
自定义过滤器

自定义 Python 函数:

1
2
3
4
5
6
7
8
9
10
# 自定义过滤器:条件求和
def sum_if(items, attribute=None, condition=None):
total = 0
for item in items:
value = item[attribute] if attribute else item
# 如果没有条件或者条件满足,则累加
if condition is None or condition(item):
total += value
return total

  • 模板代码
1
所有支出总和:{{ expenses | sum_if('amount') }}

效果测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

公式运算示例:
a = {{a}}, b = {{b}}

算术运算
◦ +:加法 {{ a + b }}
◦ -:减法 {{ a - b }}
◦ *:乘法 {{ a * b }}
◦ /:除法 {{ a / b }} (结果是浮点数)
◦ //:整除 {{ a // b }} (结果是整数)
◦ %:取模 {{ a % b }}
◦ **:幂运算 {{ a ** 2 }}

==:等于 {{a==b}}
• !=:不等于 {{a!=b}}
• >:大于 {{a>b}}
• >=:大于等于{{a>=b}}
• <:小于 {{a<b}}
• <=:小于等于 {{a<=b}}


逻辑运算 (常用于 if 语句)
• and {{a and b}}
• or {{a or b}}
• not {{not a}}
• ( ):用于改变优先级 {{(a + b) * b}}




字符串连接: {{ s1 + " ----- " + s2 }}
列表/元组索引: {{ my_list[0] }}
• 字典键值访问:
◦ {{ my_dict['key'] }}
◦ 或 {{ my_dict.key }} (如果键是有效的标识符)
• 获取属性: {{ obj.test_attr }}
• 函数: {{ first | get_full_name(last) }}
• 三元表达式: {{ "Yes" if a > 1 else "No" }}


百分比表示:{{ a, b | my_filterA }}


多元素求和

1. 内置过滤器
数字列表的总和是:{{ numbers | sum }}
总支出:{{ expenses | sum(attribute='amount') }}


2. for 循环自定义变量
{% set ns = namespace(total=0) %}
{% for expense in expenses %}
{% set ns.total = ns.total + expense.amount %}
当前累计支出:{{ ns.total }}
{% endfor %}
总支出:{{ ns.total }}


3. 自定义过滤器
所有支出总和:{{ expenses | sum_if('amount') }}

  • Python 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from docxtpl import DocxTemplate, InlineImage
from docx.shared import Pt
import os
import jinja2


# 添加全局函数而不是过滤器
def calculate_percentage_vvd(a):
return f"{(a ) * 100:.2f}%"

class Test:
pass


test_obj = Test()
test_obj.test_attr = "test_attr"

# 自定义过滤器:条件求和
def sum_if(items, attribute=None, condition=None):
total = 0
for item in items:
value = item[attribute] if attribute else item
# 如果没有条件或者条件满足,则累加
if condition is None or condition(item):
total += value
return total


# 数据准备
context = {
"test_vm": "AAA",
"a": 1,
"b": 4,
"s1": 'test_str1',
"s2": 'test_str2',
"my_list": [1, 2, 3, 4, 5],
"my_dict": {
"key": "dict_key",
},
"obj": test_obj,
"first": "John",
"last": "Doe",
'numbers': [10, 20, 30, 40, 50],
'expenses': [
{'amount': 100, 'category': 'food'},
{'amount': 200, 'category': 'transport'},
{'amount': 150, 'category': 'entertainment'},
]
}

def get_full_name(first, last):
return f"{first} {last}"

jinja_env = jinja2.Environment()
jinja_env.filters['my_filterA'] = calculate_percentage_vvd
jinja_env.filters['get_full_name'] = get_full_name
jinja_env.filters['sum_if'] = sum_if


# 加载模板
doc = DocxTemplate("template_plus.docx")


# 创建InlineImage对象并添加到context
context['my_image'] = InlineImage(doc, 'test.jpg', width=Pt(100))
context['my_image_2'] = InlineImage(doc, 'test.jpg', width=Pt(300))

# 渲染Word文档
doc.render(context, jinja_env)
output_docx = "output.docx"
doc.save(output_docx)

# 转换为PDF
def convert_to_pdf(docx_path):
pdf_path = docx_path.replace('.docx', '.pdf')
cmd = f"libreoffice --headless --convert-to pdf {docx_path} --outdir ."
os.system(cmd)
return pdf_path

pdf_output = convert_to_pdf(output_docx)
print(f"PDF生成成功: {pdf_output}")
  • 生成效果

问题填坑

多行合并表格渲染异常

我遇到过 vm 表格渲染异常的状况,代码没有问题,渲染出来只有 3 行,而且 vm 合并列异常(只有2行)

问题定位到是表格格式的问题

  • 解决方案:选中所有表格,清楚直接格式即可。

参考资料



文章链接:
https://www.zywvvd.com/notes/coding/python/python-word-template-by-jinja2/python-word-template-by-jinja2/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

Python 利用 docxtpl 和 Jinja2 生成基于模板的 Word 文档
https://www.zywvvd.com/notes/coding/python/python-word-template-by-jinja2/python-word-template-by-jinja2/
作者
Yiwei Zhang
发布于
2025年8月13日
许可协议