Jinja2 学习指南
Jinja2 学习指南
Jinja2 是 Python 的模板引擎:将数据传入模板,生成文本。常见用途包括生成 HTML、配置文件、提示词和 JSON。
本文基于 Jinja2==3.1.6,先介绍语法与模板复用,再通过完整示例演示如何动态生成 JSON,以及如何添加自定义 filter。
模板、Context 和渲染结果
三个概念:
- 模板:带有变量和逻辑的文本。
- Context:传入模板的数据,通常用 Python 字典表示。
- 渲染:将 context 应用到模板,生成结果。
安装:
python -m pip install Jinja2==3.1.6最小示例:
from jinja2 import Environment, StrictUndefined
# autoescape 控制是否对输出值进行 HTML 转义,例如将 < 转为 <。
# 这里生成纯文本,因此关闭;生成 HTML 时应启用。
env = Environment(undefined=StrictUndefined, autoescape=False)
template = "你好,{{ name }}!"
context = {"name": "张三"}
result = env.from_string(template).render(context)
assert result == "你好,张三!"
print(result)from_string() 将字符串编译成模板,render() 接收上下文并执行渲染。上下文既可以通过 render(context) 直接传入字典,也可以通过 render(**context) 展开为关键字参数;在本例中,两种写法的效果相同。
同一个模板需要反复使用时,可以先编译一次,再用不同 context 调用render()。 通常也可以复用配置好的Environment。
三种基本标记
{{ ... }}:计算并输出表达式。
{% ... %}:执行模板语句,例如条件、循环、赋值。
{# ... #}:模板注释,不会出现在渲染结果中。
{# 这是一条注释 #}
名称:{{ user.name }}
{% if user.enabled %}
状态:启用
{% else %}
状态:停用
{% endif %}字典可以通过 user.name 或 user["name"] 访问。
点号访问优先查找对象属性,方括号访问优先查找字典键。例如,字典user = {"items": ["书", "笔"]}中,user.items会取到字典自带的items方法,而user["items"]才会取到列表["书", "笔"]。因此,读取与字典方法同名的键时,应使用方括号。
表达式与字符串拼接
Jinja 支持常见的算术、比较和逻辑表达式:
{{ price * quantity }}
{% if count > 10 and enabled %}满足条件{% endif %}
{{ "启用" if enabled else "停用" }}~ 是字符串拼接运算符,会先把操作数转为字符串。
{{ source ~ ":" ~ document_id }}当 source="video"、document_id=42 时,结果为 video:42。类似于 Python:
source = "video"
document_id = 42
result = str(source) + ":" + str(document_id)
assert result == "video:42"不要将 Jinja 表达式当作任意 Python 代码:模板支持自己的语法,并不是 Python 解释器。
循环与条件
{% for item in items %}
{{ loop.index }}. {{ item.name }}
{% else %}
暂无数据
{% endfor %}for 的 else 在没有元素被迭代时执行。loop 是 Jinja 在 for 循环内部自动提供的特殊变量,无需在 context 中定义;它指向当前循环的上下文对象,可通过以下属性获取循环信息:
loop.index:从 1 开始的序号。
loop.index0:从 0 开始的序号。
loop.first、loop.last:是否为第一项、最后一项。
loop.length:本次循环的元素数量。
可以直接筛选循环元素:
{% for item in items if item.enabled %}
{{ item.name }}
{% endfor %}loop.changed(value) 用于判断传入的值是否发生变化:第一次调用时返回 true;之后与上一次调用的值比较,不同则返回 true,相同则返回 false。
例如,依次遍历分类为“图书、图书、文具”的三条记录,希望每个连续分类只输出一次标题:
{% for item in items %}
{% if loop.changed(item.category) %}
分类:{{ item.category }}
{% endif %}
- {{ item.name }}
{% endfor %}第一条记录输出“分类:图书”,第二条分类未变,不重复输出标题;第三条分类变为“文具”,输出新标题。
loop.changed() 只比较调用时传入的值,不负责排序或分组。如果分类顺序是“图书、文具、图书”,就会输出三次标题;需要同类记录集中展示时,应先按分类排序。Filter 与 Test
Filter:处理值
Filter 使用 | 连接,可以传参数、连续调用:
{{ name | upper }}
{{ items | length }}
{{ names | join(", ") }}
{{ users | selectattr("enabled") | map(attribute="name") | join(", ") }}最后一个例子依次筛选启用的用户、提取姓名、拼接为字符串。
from jinja2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined, autoescape=False)
template = '{{ users | selectattr("enabled") | map(attribute="name") | join(", ") }}'
context = {
"users": [
{"name": "张三", "enabled": True},
{"name": "李四", "enabled": False},
{"name": "王五", "enabled": True},
]
}
result = env.from_string(template).render(context)
assert result == "张三, 王五"
print(result)处理过程:
selectattr("enabled")保留enabled为真值的用户,即张三和王五。
map(attribute="name")提取这两个用户的name字段。
join(", ")用逗号和空格连接姓名,输出张三, 王五。
常用的集合过滤器还包括 sort、groupby、rejectattr、unique。部分过滤器返回迭代器,需要列表时使用 |list。
Test:判断值
Test 使用 is:
{% if name is defined %}变量已定义{% endif %}
{% if value is none %}值为 None{% endif %}
{% if value is string %}值是字符串{% endif %}
{% if value is mapping %}值是映射{% endif %}未定义变量与默认值
建议使用 StrictUndefined,让缺少变量的问题在渲染时暴露,而不是静默输出空字符串。
可选变量可以显式提供默认值:
{{ description | default("暂无描述") }}默认只替换未定义的变量;要同时替换 None、空字符串、False、0 等假值,使用:
{{ description | default("暂无描述", true) }}default(..., true)也会替换合法的0或False,使用前应确认这符合预期。
自定义 Filter
自定义 filter 是一个 Python 函数。通过 env.filters 注册后,就能在模板中使用。
from jinja2 import Environment, StrictUndefined
def zfill(value: int | str, width: int = 6) -> str:
"""将值转为字符串,不足指定宽度时在左侧补零。"""
return str(value).zfill(width)
env = Environment(undefined=StrictUndefined, autoescape=False)
env.filters["zfill"] = zfill
template = '{{ source ~ ":" ~ (document_id | zfill(6)) }}'
context = {"source": "video", "document_id": 42}
result = env.from_string(template).render(context)
assert result == "video:000042"
print(result)表达式 document_id | zfill(6) 对应函数调用 zfill(document_id, 6):管道左侧的值作为第一个参数,括号中的 6 是后续参数。
超过指定宽度时,这个过滤器不会截断原值。
过滤器应在编译或加载模板前注册。自定义 test 注册到env.tests,全局函数或常量注册到env.globals。
根据 Context 生成 JSON
推荐在模板中构造字典或列表,再用 tojson 序列化整个对象。这样不需要手动处理 JSON 的引号、逗号和转义。
下面是完整示例:模板是字符串,context 是字典,自定义 zfill 用于格式化 ID。
"""根据 context 渲染主键 JSON,并验证它可以被解析。"""
import json
from typing import Any
from jinja2 import Environment, StrictUndefined
def zfill(value: int | str, width: int = 6) -> str:
"""将 ID 转成字符串并在左侧补零,例如 42 转为 000042。"""
return str(value).zfill(width)
def render_json(template: str, context: dict[str, Any]) -> str:
"""渲染模板,确保结果是合法 JSON 后返回文本。"""
env = Environment(
undefined=StrictUndefined,
autoescape=False,
trim_blocks=True, # 移除 {% ... %} 语句块后紧接的第一个换行。
lstrip_blocks=True, # 去掉 {% if %}、{% for %} 等控制语句前的行首缩进。
)
# 注册后,模板可通过 document_id | zfill(6) 调用。
env.filters["zfill"] = zfill
rendered = env.from_string(template).render(context).strip()
json.loads(rendered)
return rendered
def main() -> None:
template = """
{% if source == "video" %}
{{ {
"tenant_id": tenant_id,
"video_id": source ~ ":" ~ (document_id | zfill(6))
} | tojson }}
{% else %}
{{ {
"tenant_id": tenant_id,
"document_id": document_id
} | tojson }}
{% endif %}
"""
context: dict[str, Any] = {
"tenant_id": 100,
"source": "video",
"document_id": 42,
}
rendered = render_json(template, context)
print("生成的 JSON:")
print(rendered)
# 模拟接收方解析 JSON。
primary_key = json.loads(rendered)
print("\n解析后的 Python 对象:")
print(primary_key)
print("\n字段类型:")
for name, value in primary_key.items():
print(f"{name}: {type(value).__name__}")
if __name__ == "__main__":
main()source="video" 时输出:
{"tenant_id": 100, "video_id": "video:000042"}将 source 改为 "document" 后输出:
{"document_id": 42, "tenant_id": 100}为什么不要直接拼接 JSON 字符串
下面这种模板容易因数据中出现引号或换行而生成错误 JSON:
{"name": "{{ name }}"}推荐写为:
{{ {"name": name} | tojson }}tojson 会将可序列化的 Python 值转换为 JSON,包括将 None 转为 null,将布尔值转为 true、false。它不会自动将任意 Python 对象转换为 JSON;日期等类型需要先转换。
macro 与 import:复用带参数的模板逻辑
macro 类似模板中的函数,支持参数和默认值:
{% macro label(name, suffix="") -%}
{{ name }}{{ suffix }}
{%- endmacro %}
{{ label("任务", "已完成") }}在常规字符串渲染环境中,宏返回渲染后的文本,不用于返回任意 Python 对象。
宏可以保存在另一个模板中,用 import 导入。下面通过 DictLoader 将所有模板都保留为 Python 字符串:
from jinja2 import DictLoader, Environment, StrictUndefined
templates = {
"macros.j2": '{% macro label(name) %}[{{ name }}]{% endmacro %}',
"main.j2": '{% import "macros.j2" as ui %}{{ ui.label(name) }}',
}
env = Environment(
loader=DictLoader(templates), undefined=StrictUndefined, autoescape=False
)
result = env.get_template("main.j2").render({"name": "完成"})
assert result == "[完成]"
print(result)也可以只导入某个宏:
{% from "macros.j2" import label %}
{{ label("完成") }}include 与 import 的区别
include 渲染另一个模板并插入结果;import 导入宏等定义,供后续调用。
from jinja2 import DictLoader, Environment, StrictUndefined
env = Environment(
loader=DictLoader({
"header.j2": "标题:{{ title }}",
"main.j2": '{% include "header.j2" %}\n正文',
}),
undefined=StrictUndefined,
autoescape=False,
)
result = env.get_template("main.j2").render({"title": "执行报告"})
assert result == "标题:执行报告\n正文"
print(result)上下文的默认行为不同:
include默认使用当前上下文,因此示例中的header.j2可以访问title。
import默认不携带调用处的上下文;通常通过宏参数传值。环境中的全局变量仍然可用。
需要改变默认行为时:
{% include "header.j2" without context %}
{% import "macros.j2" as ui with context %}include 还支持忽略不存在的模板:
{% include "optional.j2" ignore missing %}仅使用 from_string() 并没有为环境配置加载器;如果模板内部需要 include、import 或 extends,仍需配置 DictLoader、FileSystemLoader 等加载器。
extends、block 与 super():模板继承
模板继承可以理解为:父模板规定整体结构,并用 block 留出可以替换的区域;子模板用同名 block 提供这些区域的内容。
第一步:父模板定义结构和可替换区域
父模板 base.j2:
标题:{% block title %}默认标题{% endblock %}
内容:{% block content %}{% endblock %}这里定义了两个区域:
title:默认内容是“默认标题”。
content:默认内容为空。
标题:、内容: 以及它们之间的换行位于 block 外,属于父模板的固定结构。子模板覆盖 block 时,这些固定文字仍然保留。
第二步:子模板覆盖同名区域
子模板 report.j2:
{% extends "base.j2" %}
{% block title %}执行报告 · {{ super() }}{% endblock %}
{% block content %}共处理 {{ count }} 条记录。{% endblock %}{% extends "base.j2" %} 表示使用 base.j2 的整体结构。子模板中的 title 和 content 分别替换父模板中同名区域的内容。
super() 表示“取父模板中这个 block 的内容”。这里位于 title 内,所以得到“默认标题”,最终标题区域是“执行报告 · 默认标题”。如果不调用 super(),父模板的默认标题就不会保留。
子模板没有覆盖的 block,继续使用父模板的默认内容。block 是模板中的命名区域,不是 context 中的变量。第三步:渲染子模板,得到完整结果
传入 count=3,渲染 report.j2,结果是:
标题:执行报告 · 默认标题
内容:共处理 3 条记录。可以按下面的顺序理解:
- 使用父模板的
标题:。
- 在
title区域填入子模板的内容;其中super()补入父模板的“默认标题”。
- 保留父模板的换行和
内容:。
- 在
content区域填入子模板的内容,并将count渲染为3。
渲染的是子模板,但输出采用父模板的整体结构。 不需要先手动渲染父模板,再拼接子模板。
完整 Python 示例
下面用 DictLoader 为两个字符串模板指定名称:
from jinja2 import DictLoader, Environment, StrictUndefined
templates = {
"base.j2": (
"标题:{% block title %}默认标题{% endblock %}\n"
"内容:{% block content %}{% endblock %}"
),
"report.j2": """{% extends "base.j2" %}
{% block title %}执行报告 · {{ super() }}{% endblock %}
{% block content %}共处理 {{ count }} 条记录。{% endblock %}
""",
}
env = Environment(
loader=DictLoader(templates),
undefined=StrictUndefined,
autoescape=False,
)
template = env.get_template("report.j2")
result = template.render({"count": 3})
assert result == "标题:执行报告 · 默认标题\n内容:共处理 3 条记录。"
print(result)可以尝试两处修改:
- 删除子模板标题中的
· {{ super() }},第一行就变成标题:执行报告。
- 删除子模板的整个
titleblock,第一行就变成标题:默认标题。
call 与 caller():向宏传递模板内容
call 用于调用宏,并向宏传入一段模板内容。宏负责外层结构,调用方决定内部内容;宏通过 caller() 将这段内容渲染到指定位置。
先定义宏:
{% macro section(title) %}
标题:{{ title }}
{{ caller() }}
{% endmacro %}这个宏输出标题,然后在 caller() 所在的位置输出调用方传入的内容。
调用宏:
{% call section("运行结果") %}
共处理 {{ count }} 条记录。
{% endcall %}这次调用传入了两样东西:
- 参数
title:字符串"运行结果"。
- 模板内容:
call与endcall之间的共处理 {{ count }} 条记录。。
假设 context 中的 count=3,渲染后得到以下内容(省略额外空行):
标题:运行结果
共处理 3 条记录。其中,{{ title }} 输出“运行结果”,{{ caller() }} 渲染调用方传入的模板内容,并将 count 替换为 3。
caller是 Jinja 为这次call调用自动提供的特殊变量,无需在 context 中定义。它指向调用方传入的那段模板内容,可通过caller()调用。
set、作用域与 namespace
set 可以定义变量,也可以捕获一段渲染结果:
{% set title = "报告" %}
{% set summary %}共 {{ items | length }} 条记录{% endset %}循环中的普通赋值不会更新循环外的同名变量。 需要跨循环保存状态时使用 namespace:
{% set ns = namespace(total=0) %}
{% for item in items %}
{% set ns.total = ns.total + item.size %}
{% endfor %}
总大小:{{ ns.total }}如果只是求和,可以直接写 {{ items | sum(attribute="size") }};复杂计算通常放在 Python 中完成。
空白控制
环境级配置:
trim_blocks=True:移除{% ... %}语句块后紧接的第一个换行。
lstrip_blocks=True:去掉{% if %}、{% for %}等控制语句前的行首缩进。
局部控制:
{{- value -}}
{%- if enabled -%}
内容
{%- endif -%}左侧的 - 删除标记前的空白,右侧的 - 删除标记后的空白,可能包括换行。应按需使用,避免把本来需要分开的文字连接起来。
按输出类型配置环境
JSON 与纯文本
本指南的 JSON 示例使用 autoescape=False,由 tojson 负责 JSON 序列化。
HTML
生成 HTML 时应启用自动转义,避免把输入数据直接解释成 HTML:
from jinja2 import Environment, StrictUndefined
env = Environment(autoescape=True, undefined=StrictUndefined)
result = env.from_string("<p>{{ text }}</p>").render({"text": "<script>"})
assert result == "<p><script></p>"同时使用多种类型的模板文件时,可以通过 select_autoescape() 按文件扩展名决定是否启用自动转义。例如,下面的配置为 .html、.xml 模板启用转义,其他扩展名默认不启用:
from jinja2 import Environment, FileSystemLoader, select_autoescape
env = Environment(
loader=FileSystemLoader("templates"),
autoescape=select_autoescape(["html", "xml"]),
)|safe 则用于标记一个值,让它跳过自动转义。例如,text 的值为 "<b>你好</b>",启用自动转义后:
{{ text }}输出<b>你好</b>,浏览器显示文字<b>你好</b>。
{{ text | safe }}直接输出<b>你好</b>,浏览器将标签解释为 HTML,显示加粗的“你好”。
safe 只跳过转义,不会检查或清理 HTML。 仅在内容已确认可安全作为 HTML 输出时使用,不要直接用于未经处理的用户输入。