Jinja 模板基础知识

admin2025-06-29 16:35:195038

内容

介绍

Jinja2 是什么?

它在哪里使用?

它有什么好?

我为什么要使用它?

它是如何工作的?

Jinja 模板基础知识

变量替换

例子

Python 示例

实例

字典作为变量

未定义的变量

添加评论

结论

参考

包含本文资源的 GitHub 存储库

介绍

Jinja2 是什么?

Jinja2 是 Python 生态系统中广泛使用的功能丰富的模板语言。它可以直接在您的 Python 程序中使用,并且许多大型应用程序都将其用作模板渲染引擎。

模板语言允许创建基于文本的文档,其中一些内容可以动态生成。生成的文件可以是 HTML、JSON、XML 或任何使用纯文本作为编码的文件。这个想法是在代码中捕获业务逻辑,同时提供模板设计器工具来控制最终文档的流程和布局。

它在哪里使用?

使用 Jinja2 的一些值得注意的应用程序示例是 Ansible、Django、Flask、Salt 和 Trac。许多其他 Python Web 框架以及无数其他 Python 项目也使用它。

它有什么好?

Jinja2 有很多很棒的功能:

控制结构(循环和条件语句)

丰富的内置过滤器和测试集

模板继承

支持自定义过滤器

HTML 转义

用于安全呈现不受信任模板的沙箱环境

易于调试

可配置的语法

上述功能的讨论和示例使用将构成本系列的大部分内容。

我为什么要使用它?

像 Flask 和 Django 这样的 Web 框架,或者像 Ansible 和 Salt 这样的自动化框架,为 Jinja 提供了开箱即用的支持。使用其中任何一个时,这是模板引擎的自然选择。Ansible 甚至在其 Playbooks 中使用了大量的 Jinja 语法。

对于您自己的程序,如果您有从数据结构动态生成的文本块,您应该考虑使用 Jinja2。它不仅在逻辑上将您的模板与您的代码分开,还允许其他人独立地对模板进行更改,而无需修改应用程序的源代码。

我认为对 Jinja2 有很好的了解会让你变得更有效率。它在网络自动化领域也无处不在。随着 Jinja 的广泛使用,您会发现花时间学习它是值得的。

它是如何工作的?

Jinja2 本质上需要两个源成分,模板和数据,它们将用于呈现最终文档。

Jinja2 不关心数据来自哪里,这可能来自某些 API 返回的 JSON,从静态 YAML 文件加载,或者只是在我们的应用程序中定义的 Python Dict。

重要的是我们有 Jinja 模板和一些数据来渲染它。

Jinja 模板基础知识

我们现在知道 Jinja 是什么以及为什么要使用它。是时候开始查看简单的示例以熟悉模板的一般外观和结构了。

模板化的基本思想是获取一些文本文档,并找出所有实例中哪些位不变,哪些可以参数化。也就是说,我们希望文本的某些元素根据我们手头的可用数据进行更改。

由于我主要使用网络设备配置,这就是我将在示例中使用的内容。

变量替换

下面是一个简短的 Cisco IOS 配置片段,我们将在第一个示例中使用它。

hostname par-rtr-core-01

no ip domain lookup

ip domain name local.lab

ip name-server 1.1.1.1

ip name-server 8.8.8.8

ntp server 0.pool.ntp.org prefer

ntp server 1.pool.ntp.org

我们需要采取的第一步是识别静态元素和可能在设备之间更改的元素。

在我们的例子中,“hostname”、“ip name-server”等词是特定网络操作系统使用的配置语句。只要设备上运行相同的 NOS,它们就保持不变。

实际的主机名,以及可能的名称服务器和 ntp 服务器的名称,应转换为在呈现模板时将替换实际值的变量。

现在,我对某些元素说“可能”,因为这些决定是特定于您的环境的。通常,即使当前在任何地方都使用相同的值,早期参数化这些元素也会更容易。随着时间的推移,我们的网络可能会增长,其中一些值可能取决于区域或数据中心位置,这自然适合使用变量引用。或者您可能想更改其中一个名称服务器,通过参数化这些值,您只需在一处更改它们,然后为所有设备重新生成配置。

为了我们的示例,我决定将主机名、名称服务器和 ntp 服务器转换为变量。我们的最终模板可以在下面找到:

hostname {{ hostname }}

no ip domain lookup

ip domain name local.lab

ip name-server {{ name_server_pri }}

ip name-server {{ name_server_sec }}

ntp server {{ ntp_server_pri }} prefer

ntp server {{ ntp_server_sec }}

在 Jinja 中,在双开和双闭花括号之间发现的任何内容都会告诉引擎评估然后打印它。在大括号之间找到的唯一内容是名称,特别是变量名称。Jinja 希望您将此变量提供给引擎,并且它只需将变量替换{{ name }}语句引用的值替换为该值。

换句话说,Jinja 只是用变量名代替它的值。这是您将在模板中使用的最基本的组件。

好的,所以一件事被另一件事取代。但是我们如何定义“事物”以及如何将其赋予 Jinja 引擎?

这是我们需要选择将数据提供给模板的数据格式和工具的地方。

有很多选项,以下是最常用的。

对于数据格式:

YAML 文件

JSON文件

本机 Python 字典

对于胶水,一些选项:

Python 脚本

Ansible 剧本

Web 框架中的内置支持(Flask、Django)

例子

对于我的大多数示例,我将使用各种 Python 脚本和 Ansible playbook,数据来自本机 Python dict 以及 YAML 和 JSON 文件。

在这里,我将使用最小的 Python 脚本,然后是 Ansible 剧本。Ansible 示例展示了使用很少或根本没有编程技能来生成模板是多么容易。您还将在基础设施自动化领域看到很多 Ansible,因此最好了解如何使用它来生成带有模板的文件。

Python 示例

首先,Python脚本:

from jinja2 import Template

template = """hostname {{ hostname }}

no ip domain lookup

ip domain name local.lab

ip name-server {{ name_server_pri }}

ip name-server {{ name_server_sec }}

ntp server {{ ntp_server_pri }} prefer

ntp server {{ ntp_server_sec }}"""

data = {

"hostname": "core-sw-waw-01",

"name_server_pri": "1.1.1.1",

"name_server_sec": "8.8.8.8",

"ntp_server_pri": "0.pool.ntp.org",

"ntp_server_sec": "1.pool.ntp.org",

}

j2_template = Template(template)

print(j2_template.render(data))

和输出:

hostname core-sw-waw-01

no ip domain lookup

ip domain name local.lab

ip name-server 1.1.1.1

ip name-server 8.8.8.8

ntp server 0.pool.ntp.org prefer

ntp server 1.pool.ntp.org

它工作得非常好。模板是使用我们提供给它的数据呈现的。

正如你所看到的,这真的很简单,模板只是一些带有占位符的文本,数据是一个标准的 Python 字典,每个键名对应于模板中的变量名。我们只需要创建 jinja2 模板对象并将我们的数据传递给它的渲染方法。

我应该提一下,我们在上面的脚本中渲染 Jinja 的方式应该只用于调试和概念验证代码。在现实世界中,数据应该来自外部文件或数据库,并且应该在我们设置 Jinja 环境后加载模板。我不想一开始就搅浑水,因为稍后我们会更深入地研究这些概念。

实例

只是为了展示替代方案,我们还将使用 Ansible 渲染相同的模板。这里模板存储在一个单独的文件中,数据来自与设备名称匹配的主机 var 文件,这就是我们通常记录每个主机数据的方式。

下面是目录结构:

przemek@quasar:~/nauto/jinja/ansible$ ls -R

.:

hosts.yml host_vars out templates templ-simple-render.yml vars

./host_vars:

core-sw-waw-01.yml

./out:

./templates:

core-sw-waw-01.j2

./vars:

带有数据的 YAML 文件:

(venv) przemek@quasar:~/nauto/jinja/ansible$ cat host_vars/core-sw-waw-01.yml

---

hostname: core-sw-waw-01

name_server_pri: 1.1.1.1

name_server_sec: 8.8.8.8

ntp_server_pri: 0.pool.ntp.org

ntp_server_sec: 1.pool.ntp.org

模板与 Python 示例中使用的模板相同,但它存储在外部文件中:

(venv) przemek@quasar:~/nauto/jinja/ansible$ cat templates/base-cfg.j2

hostname {{ hostname }}

no ip domain lookup

ip domain name local.lab

ip name-server {{ name_server_pri }}

ip name-server {{ name_server_sec }}

ntp server {{ ntp_server_pri }} prefer

ntp server {{ ntp_server_sec }}

最后,进行渲染的剧本:

(venv) przemek@quasar:~/nauto/jinja/ansible$ cat j2-simple-render.yml

---

- hosts: core-sw-waw-01

gather_facts: no

connection: local

tasks:

- name: Render config for host

template:

src: "templates/base-cfg.j2"

dest: "out/{{ inventory_hostname }}.cfg"

剩下的就是执行我们的剧本:

(venv) przemek@quasar:~/nauto/jinja/ansible$ ansible-playbook -i hosts.yml j2-simple-render.yml

PLAY [core-sw-waw-01] *************************************************************************************************************

TASK [Render config for host] *****************************************************************************************************

changed: [core-sw-waw-01]

PLAY RECAP ************************************************************************************************************************

core-sw-waw-01 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

结果与 Python 脚本的输出相匹配,除了这里我们将输出保存到文件中:

(venv) przemek@quasar:~/nauto/jinja/ansible$ cat out/core-sw-waw-01.cfg

hostname core-sw-waw-01

no ip domain lookup

ip domain name local.lab

ip name-server 1.1.1.1

ip name-server 8.8.8.8

ntp server 0.pool.ntp.org prefer

ntp server 1.pool.ntp.org

这些示例可能并不过分令人兴奋,但仅通过变量替换我们就可以创建一些有用的模板。您还可以看到在 Ansible 中开始渲染模板所需的工作量非常小。

字典作为变量

让我们继续变量替换,但我们将使用更复杂的数据结构。这次我们将使用字典变量。

使用字典(也称为哈希表或对象)允许对相关数据进行逻辑分组。例如,与接口相关的属性可以存储在字典中,这里以 JSON 格式显示:

{

"interface": {

"name": "GigabitEthernet1/1",

"ip_address": "10.0.0.1/31",

"description": "Uplink to core",

"speed": "1000",

"duplex": "full",

"mtu": "9124"

}

}

访问字典中的项目非常方便,您只需要知道键即可获得相应的值,因此在 JSON 和 YAML 的世界中无处不在。

Jinja 提供了一种使用“点”表示法访问字典键的便捷方法。但是,这只适用于名称中没有特殊字符的键。

使用上面的接口字典,我们可以使用下面的模板创建接口配置片段:

interface {{ interface.name }}

description {{ interface.description }}

ip address {{ interface.ip_address }}

speed {{ interface.speed }}

duplex {{ interface.duplex }}

mtu {{ interface.mtu }}

这就是我们在渲染后得到的:

interface GigabitEthernet1/1

description Uplink to core

ip address 10.0.0.1/31

speed 1000

duplex full

mtu 9124

所以这很简单,但已经为使用简单变量开辟了更多可能性,特别是对于具有多个属性的对象。

现在,请记住我提到过“点”表示法不能与名称中包含特殊字符的键一起使用。如果您有.- 点、-- 破折号或任何其他不允许作为 Python 变量名称中的字符的字符,则不能使用点表示法。在这些情况下,您需要使用标准 Python 下标表示法[]。我发现这主要是 IP 地址密钥的问题。

例如,要访问10.0.0.0/24以下字典中的键,我们必须使用 Python 下标:

prefixes:

10.0.0.0/24:

description: Corporate NAS

region: Europe

site: Telehouse-West

模板使用10.0.0.0/24键:

Details for 10.0.0.0/24 prefix:

Description: {{ prefixes['10.0.0.0/24'].description }}

Region: {{ prefixes['10.0.0.0/24'].region }}

Site: {{ prefixes['10.0.0.0/24'].site }}

未定义的变量

我觉得这是一个讨论 Jinja 遇到未定义变量时会发生什么的好地方。在处理使用大量变量的较大模板时,这实际上是相对常见的。

默认情况下,当遇到带有未定义变量的评估语句时,Jinja 会将其替换为空字符串。这对于编写他们的第一个模板的人来说常常是一个惊喜。

undefined可以通过将Template 和 Environment 对象采用的参数设置为不同的 Jinja 未定义类型来更改此行为。默认类型是Undefined,但还有其他类型可用,StrictUndefined是最有用的一种。通过使用StrictUndefined类型,我们告诉 Jinja 在尝试使用未定义变量时引发错误。

将以下模板的渲染结果与提供的数据进行比较,第一个使用默认Undefined类型,第二个使用StrictUndefined:

from jinja2 import Template

template = "Device {{ name }} is a {{ type }} located in the {{ site }} datacenter."

data = {

"name": "waw-rtr-core-01",

"site": "warsaw-01",

}

j2_template = Template(template)

print(j2_template.render(data))

Device waw-rtr-core-01 is a located in the warsaw-01 datacenter.

我们的模板引用了名为的变量type,但我们提供的数据没有该变量,因此最终评估结果为空字符串。

第二次运行将使用StrictUndefined类型:

from jinja2 import Template, StrictUndefined

template = "Device {{ name }} is a {{ type }} located in the {{ site }} datacenter."

data = {

"name": "waw-rtr-core-01",

"site": "warsaw-01",

}

j2_template = Template(template, undefined=StrictUndefined)

(venv) przemek@quasar:~/nauto/jinja/python$ python j2_undef_var_strict.py

Traceback (most recent call last):

File "j2_undef_var_strict.py", line 12, in

print(j2_template.render(data))

File "/home/przemek/nauto/jinja/python/venv/lib/python3.6/site-packages/jinja2/environment.py", line 1090, in render

self.environment.handle_exception()

File "/home/przemek/nauto/jinja/python/venv/lib/python3.6/site-packages/jinja2/environment.py", line 832, in handle_exception

reraise(*rewrite_traceback_stack(source=source))

File "/home/przemek/nauto/jinja/python/venv/lib/python3.6/site-packages/jinja2/_compat.py", line 28, in reraise

raise value.with_traceback(tb)

File "