详解模板渲染引擎 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 框架之外的很多地方。