• Jinja2 教程 第 1 部分 介绍和变量替换


    内容

    介绍

    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 本质上需要两个源成分,模板和数据,它们将用于呈现最终文档。

    j2-渲染流

    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 <module>
        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 "<template>", line 1, in top-level template code
    jinja2.exceptions.UndefinedError: 'type' is undefined
    

    通过严格的错误检查,我们会立即得到错误。

    值得注意的是 AnsibleStrictUndefined默认使用,所以当你使用它来渲染 Jinja 模板时,只要有对未定义变量的引用,你就会得到错误。

    总的来说,我建议始终启用未定义类型StrictUndefined如果您不这样做,您的模板中可能会出现一些很难找到的非常细微的错误。一开始可能很容易理解为什么输出中缺少一个值,但随着时间的推移,随着模板越来越大,人眼很难注意到有什么不对劲的地方。您绝对不想只在将配置加载到您的设备时意识到您的模板已损坏!

    添加评论

    在结束这篇文章之前,我只是想向您展示如何在模板中包含评论。一般来说,模板应该是自我解释的,但是当多人在同一个模板上工作时,注释会派上用场,你未来的自己可能也会感谢解释不明显的部分。您还可以使用注释语法在调试期间禁用部分模板。

    使用{# ... #}语法添加注释。{#介于两者之间的任何内容都#}被视为注释,并且将被引擎忽略。

    下面是我们的第一个示例,其中添加了一些注释:

    from jinja2 import Template
    
    template = """hostname {{ hostname }}
    
    {# DNS configuration -#}
    no ip domain lookup
    ip domain name local.lab
    ip name-server {{ name_server_pri }}
    ip name-server {{ name_server_sec }}
    
    {# Time servers config, we should use pool.ntp.org -#}
    ntp server {{ ntp_server_pri }} prefer
    ntp server {{ ntp_server_sec }}
    ntp server {{ ntp_server_trd }}"""
    
    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
    ntp server 
    

    没有评论的迹象。尽管您可能-在关闭之前已经注意到(破折号)字符#}如果没有那个破折号,每条评论之后都会添加一个额外的空行。

    Jinja 中的空格处理不是很直观,它可能是该语言中最令人困惑的部分之一。在未来的一篇文章中,我将详细讨论不同的场景和技术,它们将使您的模板看起来完全符合您的要求。

    结论

    Jinja 教程系列的第一篇文章到此结束。我在这里向您展示的内容应该足以让您开始创建自己的模板。在以后的文章中,我们将了解其他功能,并通过示例说明用例。

    敬请关注!

    参考

  • 相关阅读:
    A
    B
    A
    A
    B
    C
    有趣的平方和的推导
    一篇写的非常好的匈牙利算法文章
    2014 UESTC Training for Data Structures G
    2014 UESTC Training for Data Structures H
  • 原文地址:https://www.cnblogs.com/a00ium/p/16058267.html
Copyright © 2020-2023  润新知