這是 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