Jinja2 教程 - 第 4 部分 - 模板過濾器
這是 Jinja2 教程的第 4 部分,我們將繼續研究語言特性,特別是我們將討論模板過濾器。我們將瞭解過濾器是什麼以及如何在模板中使用它們。我還將向您展示如何編寫自己的自定義過濾器。
Jinja2 過濾器概述
讓我們直接進入。 Jinja2 過濾器是我們用來轉換變數中儲存的資料的東西。|
我們通過在變數後放置管道符號和過濾器名稱來應用過濾器。
過濾器可以更改源資料的外觀和格式,甚至可以生成從輸入值派生的新資料。重要的是原始資料被轉換的結果替換,這就是最終呈現的模板。
下面是一個示例,展示了一個簡單的過濾器的作用:
模板:
First name: {{ first_name | capitalize }}
資料:
first_name: przemek
結果:
First name: Przemek
我們將first_name
變數傳遞給capitalize
過濾器。正如過濾器的名稱所暗示的那樣,變數儲存的字串最終將大寫。這正是我們所看到的。很酷,對吧?
將過濾器視為將 Jinja2 變數作為引數的函式可能會有所幫助,與標準 Python 函式的唯一區別是我們使用的語法。
Python 等價物capitalize
看起來像這樣:
def capitalize(word):
return word.capitalize()
first_name = "przemek"
print("First name: {}".format(capitalize(first_name)))
太好了,你說。但我怎麼知道這capitalize
是一個過濾器?它從哪裡來的?
這裡沒有魔法。有人必須對所有這些過濾器進行編碼,並讓它們可供我們使用。Jinja2 帶有許多有用的過濾器,capitalize
就是其中之一。
所有內建過濾器都記錄在 Jinja2 官方文件中。我在參考文獻中包含了連結,在這篇文章的後面,我將展示一些在我看來更有用的過濾器示例。
多個引數
我們不僅限於簡單的過濾器,例如capitalize
. 一些過濾器可以在括號中使用額外的引數。這些可以是關鍵字或位置引數。
下面是一個採用額外引數的過濾器示例。
模板:
ip name-server {{ name_servers | join(" ") }}
資料:
name_servers:
- 1.1.1.1
- 8.8.8.8
- 9.9.9.9
- 8.8.4.4
結果:
ip name-server 1.1.1.1 8.8.8.8 9.9.9.9 8.8.4.4
過濾器通過將列表的元素與空格作為分隔符粘合在一起來獲取join
儲存的列表並建立一個字串。name_servers
分隔符是我們在括號中提供的引數,我們可以根據需要使用不同的引數。
您應該參考文件以瞭解哪些引數(如果有)可用於給定過濾器。大多數過濾器使用合理的預設值,並且不需要顯式指定所有引數。
連結過濾器
我們已經看到了基本的過濾器用法,但我們可以做得更多。我們可以將過濾器連結在一起。這意味著可以一次使用多個過濾器,每個過濾器用管道分隔|
。
Jinja 從左到右應用鏈式過濾器。來自最左邊過濾器的值被送入下一個過濾器,並重復該過程直到沒有更多過濾器。只有最終結果才會出現在渲染模板中。
讓我們看看它是如何工作的。
資料:
scraped_acl:
- " 10 permit ip 10.0.0.0/24 10.1.0.0/24"
- " 20 deny ip any any"
模板
{{ scraped_acl | first | trim }}
結果
10 permit ip 10.0.0.0/24 10.1.0.0/24
我們傳遞了包含兩個要first
過濾的專案的列表。這從列表中返回了第一個元素,並將其交給trim
過濾器刪除了前導空格。
最終結果是 line 10 permit ip 10.0.0.0/24 10.1.0.0/24
。
過濾器連結是一項強大的功能,它允許我們一次執行多個轉換。另一種方法是儲存中間結果,這會降低可讀性並且不會那麼優雅。
附加過濾器和自定義過濾器
儘管它們很棒,但內建過濾器非常通用,許多用例需要更具體的過濾器。這就是為什麼像 Ansible 或 Salt 這樣的自動化框架提供了許多額外的過濾器來覆蓋廣泛的場景。
在這些框架中,您會發現過濾器可以轉換 IP 物件、在 YAML/Json 中顯示資料,甚至應用正則表示式,僅舉幾例。在參考資料中,您可以找到每個框架中可用過濾器的文件連結。
最後,您可以自己建立新的過濾器!Jinja2 提供了新增自定義過濾器的鉤子。這些只是 Python 函式,因此如果您在編寫 Python 函式之前也可以編寫自己的過濾器!
上述自動化框架也支援自定義過濾器,編寫它們的過程類似於 vanilla Jinja2。您再次需要編寫 Python 函式,然後給定工具的文件將向您顯示將模組註冊為過濾器所需的步驟。
為什麼要使用過濾器?
沒有工具能很好地解決所有問題。有些工具是尋找問題的解決方案。那麼,為什麼要使用 Jinja2 過濾器呢?
與大多數模板語言一樣,Jinja 的建立考慮了 Web 內容。雖然資料以標準化格式儲存在資料庫中,但我們在向用戶顯示文件時經常需要對其進行轉換。這就是像 Jinja 這樣的語言及其過濾器可以隨時隨地修改資料的呈現方式,而無需觸及後端。這就是過濾器的賣點。
下面是我個人的看法,為什麼我認為 Jinja2 過濾器是該語言的一個很好的補充:
1.它們允許非程式設計師執行簡單的資料轉換。
這適用於普通過濾器以及自動化框架提供的額外過濾器。例如,網路工程師知道他們的 IP 地址,他們可能希望在沒有任何程式設計知識的情況下在模板中對其進行操作。過濾器來拯救!
2. 你會得到可預測的結果。
如果你使用一般可用的過濾器,任何有一些 Jinja2 經驗的人都會知道他們在做什麼。這使人們在檢視其他人編寫的模板時能夠加快速度。
3. 過濾器維護良好且經過測試。
內建過濾器以及自動化框架提供的過濾器被很多人廣泛使用。這使您對他們給出正確的結果並且沒有很多錯誤充滿信心。
4. 最好的程式碼是完全沒有程式碼。
在您向程式新增資料轉換操作或建立新過濾器的那一刻,您將永遠對程式碼負責。任何錯誤、功能請求和測試都將在解決方案的整個生命週期內出現。在學習時寫儘可能多的東西,但儘可能在生產中使用已經可用的解決方案。
什麼時候不使用過濾器?
過濾器非常強大,可以為我們節省大量時間。但權力越大,責任越大。過度使用過濾器,您最終會得到難以理解和維護的模板。
你知道那些沒有人,包括你自己,能在幾個月後理解的聰明的一班人嗎?通過連結大量過濾器很容易進入這些情況,尤其是那些接受多個引數的過濾器。
我使用以下啟發式方法來幫助我確定我所做的是否太複雜:
- 我寫的東西是我理解的極限嗎?
- 我是不是覺得我剛剛寫的很聰明?
- 我是否以一種起初看起來並不明顯的方式使用了許多鏈式過濾器?
如果您對上述至少一項的回答是肯定的,那麼您可能正在處理“為自己好而太聰明”的情況。您的用例可能沒有更簡單的解決方案,但您可能需要進行重構。如果您不確定是否是這種情況,最好詢問您的同事或諮詢社群。
為了向您展示事情會變得多麼糟糕,這是我幾年前寫的 Jinja2 行的示例。這些使用 Ansible 提供的過濾器,它變得非常複雜,以至於我不得不定義中間變數。
看看它並嘗試弄清楚它做了什麼,更重要的是,它是如何做到的。
模板,為了簡潔而刪減:
{% for p in ibgp %} {% set jq = "[?name=='" + p.port + "'].{ myip: ip, peer: peer }" %} {% set el = ports | json_query(jq) %} {% set peer_ip = hostvars[el.0.peer] | json_query('ports[*].ip') | ipaddr(el.0.myip) %} ... {% endfor %}
與模板一起使用的示例資料:
ibgp: - { port: Ethernet1 } - { port: Ethernet2 } .. ports: - { name: Ethernet1, ip: "10.0.12.1/24", speed: 1000full, desc: "vEOS-02
這裡有很多東西要解壓。在第一行中,我將查詢字串分配給變數作為字元轉義問題的解決方法。在第二行中,我使用json_query
來自第一行變數的引數應用過濾器,結果儲存在另一個輔助變數中。最後,在第三行中,我應用了兩個鏈式過濾器json_query
和ipaddr
.
這三行的最終結果應該是在給定介面上找到的 BGP 對等體的 IP 地址。
我相信你會同意我的看法,這很糟糕。這個解決方案在我之前提到的啟發式方法旁邊是否有任何標記?是的!他們三個!這是重構的主要候選者。
在這種情況下,我們通常可以做兩件事:
- 在呼叫渲染的上層對資料進行預處理,例如 Python、Ansible 等。
- 編寫自定義過濾器。
- 修改資料模型,看看是否可以簡化。
在這種情況下,我選擇了選項 2,我編寫了自己的過濾器,幸運的是,這是我們列表中的下一個主題。
編寫自己的過濾器
正如我已經提到的,要編寫自定義過濾器,您需要親自動手並編寫一些 Python 程式碼。不過不要害怕!如果你曾經寫過一個帶引數的函式,那麼你已經得到了它所需要的一切。沒錯,我們不需要做太花哨的事情,任何普通的 Python 函式都可以成為過濾器。它只需要至少接受一個引數並且它必須返回一些東西。
這是我們將在 Jinja2 引擎中註冊為過濾器的函式示例:
# hash_filter.py import hashlib def j2_hash_filter(value, hash_type="sha1"): """ Example filter providing custom Jinja2 filter - hash Hash type defaults to 'sha1' if one is not specified :param value: value to be hashed :param hash_type: valid hash type :return: computed hash as a hexadecimal string """ hash_func = getattr(hashlib, hash_type, None) if hash_func: computed_hash = hash_func(value.encode("utf-8")).hexdigest() else: raise AttributeError( "No hashing function named {hname}".format(hname=hash_type) ) return computed_hash
在 Python 中,這是我們告訴 Jinja2 過濾器的方式:
# hash_filter_test.py import jinja2 from hash_filter import j2_hash_filter env = jinja2.Environment() env.filters["hash"] = j2_hash_filter tmpl_string = """MD5 hash of '$3cr3tP44$$': {{ '$3cr3tP44$$' | hash('md5') }}""" tmpl = env.from_string(tmpl_string) print(tmpl.render())
渲染結果:
MD5 hash of '$3cr3tP44$$': ec362248c05ae421533dd86d86b6e5ff
看那個!我們自己的過濾器!它的外觀和感覺就像內建的 Jinja 過濾器,對吧?
它有什麼作用?它公開了 Pythonhashlib
庫中的雜湊函式,以允許在 Jinja2 模板中直接使用雜湊。如果你問我,那就太好了。
簡而言之,以下是建立自定義過濾器所需的步驟:
-
建立一個至少接受一個引數並返回一個值的函式。第一個引數始終是
|
符號前面的 Jinja 變數。括號中提供了後續引數(...)
。 -
在 Jinja2 環境中註冊函式。在 Python 中,將您的函式插入到
filters
字典中,這是Environment
物件的一個屬性。鍵名是您希望呼叫過濾器的名稱,在這裡hash
,值是您的功能。 - 您現在可以像使用任何其他 Jinja 過濾器一樣使用您的過濾器。
使用 Ansible 自定義過濾器修復“太聰明”的解決方案
我們知道如何編寫自定義過濾器,所以現在我可以向您展示我是如何用聰明的技巧替換模板的一部分的。
這是我完全榮耀的自定義過濾器:
# get_peer_info.py import ipaddress def get_peer_info(our_port_info, hostvars): peer_info = {"name": our_port_info["peer"]} our_net = ipaddress.IPv4Interface(our_port_info["ip"]).network peer_vars = hostvars[peer_info["name"]] for _, peer_port_info in peer_vars["ports"].items(): if not peer_port_info["ip"]: continue peer_net_obj = ipaddress.IPv4Interface(peer_port_info["ip"]) if our_net == peer_net_obj.network: peer_info["ip"] = peer_net_obj.ip break return peer_info class FilterModule(object): def filters(self): return { 'get_peer_info': get_peer_info }
第一部分是你以前見過的,它是一個 Python 函式,它接受兩個引數並返回一個值。當然,它比“聰明的”三行更長,但它的可讀性要強得多。
這裡有更多的結構,變數有有意義的名字,我可以馬上知道它在做什麼。更重要的是,我知道它是如何做到的,過程被分解成許多易於遵循的單獨步驟。
Ansible 中的自定義過濾器
我的解決方案的第二部分與普通 Python 示例有點不同:
class FilterModule(object): def filters(self): return { 'get_peer_info': get_peer_info }
這就是你告訴 Ansible 你想get_peer_info
註冊為 Jinja2 過濾器的方式。
您FilterModule
使用一種名為 的方法建立名為的類filters
。此方法必須返回帶有過濾器的字典。字典中的鍵是過濾器的名稱,值是函式。我說的是過濾器而不是過濾器,因為您可以在一個檔案中註冊多個過濾器。如果您願意,您可以選擇為每個檔案設定一個過濾器。
完成所有操作後,您需要將 Python 模組放入filter_plugins
目錄中,該目錄應位於儲存庫的根目錄中。有了這些,您就可以在 Ansible Playbooks 和 Jinja2 模板中使用您的過濾器。
您可以在下面看到我的劇本與模組相關的目錄deploy_base.yml
結構get_peer_info.py
。
.
├── ansible.cfg
├── deploy_base.yml
├── filter_plugins
│ └── get_peer_info.py
├── group_vars
...
├── hosts
├── host_vars
...
└── roles
└── base
Jinja2 過濾器 - 使用示例
所有 Jinja2 過濾器都在官方文件中有詳細記錄,但我覺得其中一些可以使用更多示例。您將在下面找到我的主觀選擇以及一些評論和解釋。
批
batch(value, linecount, fill_with=None)
- 允許您將列表元素分組到多個儲存桶中,每個儲存桶最多包含n 個元素,其中n是我們指定的數字。可選地,我們還可以要求batch
使用預設條目填充儲存桶,以使所有儲存桶的長度恰好為 n。結果是列表列表。
我發現將專案分成固定大小的組很方便。
模板:
{% for i in sflow_boxes|batch(2) %} Sflow group{{ loop.index }}: {{ i | join(', ') }} {% endfor %}
資料:
sflow_boxes:
- 10.180.0.1
- 10.180.0.2
- 10.180.0.3
- 10.180.0.4
- 10.180.0.5
結果:
Sflow group1: 10.180.0.1, 10.180.0.2
Sflow group2: 10.180.0.3, 10.180.0.4
Sflow group3: 10.180.0.5
中央
center(value, width=80)
- 通過新增空格填充在給定寬度的欄位中居中值。在向報告新增格式時很方便。
模板:
{{ '-- Discovered hosts --' | center }} {{ hosts | join('\n') }}
資料:
hosts:
- 10.160.0.7
- 10.160.0.9
- 10.160.0.3
結果:
-- Discovered hosts --
10.160.0.7
10.160.0.9
10.160.0.15
預設
default(value, default_value='', boolean=False)
- 如果未指定傳遞的變數,則返回預設值。用於防範未定義的變數。也可以用於我們想要設定為預設值的可選屬性。
在下面的示例中,我們將介面放置在其配置的 vlan 中,或者如果未指定 vlan,我們預設將它們分配給 vlan 10。
模板:
{% for intf in interfaces %} interface {{ intf.name }} switchport mode access switchport access vlan {{ intf.vlan | default('10') }} {% endfor %}
資料:
interfaces:
- name: Ethernet1
vlan: 50
- name: Ethernet2
vlan: 50
- name: Ethernet3
- name: Ethernet4
結果:
interface Ethernet1
switchport mode access
switchport access vlan 50
interface Ethernet2
switchport mode access
switchport access vlan 50
interface Ethernet3
switchport mode access
switchport access vlan 10
interface Ethernet4
switchport mode access
switchport access vlan 10
字典排序
dictsort(value, case_sensitive=False, by='key', reverse=False)
- 允許我們對字典進行排序,因為它們在 Python 中預設不排序。預設情況下按鍵排序,但您可以使用屬性請求按值by='value'
排序。
在下面的示例中,我們按名稱(dict 鍵)對字首列表進行排序:
模板:
{% for pl_name, pl_lines in prefix_lists | dictsort %} ip prefix list {{ pl_name }} {{ pl_lines | join('\n') }} {% endfor %}
資料:
prefix_lists:
pl-ntt-out:
- permit 10.0.0.0/23
pl-zayo-out:
- permit 10.0.1.0/24
pl-cogent-out:
- permit 10.0.0.0/24
結果:
ip prefix list pl-cogent-out
permit 10.0.0.0/24
ip prefix list pl-ntt-out
permit 10.0.0.0/23
ip prefix list pl-zayo-out
permit 10.0.1.0/24
在這裡,我們按優先順序(dict值)排序一些對等列表,更高的值更受歡迎,因此使用reverse=true
:
模板:
BGP peers by priority {% for peer, priority in peer_priority | dictsort(by='value', reverse=true) %} Peer: {{ peer }}; priority: {{ priority }} {% endfor %}
資料:
peer_priority:
ntt: 200
zayo: 300
cogent: 100
結果:
BGP peers by priority
Peer: zayo; priority: 300
Peer: ntt; priority: 200
Peer: cogent; priority: 100
漂浮
float(value, default=0.0)
- 將值轉換為浮點數。API 響應中的數值有時以字串形式出現。通過float
我們可以確保在進行比較之前轉換字串。
這是一個使用float
.
模板:
{% if eos_ver | float >= 4.22 %} Detected EOS ver {{ eos_ver }}, using new command syntax. {% else %} Detected EOS ver {{ eos_ver }}, using old command syntax. {% endif %}
資料:
eos_ver: "4.10"
結果
Detected EOS ver 4.10, using old command syntax.
通過...分組
groupby(value, attribute)
- 用於根據屬性之一對物件進行分組。您可以選擇使用點表示法按巢狀屬性進行分組。此過濾器可用於基於特徵值的報告或為僅適用於物件子集的操作選擇專案。
在下面的示例中,我們根據分配給它們的 vlan 對介面進行分組:
模板:
{% for vid, members in interfaces | groupby(attribute='vlan') %} Interfaces in vlan {{ vid }}: {{ members | map(attribute='name') | join(', ') }} {% endfor %}
資料:
interfaces:
- name: Ethernet1
vlan: 50
- name: Ethernet2
vlan: 50
- name: Ethernet3
vlan: 50
- name: Ethernet4
vlan: 60
結果:
Interfaces in vlan 50: Ethernet1, Ethernet2, Ethernet3
Interfaces in vlan 60: Ethernet4
整數
int(value, default=0, base=10)
- 與浮點數相同,但在這裡我們將值轉換為整數。也可用於將其他基數轉換為十進位制基數:
下面的示例顯示了十六進位制到十進位制的轉換。
模板:
LLDP Ethertype hex: {{ lldp_ethertype }} dec: {{ lldp_ethertype | int(base=16) }}
資料:
lldp_ethertype: 88CC
結果:
LLDP Ethertype
hex: 88CC
dec: 35020
加入
join(value, d='', attribute=None)
- 非常非常有用的過濾器。獲取序列的元素並將連線的元素作為字串返回。
對於只想顯示專案而不進行任何操作的情況,它可以代替for
迴圈。在這些情況下,我發現join
版本更具可讀性。
模板:
ip name-server {{ name_servers | join(" ") }}
資料:
name_servers:
- 1.1.1.1
- 8.8.8.8
- 9.9.9.9
- 8.8.4.4
結果:
ip name-server 1.1.1.1 8.8.8.8 9.9.9.9 8.8.4.4
地圖
map(*args, **kwargs)
- 可用於查詢屬性或對序列中的所有物件應用過濾器。
例如,如果您想跨裝置名稱規範字母大小寫,您可以一次性應用過濾器。
模板:
Name-normalized device list: {{ devices | map('lower') | join('\n') }}
資料:
devices:
- Core-rtr-warsaw-01
- DIST-Rtr-Prague-01
- iNET-rtR-berlin-01
結果:
Name-normalized device list:
core-rtr-warsaw-01
dist-rtr-prague-01
Inet-rtr-berlin-01
就我個人而言,我發現它對於跨大量物件檢索屬性及其值最有用。這裡我們只對name
屬性的值感興趣:
模板:
Interfaces found: {{ interfaces | map(attribute='name') | join('\n') }}
資料:
interfaces:
- name: Ethernet1
mode: switched
- name: Ethernet2
mode: switched
- name: Ethernet3
mode: routed
- name: Ethernet4
mode: switched
結果:
Interfaces found:
Ethernet1
Ethernet2
Ethernet3
Ethernet4
拒絕
reject(*args, **kwargs)
- 通過應用 Jinja2 測試並拒絕測試成功的物件來過濾專案序列。如果測試結果為 ,則該專案將從最終列表中刪除true
。
在這裡,我們只想顯示公共 BGP AS 編號。
模板:
Public BGP AS numbers: {% for as_no in as_numbers| reject('gt', 64495) %} {{ as_no }} {% endfor %}
資料:
as_numbers:
- 1794
- 28910
- 65203
- 64981
- 65099
結果:
Public BGP AS numbers:
1794
28910
拒絕屬性
rejectattr(*args, **kwargs)
- 與reject
過濾器相同,但測試應用於物件的選定屬性。
如果您選擇的測試需要引數,請在測試名稱之後提供它們,用逗號分隔。
在此示例中,我們希望通過將 test 應用於“mode”屬性來從列表中刪除“switched”介面。
模板:
Routed interfaces: {% for intf in interfaces | rejectattr('mode', 'eq', 'switched') %} {{ intf.name }} {% endfor %}
資料:
interfaces:
- name: Ethernet1
mode: switched
- name: Ethernet2
mode: switched
- name: Ethernet3
mode: routed
- name: Ethernet4
mode: switched
結果:
Routed interfaces:
Ethernet3
選擇
select(*args, **kwargs)
- 通過僅保留通過 Jinja2 測試的元素來過濾序列。這個過濾器是相反的reject
。您可以使用其中任何一種,具體取決於在給定場景中感覺更自然的情況。
與此類似,reject
還有一個selectattr
過濾器,其工作原理與每個物件的屬性相同,select
但適用於每個物件的屬性。
下面我們要報告在我們的裝置上找到的私有 BGP AS 編號。
模板:
Private BGP AS numbers: {% for as_no in as_numbers| select('gt', 64495) %} {{ as_no }} {% endfor %}
資料:
as_numbers:
- 1794
- 28910
- 65203
- 64981
- 65099
結果:
Private BGP AS numbers:
65203
64981
65099
Tojson
tojson(value, indent=None)
- 以 JSON 格式轉儲資料結構。當需要 JSON 的應用程式使用呈現的模板時很有用。也可以用作pprint
美化變數除錯輸出的替代方法。
模板:
{{ interfaces | tojson(indent=2) }}
資料:
interfaces:
- name: Ethernet1
vlan: 50
- name: Ethernet2
vlan: 50
- name: Ethernet3
vlan: 50
- name: Ethernet4
vlan: 60
結果:
[
{
"name": "Ethernet1",
"vlan": 50
},
{
"name": "Ethernet2",
"vlan": 50
},
{
"name": "Ethernet3",
"vlan": 50
},
{
"name": "Ethernet4",
"vlan": 60
}
]
獨特
unique(value, case_sensitive=False, attribute=None)
- 返回給定集合中唯一值的列表。與過濾器很好地配對,map
用於查詢用於給定屬性的一組值。
在這裡,我們正在查詢我們跨介面使用的訪問 VLAN。
模板:
Access vlans in use: {{ interfaces | map(attribute='vlan') | unique | join(', ') }}
資料:
interfaces:
- name: Ethernet1
vlan: 50
- name: Ethernet2
vlan: 50
- name: Ethernet3
vlan: 50
- name: Ethernet4
vlan: 60
結果:
Access vlans in use: 50, 60
結論
有了這個相當長的示例列表,我們到了教程的這一部分。Jinja2 過濾器可以成為一個非常強大的工具,我希望我的解釋能幫助你看到它們的潛力。
您確實需要記住明智地使用它們,如果它開始看起來笨拙並且感覺不對,請檢視替代方案。看看您是否可以將複雜性移到模板之外,修改您的資料模型,或者如果這不可能,請編寫您自己的過濾器。
這都是我的。一如既往,期待再次見到您,更多 Jinja2 帖子即將釋出!
參考
- Jinja2 內建過濾器,官方文件:https ://jinja.palletsprojects.com/en/2.11.x/templates/#builtin-filters
- Jinja2 自定義過濾器,官方文件:https ://jinja.palletsprojects.com/en/2.11.x/api/#custom-filters
- Ansible 官方文件中提供的所有過濾器:https ://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html
- Salt,官方文件中提供的所有過濾器:https ://docs.saltstack.com/en/latest/topics/jinja/index.html#filters
- 包含這篇文章資源的 GitHub 儲存庫。可在:https ://github.com/progala/ttl255.com/tree/master/jinja2/jinja-tutorial-p4-template-filters