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 提供了開箱即用的支援。
對於您自己的程式,如果您有從資料結構動態生成的文字塊,您應該考慮使用 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 <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 教程系列的第一篇文章到此結束。我在這裡向您展示的內容應該足以讓您開始建立自己的模板。在以後的文章中,我們將瞭解其他功能,並通過示例說明用例。
敬請關注!
參考
- Jinja2 (2.11.x) 最新版本的官方文件。可在:https ://jinja.palletsprojects.com/en/2.11.x/
- PyPi 上的 Jinja2 Python 庫。可在:https ://pypi.org/project/Jinja2/
- 帶有 Jinja 原始碼的 GitHub 儲存庫。可在:https ://github.com/pallets/jinja/
- 包含這篇文章資源的 GitHub 儲存庫。可在:https ://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p1-intro-substitution