打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
详解模板渲染引擎 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=[123],
    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=[12345],
    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 框架之外的很多地方。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
鸡蛋羹 ? 在web.py中使用jinja2模板
第21天:Web开发 Jinja2模板引擎
Flask搭建简单服务器
Flask 教程,第二部分:模板
模板templates(17)
用python帮你生产指定内容的word文档
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服