Python-Jinja2 基础
·
Python-Jinja2 基础
文章目录
第一章:环境配置与模板加载
Q1:如何创建基础的 Jinja2 环境并加载模板文件?
A: 使用 Environment 配合 FileSystemLoader 是最常见的加载方式。
from jinja2 import Environment, FileSystemLoader
# 1. 创建加载器,指向模板文件所在目录
loader = FileSystemLoader('templates')
# 2. 创建环境对象
env = Environment(loader=loader)
# 3. 加载具体模板
template = env.get_template('hello.html')
# 4. 渲染并传入变量
output = template.render(name='World')
print(output) # 输出取决于模板内容
关键概念:
FileSystemLoader:从文件系统目录加载模板Environment:Jinja2 的核心配置中心,管理加载器、过滤器、全局变量等get_template():按文件名从加载器中获取模板对象render():执行模板渲染,返回字符串
Q2:除了文件系统,还有哪些模板加载方式?
A: Jinja2 提供了多种内置加载器:
| 加载器 | 用途 | 示例场景 |
|---|---|---|
FileSystemLoader |
从目录加载 .html 文件 |
Web 项目模板目录 |
PackageLoader |
从 Python 包内加载 | 发布为 pip 包的库项目 |
DictLoader |
从字典中加载模板字符串 | 单元测试、动态模板 |
FunctionLoader |
通过回调函数加载 | 从数据库/网络加载模板 |
ChoiceLoader |
组合多个加载器 | 先查缓存目录,再查默认目录 |
PrefixLoader |
按前缀路由到不同加载器 | 多主题/多版本模板管理 |
from jinja2 import DictLoader, Environment
# DictLoader 示例:直接传入模板字典
templates = {
'hello.html': '<h1>Hello {{ name }}!</h1>'
}
env = Environment(loader=DictLoader(templates))
template = env.get_template('hello.html')
print(template.render(name='Jinja2')) # <h1>Hello Jinja2!</h1>
Q3:Environment 有哪些常用配置参数?
A:
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=True, # 自动转义HTML(防XSS)
trim_blocks=True, # 去除标签后的第一个空行
lstrip_blocks=True, # 去除块前的空格
enable_async=True, # 启用异步支持(async/await)
cache_size=400, # 模板缓存大小,0=不缓存,-1=无限制
auto_reload=True, # 开发时自动重载修改的模板
undefined=StrictUndefined # 变量未定义时严格报错
)
重点参数说明:
trim_blocks=True:移除{% %}标签后的换行符,解决模板渲染后多余空行问题lstrip_blocks=True:配合trim_blocks,移除块标签前的缩进空格undefined:控制未定义变量的行为Undefined(默认):静默处理,返回空字符串DebugUndefined:返回调试信息(如{{ missing_var }})StrictUndefined:抛出UndefinedError异常(推荐生产环境使用)
第二章:模板语法基础
Q4:Jinja2 模板中如何输出变量和表达式?
A: 使用双大括号 {{ }} 进行变量插值和表达式计算。
<!-- templates/user.html -->
<h1>{{ user.name }}</h1>
<p>年龄:{{ user.age }}</p>
<p>明年年龄:{{ user.age + 1 }}</p>
<p>全名:{{ user.first_name ~ ' ' ~ user.last_name }}</p>
<p>是否存在:{{ user is defined }}</p>
语法要点:
| 语法 | 说明 |
|---|---|
{{ var }} |
输出变量 |
{{ var.attr }} |
访问对象属性(等价于 Python 的 var.attr 或 var['attr']) |
{{ var['key'] }} |
字典/映射访问 |
{{ a ~ b }} |
字符串拼接(~ 运算符) |
{{ a + b }} |
数学运算 |
{{ func() }} |
调用函数 |
Q5:如何在模板中使用条件判断?
A: 使用 {% if %} 控制结构。
{% if user.is_vip %}
<p>欢迎 VIP 用户!</p>
{% elif user.score > 100 %}
<p>您的高级会员</p>
{% else %}
<p>普通用户</p>
{% endif %}
支持的条件表达式:
{% if value is none %} <!-- 判断是否为 None -->
{% if value is defined %} <!-- 判断变量是否定义 -->
{% if value is string %} <!-- 判断是否为字符串 -->
{% if value is number %} <!-- 判断是否为数字 -->
{% if value is iterable %} <!-- 判断是否可迭代 -->
{% if value is mapping %} <!-- 判断是否为字典/映射 -->
{% if 'key' in dict %} <!-- 成员判断 -->
Q6:如何在模板中循环遍历数据?
A: 使用 {% for %} 循环。
<ul>
{% for item in items %}
<li>{{ loop.index }}. {{ item.name }} - ¥{{ item.price }}</li>
{% else %}
<li>暂无商品</li>
{% endfor %}
</ul>
loop 对象的常用属性:
| 属性 | 说明 |
|---|---|
loop.index |
当前迭代次数(从 1 开始) |
loop.index0 |
当前迭代次数(从 0 开始) |
loop.first |
是否为第一次迭代 |
loop.last |
是否为最后一次迭代 |
loop.length |
可迭代对象的总长度 |
loop.revindex |
反向索引(从 1 开始) |
loop.previtem |
上一个条目 |
loop.nextitem |
下一个条目 |
字典遍历:
{% for key, value in user.items() %}
<p>{{ key }}: {{ value }}</p>
{% endfor %}
Q7:如何在模板中定义和复用代码块(宏)?
A: 使用 {% macro %} 定义可复用的模板函数。
<!-- 定义宏 -->
{% macro input(name, value='', type='text', size=20) %}
<input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}">
{% endmacro %}
<!-- 调用宏 -->
<p>{{ input('username') }}</p>
<p>{{ input('password', type='password') }}</p>
宏的高级用法:
<!-- 接收可变数量的位置参数和关键字参数 -->
{% macro dialog(title, *contents, **kwargs) %}
<div class="dialog" {% if kwargs.class %}class="{{ kwargs.class }}"{% endif %}>
<h2>{{ title }}</h2>
{% for content in contents %}
<p>{{ content }}</p>
{% endfor %}
</div>
{% endmacro %}
第三章:模板继承与包含
Q8:如何实现模板的继承和布局复用?
A: 使用 {% extends %}、{% block %} 和 {% super() %} 实现模板继承。
<!-- templates/base.html (基础布局) -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}默认标题{% endblock %}</title>
{% block head %}
<link rel="stylesheet" href="style.css">
{% endblock %}
</head>
<body>
<div id="content">
{% block content %}{% endblock %}
</div>
<footer>
{% block footer %}
<p>© 2026 版权所有</p>
{% endblock %}
</footer>
</body>
</html>
<!-- templates/child.html (子模板) -->
{% extends "base.html" %}
{% block title %}首页 - 我的网站{% endblock %}
{% block head %}
{{ super() }} <!-- 保留父模板的 head 内容 -->
<script src="custom.js"></script>
{% endblock %}
{% block content %}
<h1>欢迎来到首页</h1>
{% endblock %}
继承规则:
- 子模板必须第一行使用
{% extends %} - 子模板只能定义
block中声明的内容,块外的内容会被忽略 {{ super() }}用于保留父模板中该块的原始内容
Q9:如何在模板中包含其他模板片段?
A: 使用 {% include %} 引入其他模板。
<!-- 包含完整模板 -->
{% include 'header.html' %}
<!-- 包含时忽略缺失的模板(不报错) -->
{% include 'sidebar.html' ignore missing %}
<!-- 包含时传入额外变量 -->
{% include 'helper.html' with context %} <!-- 传递当前上下文 -->
{% include 'helper.html' without context %} <!-- 不传递当前上下文 -->
与继承的区别:
extends:建立父子关系,子模板填充父模板的blockinclude:简单的模板拼接,被包含的模板是独立渲染的片段
第四章:过滤器(Filters)
Q10:Jinja2 有哪些常用的内置过滤器?
A:
| 过滤器 | 功能 | 示例 |
|---|---|---|
{{ var | default('N/A') }} |
设置默认值 | 变量未定义或为空时显示 ‘N/A’ |
{{ var | escape }} 或 e |
HTML 转义 | 将 < 转为 < |
{{ var | safe }} |
标记安全 | 禁止转义,直接输出原始 HTML |
{{ list | length }} |
获取长度 | 返回列表/字符串长度 |
{{ list | first }} |
获取首元素 | |
{{ list | last }} |
获取末元素 | |
{{ list | join(', ') }} |
拼接字符串 | 将列表用指定分隔符连接 |
{{ list | sort }} |
排序 | 支持 sort(attribute='name') |
{{ list | reverse }} |
反转 | |
{{ list | unique }} |
去重 | |
{{ list | selectattr('active') }} |
按属性筛选 | 筛选 active 为真的对象 |
{{ list | rejectattr('deleted') }} |
按属性排除 | |
{{ str | upper }} |
转大写 | |
{{ str | lower }} |
转小写 | |
{{ str | title }} |
单词首字母大写 | |
{{ str | trim }} |
去除首尾空格 | |
{{ str | replace('a', 'b') }} |
替换 | |
{{ str | truncate(20) }} |
截断 | 超过 20 字符显示 ... |
{{ num | round(2) }} |
四舍五入 | 保留 2 位小数 |
{{ num | int }} |
转整数 | |
{{ num | float }} |
转浮点数 | |
{{ dict | tojson }} |
转 JSON 字符串 | 常用于 <script> 标签内 |
{{ html | striptags }} |
去除 HTML 标签 | |
{{ var | batch(3) }} |
分批处理 | 将列表按每 3 个一组切分 |
{{ var | slice(3) }} |
切片分组 | 将列表均匀分成 3 组 |
{{ var | map(attribute='name') }} |
提取属性 | 提取列表中每个对象的 name |
链式调用:
{{ users | selectattr('is_active') | map(attribute='name') | sort | join(', ') }}
Q11:如何在 Environment 中注册自定义过滤器?
A:
from jinja2 import Environment, FileSystemLoader
import datetime
env = Environment(loader=FileSystemLoader('templates'))
# 方法 1:直接注册函数为过滤器
def format_datetime(value, format='%Y-%m-%d %H:%M'):
"""将时间戳或 datetime 对象格式化为字符串"""
if isinstance(value, datetime.datetime):
return value.strftime(format)
return value
env.filters['datetime'] = format_datetime
# 方法 2:使用装饰器(更优雅)
@env.filter('currency')
def format_currency(value, symbol='¥'):
"""格式化为货币显示"""
return f"{symbol}{value:,.2f}"
# 模板中使用
# {{ created_at | datetime }} -> 2026-05-07 19:39
# {{ created_at | datetime('%Y年%m月%d日') }} -> 2026年05月07日
# {{ price | currency }} -> ¥1,234.50
# {{ price | currency('$') }} -> $1,234.50
第五章:自动转义与安全
Q12:Jinja2 如何防止 XSS 攻击?自动转义是如何工作的?
A:
# 开启自动转义(推荐用于 HTML 模板)
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=True # 或 autoescape='html'
)
转义行为规则:
| 输入类型 | 自动转义行为 |
|---|---|
| 字符串 | 转义 HTML 特殊字符(<, >, &, ") |
| 数字 | 不转义(本身就是安全的) |
Markup 对象 |
不转义(标记为安全) |
from jinja2 import Markup
# 在 Python 中标记字符串为安全(谨慎使用!)
safe_html = Markup('<strong>粗体</strong>')
template.render(content=safe_html) # 会直接输出 <strong>标签
# 在模板中标记安全(仅当你确定数据源可信时)
# {{ html_content | safe }}
手动控制转义:
<!-- 强制转义 -->
{{ user_input | escape }}
<!-- 强制不转义(危险!确保数据源可信) -->
{{ trusted_html | safe }}
第六章:全局变量与测试
Q13:如何在所有模板中注入全局可用的变量或函数?
A:
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader('templates'))
# 注入全局变量
env.globals['site_name'] = '我的网站'
env.globals['current_year'] = 2026
# 注入全局函数
def get_asset_url(filename):
return f"/static/{filename}"
env.globals['asset'] = get_asset_url
# 模板中直接使用
# <link rel="stylesheet" href="{{ asset('style.css') }}">
# <footer>© {{ current_year }} {{ site_name }}</footer>
Q14:什么是测试(Tests)?与过滤器有什么区别?
A: 测试用于条件判断,返回布尔值;过滤器用于数据转换。
{% if number is odd %}
<p>奇数</p>
{% endif %}
{% if users is iterable %}
<p>可迭代对象</p>
{% endif %}
内置测试:
| 测试 | 说明 |
|---|---|
is defined |
变量是否定义 |
is none |
是否为 None |
is string |
是否为字符串 |
is number |
是否为数字 |
is iterable |
是否可迭代 |
is mapping |
是否为字典 |
is sequence |
是否为序列(列表/元组/字符串) |
is sameas(obj) |
是否与 obj 是同一对象 |
is odd |
是否为奇数 |
is even |
是否为偶数 |
is divisibleby(n) |
是否能被 n 整除 |
注册自定义测试:
def is_image_file(filename):
return filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif'))
env.tests['image'] = is_image_file
# 模板中使用:{% if filename is image %}...{% endif %}
第七章:高级用法
Q15:如何在模板中执行 Python 表达式或调用方法?
A: Jinja2 默认限制了部分 Python 语法,但支持以下操作:
<!-- 调用对象方法(需要方法无副作用且返回字符串/值) -->
{{ user.get_display_name() }}
<!-- 使用下标访问 -->
{{ users[0].name }}
<!-- 三元表达式 -->
{{ '是' if user.is_vip else '否' }}
<!-- 内联 if(类似 Python 的条件表达式) -->
{{ user.nickname or user.username }}
注意: Jinja2 的沙箱机制默认不允许直接调用 Python 内置函数(如 range、dict 等)。如需使用,需要通过 Environment 注入:
env.globals.update(
range=range,
dict=dict,
len=len,
enumerate=enumerate,
zip=zip
)
Q16:如何渲染模板字符串(不通过文件)?
A: 使用 Template 类直接渲染字符串。
from jinja2 import Template
template = Template('Hello {{ name }}!')
result = template.render(name='World')
print(result) # Hello World!
注意: 字符串模板不通过 Environment,因此无法使用自定义过滤器和全局变量。如需使用,应从环境创建:
from jinja2 import Environment
env = Environment()
template = env.from_string('Hello {{ name | upper }}!')
print(template.render(name='world')) # Hello WORLD!
Q17:如何处理模板渲染中的异常?
A:
from jinja2 import Environment, TemplateNotFound, UndefinedError
env = Environment(loader=FileSystemLoader('templates'))
try:
template = env.get_template('missing.html')
except TemplateNotFound as e:
print(f"模板未找到: {e}")
try:
template = env.get_template('hello.html')
result = template.render()
except UndefinedError as e:
print(f"变量未定义: {e}")
except Exception as e:
print(f"渲染错误: {e}")
第八章:完整实战示例
Q18:能否提供一个完整的 Jinja2 使用示例?
A:
# app.py
from jinja2 import Environment, FileSystemLoader, StrictUndefined
import datetime
# 1. 配置环境
env = Environment(
loader=FileSystemLoader('templates'),
autoescape=True,
trim_blocks=True,
lstrip_blocks=True,
undefined=StrictUndefined # 严格模式:未定义变量直接报错
)
# 2. 注册自定义过滤器
@env.filter('datetime')
def format_datetime(value, fmt='%Y-%m-%d'):
if isinstance(value, datetime.datetime):
return value.strftime(fmt)
return value
# 3. 注册全局函数
env.globals['now'] = lambda: datetime.datetime.now()
# 4. 准备数据
users = [
{'name': '张三', 'age': 28, 'is_vip': True, 'created_at': datetime.datetime(2026, 1, 15)},
{'name': '李四', 'age': 22, 'is_vip': False, 'created_at': datetime.datetime(2026, 3, 20)},
{'name': '王五', 'age': 35, 'is_vip': True, 'created_at': datetime.datetime(2025, 8, 10)},
]
# 5. 渲染模板
template = env.get_template('users.html')
html = template.render(
page_title='用户列表',
users=users
)
print(html)
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %} - {{ site_name }}</title>
<style>
.vip { color: gold; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
{% block content %}{% endblock %}
<footer>
<p>生成时间:{{ now() | datetime('%Y-%m-%d %H:%M:%S') }}</p>
</footer>
</body>
</html>
<!-- templates/users.html -->
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<h1>{{ page_title }}</h1>
<table>
<thead>
<tr>
<th>#</th>
<th>姓名</th>
<th>年龄</th>
<th>会员状态</th>
<th>注册时间</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="{{ 'vip' if user.is_vip else '' }}">
<td>{{ loop.index }}</td>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>
{% if user.is_vip %}
⭐ VIP会员
{% else %}
普通用户
{% endif %}
</td>
<td>{{ user.created_at | datetime('%Y年%m月%d日') }}</td>
</tr>
{% else %}
<tr>
<td colspan="5" style="text-align: center;">暂无用户数据</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>平均年龄:{{ (users | map(attribute='age') | sum) / (users | length) }} 岁</p>
{% endblock %}
附录:速查表
| 场景 | 语法 |
|---|---|
| 输出变量 | {{ var }} |
| 执行逻辑 | {% if %}, {% for %}, {% macro %} |
| 注释 | {# 这是注释 #} |
| 继承 | {% extends %} + {% block %} |
| 包含 | {% include %} |
| 过滤器 | {{ var | filter }} |
| 测试 | {% if var is test %} |
| 字符串拼接 | {{ 'a' ~ 'b' }} |
| 忽略转义 | {{ var | safe }} |
| 默认值 | {{ var | default('N/A') }} |
更多推荐



所有评论(0)