详解模板渲染引擎 jinja2

简介: 详解模板渲染引擎 jinja2

楔子



jinja2 应该是 Python 里面最著名的模板渲染引擎了,并且提到 jinja2,很多人会立刻想到 flask,因为 flask 在渲染模板的时候用的就是它

但 jinja2 不和 flask 绑定,它是独立于 flask 存在的,这就使得 jinja2 可以应用在很多地方。像一些能够生成 html 的绘图框架、分析工具,内部很多都使用了 jinja2,比如 pandas, pyecharts 等等。

那么下面就来单独地介绍一下 jinja2 的模板渲染语法。


模板传参



先来看看最简单的字符串替换:

import jinja2
# 将想要替换的内容使用 {{}} 包起来
string = "姓名: {{name}}, 年龄: {{age}}"
# 得到模板对象
temp = jinja2.Template(string)
# 调用 render 方法进行渲染
# 返回渲染之后的字符串
render_string = temp.render(name="古明地觉", age=16)
print(render_string)
"""
姓名: 古明地觉, 年龄: 16
"""

用法相当简单,并且和字符串格式化非常类似:

string = "姓名: {name}, 年龄: {age}"
render_string = string.format(name="古明地觉", age=16)
print(render_string)
"""
姓名: 古明地觉, 年龄: 16
"""

两者非常类似,只不过字符串格式化使用的是一对大括号,而 jinja2 使用的是两对大括号。但如果只是简单的字符串替换,那么使用 jinja2 就有点大材小用了,因为 jinja2 支持的功能远不止这些。

import jinja2
string = "姓名: {{info['name']}}, 年龄: {{info['age']}}"
temp = jinja2.Template(string)
render_string = temp.render(
    info={"name": "古明地觉", "age": 16})
print(render_string)
"""
姓名: 古明地觉, 年龄: 16
"""

可以看到 jinja2 不仅仅支持字符串替换,在替换的时候还可以做一些额外的操作,并且这些操作不止局限于字典(以及其它对象)的取值,算术运算、函数调用也是支持的。

import jinja2
string = """{{numbers * 3}}
{{tuple1 + tuple2}}
{{np.array([1, 2, 3])}}
"""
temp = jinja2.Template(string)
render_string = temp.render(
    numbers=[1, 2, 3],
    tuple1=("a", "b"),
    tuple2=("c", "d"),
    np=__import__("numpy")
)
print(render_string)
"""
[1, 2, 3, 1, 2, 3, 1, 2, 3]
('a', 'b', 'c', 'd')
[1 2 3]
"""

还是很强大的,但有两个注意的点,首先我们不能定义空的花括号。

import jinja2
string = "---{{}}---"
try:
    temp = jinja2.Template(string)
except jinja2.TemplateSyntaxError as e:
    print(e)
"""
Expected an expression, got 'end of print statement'
"""

{{}} 内部必须要指定相应的参数,否则报错。但如果指定了参数,在渲染的时候不传,会怎么样呢?

import jinja2
string = "---{{name}}---"
temp = jinja2.Template(string)
render_string = temp.render()
print(render_string)
"""
------
"""

我们看到不传也不会报错,在渲染的时候会直接丢弃,按照来处理。可如果参数进行了某种操作,那么就必须要给参数传值了,举个例子。

import jinja2
# 对参数 name 进行了操作, 所以必须传值
string = "---{{name.upper()}}---"
temp = jinja2.Template(string)
render_string = temp.render(name="satori")
print(render_string)
"""
---SATORI---
"""
try:
    temp.render()
except jinja2.UndefinedError as e:
    print(e)
"""
'name' is undefined
"""

关于模板传参就说到这里,还是很简单的。


过滤器



过滤器的概念应该不需要多说,在 jinja2 中通过 | 来实现过滤器。

例如:{{name | length}},会返回 name 的长度。过滤器相当于是一个函数,参数接收到的值会传到过滤器中,然后过滤器根据自己的功能再返回相应的值,最后将结果渲染到页面中。

jinja2 内置了很多的过滤器,下面介绍一些常用的:

import jinja2
string = """{{array}} 的长度 -> {{array|length}}
{{array}} 的总和 -> {{array|sum}}
{{array}} 的第一个元素 -> {{array|first}}
{{array}} 的最后一个元素 -> {{array|last}}
{{array}} 使用 {{sep}} 拼接的字符串 -> {{array|join(sep)}}
{{count}} 转成整数 -> {{count|int}}
{{count}} 转成浮点数 -> {{count|float}}
{{count}} 转成字符串 -> {{count|string}}
{{count}} 的绝对值 -> {{count|abs}}
{{name}} 转成小写 -> {{name|lower}}
{{name}} 转成大写 -> {{name|upper}}
{{name}} 的 'i' 替换成 'I' -> {{name|replace('i', "I")}}
{{name}} 反向取值 -> {{name|reverse}}
字符串过长, 使用省略号表示
最多显示 10 位 -> {{long_text|truncate(length=10)}}
"""
temp = jinja2.Template(string)
render_string = temp.render(
    array=[1, 2, 3, 4, 5],
    sep='_',
    count=-666.66,
    name="Koishi",
    long_text="a" * 100
)
print(render_string)
"""
[1, 2, 3, 4, 5] 的长度 -> 5
[1, 2, 3, 4, 5] 的总和 -> 15
[1, 2, 3, 4, 5] 的第一个元素 -> 1
[1, 2, 3, 4, 5] 的最后一个元素 -> 5
[1, 2, 3, 4, 5] 使用 _ 拼接的字符串 -> 1_2_3_4_5
-666.66 转成整数 -> -666
-666.66 转成浮点数 -> -666.66
-666.66 转成字符串 -> -666.66
-666.66 的绝对值 -> 666.66
Koishi 转成小写 -> koishi
Koishi 转成大写 -> KOISHI
Koishi 的 'i' 替换成 'I' -> KoIshI
Koishi 反向取值 -> ihsioK
字符串过长, 使用省略号表示
最多显示 10 位 -> aaaaaaa...
"""

以上就是 jinja2 内置的一些常用的过滤器,然后还有一个特殊的过滤器 default。前面说了,如果不给 {{}} 里面的参数传值的话,那么默认会不显示,也不报错。但如果我们希望在不传递的时候,使用默认值该怎么办呢?

import jinja2
string = "{{sign|default('这个人很懒,什么也没留下')}}"
temp = jinja2.Template(string)
render_string = temp.render(
    sign="不装了,摊牌了,我就是高级特工氚疝钾"
)
print(render_string)
"""
不装了,摊牌了,我就是高级特工氚疝钾
"""
# 不指定,会使用默认值
render_string = temp.render()
print(render_string)
"""
这个人很懒,什么也没留下
"""

default 还有一个参数 boolean,因为 default 是否执行,不在于传的是什么值,而在于有没有传值。只要传了,就不会显示 default 里面的内容。

那如果我想当传入空字符串,空字典等等,在 Python 中为假的值,还是等价于没传值,继续显示 default 里的默认值,该怎么办呢?

很简单,可以将参数 boolean 指定为 True,表示只有当布尔值为真时,才使用我们传递的值。否则,仍显示 default 里的默认值。

import jinja2
string = "{{sign1|default('这个人很懒,什么也没留下')}}\n" \
    "{{sign2|default('这个人很懒,什么也没留下', boolean=True)}}"
temp = jinja2.Template(string)
# sign1 和 sign2 接收的都是空字典,布尔值为假
render_string = temp.render(
    sign1={},
    sign2={}
)
# 对于 sign1 而言,只要传值了,就会显示我们传的值
# 对于 sign2 而言,不仅要求传值,还要求布尔值为真,否则还是会使用默认值
print(render_string)
"""
{}
这个人很懒,什么也没留下
"""

可以看到 jinja2 内置了很多的过滤器,但如果我们的业务场景比较特殊,jinja2 内置的过滤器满足不了,该怎么办呢?没关系,jinja2 还支持我们自定制过滤器。

自定制过滤器

过滤器本质上就是个函数,因此我们只需要写个函数,定义相应的逻辑,然后注册到 jinja2 过滤器当中即可。下面我们手动实现一个 replace 过滤器。

import jinja2
string = "{{name|my_replace('i', 'I')}}"
# 定义过滤器对应的函数
# jinja2 在渲染的时候,就会执行这里的 my_replace 函数
def my_replace(s, old, new):
    """
    需要一提的是,过滤器里面接收了两个参数
    但函数要定义三个参数,因为在调用的时候 name 也会传过来
    所以像 {{name|length}} 这种,它和 {{name|length()}} 是等价的
    函数至少要能接收一个参数
    """
    return s.replace(old, new)
# 此时函数就定义好了,但它目前和过滤器还没有什么关系,只是名字一样而已
# 我们还需要将过滤器和函数注册到 jinja2 当中
# 这里调用了一个新的类 Environment
# jinja2.Template 本质上也是调用了 Environment
env = jinja2.Environment()
# 将过滤器和函数绑定起来,注册到 jinja2 当中
# 并且过滤器的名字和函数名可以不一样
env.filters["my_replace"] = my_replace
# 返回 Template 对象
temp = env.from_string(string)
# 调用 render 方法渲染
render_string = temp.render(name="koishi")
print(render_string) 
"""
koIshI
"""

Environment 是 jinja2 的核心组件,包含了配置、过滤器、全局环境等一系列重要的共享变量。如果我们想自定制过滤器的话,那么必须手动实例化这个对象,然后注册进去。通过调用它的 from_string 方法,得到 Template 对象,这样在渲染的时候就能找到我们自定制的过滤器了。

事实上,我们之前在实例化 Template 对象时,底层也是这么做的。

因此我们后续就使用 Environment 这个类,当然 Template 也是可以的。


逻辑语句



jinja2 还支持 if、for 等逻辑语句,来看一下。

import jinja2
# 如果是接收具体的值,那么使用 {{}}
# 但 if、for 等逻辑语句,则需要写在 {% %} 里面
string = """
{% if info['math'] >= 90 %}
    {{info['name']}} 的数学成绩为 A
{% elif info['math'] >= 80 %}
    {{info['name']}} 的数学成绩为 B
{% elif info['math'] >= 60 %}
    {{info['name']}} 的数学成绩为 C
{% else %}
    {{info['name']}} 的数学成绩为 D
{% endif %}"""
# 和 Python 的 if 语句类似
# 但是结尾要有一个 {%endif %}
env = jinja2.Environment()
temp = env.from_string(string)
render_string = temp.render(
    info={"math": 85, "name": "古明地觉"}
)
print(render_string)
"""
    古明地觉 的数学成绩为 B
"""
render_string = temp.render(
    info={"math": 9, "name": "琪露诺"}
)
print(render_string)
"""
    琪露诺 的数学成绩为 D
"""

并且 if 语句还可以多重嵌套,都是可以的。

然后是 for 语句:

import jinja2
# 和 Python for 循环等价
# 但不要忘记结尾的 {% endfor %}
string = """
{% for girl in girls %}
    姓名: {{girl['name']}}, 地址: {{girl['address']}}
{% endfor %}
"""
env = jinja2.Environment()
temp = env.from_string(string)
render_string = temp.render(
    girls=[{"name": "古明地觉", "address": "地灵殿"},
           {"name": "琪露诺", "address": "雾之湖"},
           {"name": "魔理沙", "address": "魔法森林"}]
)
print(render_string)
"""
    姓名: 古明地觉, 地址: 地灵殿
    姓名: 琪露诺, 地址: 雾之湖
    姓名: 魔理沙, 地址: 魔法森林
"""

所以 {% for girl in girls %} 这段逻辑和 Python 是等价的,先确定 girls 的值,然后遍历。因此在里面也可以使用过滤器,比如 {% for girl in girls|reverse %} 便可实现对 girls 的反向遍历。

如果在遍历的时候,还想获取索引呢?

import jinja2
string = """
{% for name, address in girls.items()|reverse %}
    --------------------------------
    姓名: {{name}}, 地址: {{address}}
    索引(从1开始): {{loop.index}}
    索引(从0开始): {{loop.index0}}
    是否是第一次迭代: {{loop.first}}
    是否是最后一次迭代: {{loop.last}}
    序列的长度: {{loop.length}}
    --------------------------------
{% endfor %}
"""
env = jinja2.Environment()
temp = env.from_string(string)
render_string = temp.render(girls={"古明地觉": "地灵殿",
                                   "琪露诺": "雾之湖",
                                   "魔理沙": "魔法森林"})
print(render_string)
"""
    --------------------------------
    姓名: 魔理沙, 地址: 魔法森林
    索引(从1开始): 1
    索引(从0开始): 0
    是否是第一次迭代: True
    是否是最后一次迭代: False
    序列的长度: 3
    --------------------------------
    --------------------------------
    姓名: 琪露诺, 地址: 雾之湖
    索引(从1开始): 2
    索引(从0开始): 1
    是否是第一次迭代: False
    是否是最后一次迭代: False
    序列的长度: 3
    --------------------------------
    --------------------------------
    姓名: 古明地觉, 地址: 地灵殿
    索引(从1开始): 3
    索引(从0开始): 2
    是否是第一次迭代: False
    是否是最后一次迭代: True
    序列的长度: 3
    --------------------------------
"""

可以看到 jinja2 还是很强大的,因为它不仅仅是简单的替换,而是一个模板渲染引擎,并且内部还涉及到编译原理。jinja2 也是先通过 lexing 进行分词,然后 parser 解析成 AST,再基于 optimizer 优化AST,最后在当前的环境中执行。

所以 jinja2 一般用于渲染 HTML 页面等大型文本内容,那么问题来了,如果有一个 HTML 文本,jinja2 要如何加载它呢?

from jinja2 import Environment, FileSystemLoader
env = Environment(
    # 指定一个加载器,里面传入搜索路径
    loader=FileSystemLoader(".")
)
# 在指定的路径中查找文件并打开,同样会返回 Template 对象
temp = env.get_template("login.html")
# 注意:此处不能手动调用 Template
# 如果是手动调用 Template("login.html") 的话
# 那么 "login.html" 会被当成是普通的字符串
print(temp.render())
"""
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h3>欢迎来到古明地觉的编程教室</h3>
</body>
</html>
"""

当然啦,不管是以普通字符串的形式,还是以文本文件的形式,jinja2 的语法都是不变的。


宏的定义和使用



宏,说得通俗一点,就类似于函数。我们给一系列操作进行一个封装,再给一个名字,然后调用宏名的时候,就会执行预定义好的一系列操作。

from jinja2 import Environment
env = Environment()
# 通过 {% macro %} 可以定义一个宏
# 这里的宏叫 input,当然叫什么无所谓
string = """
{% macro input(name, value="", type="text") %}
<input type="{{type}}" name="{{name}}" value="{{value}}"/>
{% endmacro %}
{{input("satori","东方地灵殿")}}
{{input("marisa","魔法森林")}}
{{input("", "提交", "submit")}}
"""
temp = env.from_string(string)
print(temp.render().strip())
"""
<input type="text" name="satori" value="东方地灵殿"/>
<input type="text" name="marisa" value="魔法森林"/>
<input type="submit" name="" value="提交"/>
"""

此外宏也是可以导入的,既然涉及到导入,那么就需要写在文件里面了。而之所以要有宏的导入,也是为了分文件编程,这样看起来更加清晰。

marco.html

{% macro input(name, value="", type="text") %}
<input type="{{type}}" name="{{name}}" value="{{value}}"/>
{% endmacro %}

这样我们就把宏单独定义在一个文件里面,先通过 import "宏文件的路径" as xxx 来导入宏,然后再通过 xxx.宏名 调用即可。注意这里必须要起名字,也就是必须要 as。或者 from "宏文件的路径" import 宏名 [as xxx],这里起别名则是可选的。

login.html

{% import "macro.html" as macro  %}
{{macro.input("satori","东方地灵殿")}}
{{macro.input("mashiro","樱花庄的宠物女孩")}}
{{macro.input("", "提交", "submit")}}

然后 Python 代码和之前类似,直接加载 login.html 然后渲染即可。


include 的使用



include 的使用就很简单了,相当于 Ctrl + C 和 Ctrl + V。

1.txt

古明地觉,一个幽灵也为之惧怕的少女
但当你推开地灵殿的大门,却发现...

2.txt

{% include "1.txt" %}
她居然在调戏她的妹妹

我们使用的文件一直都是 html 文件,但 txt 文件也是可以的。

from jinja2 import Environment, FileSystemLoader
env = Environment(
    loader=FileSystemLoader(".")
)
temp = env.get_template("2.txt")
print(temp.render())
"""
古明地觉,一个幽灵也为之惧怕的少女
但当你推开地灵殿的大门,却发现...
她居然在调戏她的妹妹
"""

所以 include 就相当于将文件里的内容复制粘贴过来。


通过 set 和 with 语句定义变量



在模板中,我们还可以定义一个变量,然后在其它的地方用。

from jinja2 import Environment
env = Environment()
string = """
{% set username="satori" %}
<h2>{{username}}</h2>
{% with username="koishi" %}
<h2>{{username}}</h2>
{% endwith %}
<h2>{{username}}</h2>
"""
temp = env.from_string(string)
# 使用 set 设置变量,在全局都可以使用
# 使用 with 设置变量,那么变量只会在 with 语句块内生效
# 所以结尾才要有 {% endwith %} 构成一个语句块
print(temp.render().strip())
"""
<h2>satori</h2>
<h2>koishi</h2>
<h2>satori</h2>
"""

此外 with 还有另一种写法:

{% with %}
{% set username = "koishi" %}
{% endwith %}

这样写也是没问题的,因为 set 在 with 里面,所以变量只会在 with 语句块内生效。


模板继承



对于很多网站的页面来说,它的四周有很多内容都是不变的,如果每来一个页面都要写一遍的话,会很麻烦。因此我们可以将不变的部分先写好,在变的部分留一个坑,这就是父模板。然后子模板继承的时候,会将父模板不变的部分继承过来,然后将变的部分,也就是父模板中挖的坑填好。总结一下就是:父模板挖坑,子模板填坑。

base.html

<p>古明地觉:我的家在哪呢?</p>
{% block 古明地觉 %}
{% endblock %}
<p>魔理沙:我的家在哪呢?</p>
{% block 魔理沙 %}
{% endblock %}
<p>芙兰朵露:我的家在哪呢?</p>
{% block 芙兰朵露 %}
{% endblock %}
<p>找不到家的话,就跟我走吧</p>

child.html

{% extends "base.html" %}
{% block 古明地觉 %}
<p>你的家在地灵殿</p>
{% endblock %}
{% block 魔理沙 %}
<p>你的家在魔法森林</p>
{% endblock %}
{% block 芙兰朵露 %}
<p>你的家在红魔馆</p>
{% endblock %}

在 base.html 里面通过 {% block %} 挖坑,子模板继承过来之后再填坑。以下是 child.html 渲染之后的内容:

执行结果没有问题,并且父模板在挖坑的时候,如果里面有内容,那么子模板在继承之后会自动清除,但也可以使用 {{super()}} 保留下来

base.html

<p>古明地觉:我的家在哪呢?</p>
{% block 古明地觉 %}
<p>父模板挖坑时填的内容</p>
{% endblock %}
<p>魔理沙:我的家在哪呢?</p>
{% block 魔理沙 %}
{% endblock %}

child.html

{% extends "base.html" %}
{% block 古明地觉 %}
<p>子模板继承的时候,默认会清空父模板的内容</p>
<p>但可以通过 super 保留下来,以下是父模板写入的内容</p>
{{super()}}
{% endblock %}
{% block 魔理沙 %}
<p>通过 self.块名() 可以在一个块内引用其它块的内容</p>
<p>以下是 古明地觉 块里的内容</p>
{{self.古明地觉()}}
{% endblock %}

以下是 child.html 渲染之后的内容:

可以看到,在引用其它块的内容时,会把其它块继承的父模板的内容一块引用过来。因为一旦继承,那么就变成自己的了。

最后 {% extend "xxx.html"  %} 要放在最上面,不然容易出问题,在 Django 会直接报错。另外子模板中的代码一定要放在 block 语句块内,如果放在了外面,jinja2 是不会渲染的。

以上就是 jinja2 相关的内容,当我们希望按照指定规则生成文件时,不妨让 jinja2 来替你完成任务吧。

作为一款模板渲染引擎,jinja2 无疑是最出名的,但其实 jinja2 是借鉴了 Django 的模板渲染引擎。只不过 Django 的引擎和 Django 本身是强耦合的,而 jinja2 是独立存在的,这也使得它可以应用在除 web 框架之外的很多地方。

相关文章
|
5月前
|
前端开发
914.【前端】Taro集成towxml渲染markdown文档
914.【前端】Taro集成towxml渲染markdown文档
340 2
|
5月前
|
移动开发 JavaScript 前端开发
Flask 结合 Highcharts 实现动态渲染图表
Flask 结合 Highcharts 实现动态渲染图表
|
5月前
|
Python
python web框架fastapi模板渲染--Jinja2使用技巧总结
python web框架fastapi模板渲染--Jinja2使用技巧总结
263 2
|
5月前
|
缓存 JavaScript 安全
Django的模板渲染(二)
Django的模板渲染(二)
|
5月前
|
前端开发 JavaScript 开发者
Django的模板渲染(一)
Django的模板渲染(一)
|
11月前
|
Web App开发 缓存 JavaScript
11 Tornado - 使用模板
11 Tornado - 使用模板
37 0
|
5月前
|
XML 安全 测试技术
Jinja2:使用Python进行模板渲染的艺术
Jinja2:使用Python进行模板渲染的艺术
259 0
|
Python
【jinja2】flask和jinja2配合使用
【jinja2】flask和jinja2配合使用
120 0
Jinja2渲染模板字符串
Jinja2渲染模板字符串
56 0
|
PHP
easyswoole实现模板渲染
easyswoole实现模板渲染
111 0