Python / jinja2
===
###### tags: `Python`
###### tags: `Python`, `jinja2`, `template`, `render`, `filter`, `upper`, `lower`, `trim`, `replace`
<br>
[TOC]
<br>
## 應用範例
### 客製化 K8s yaml
對於需求更複雜,或想保有更多模板功能(例如條件判斷、循環處理等),可以考慮使用 Jinja2 模板引擎。這樣可以利用 Jinja2 的豐富功能,應對更動態的模板需求。
```python=
from jinja2 import Template
import yaml
# 定義含有 Jinja2 變數的 YAML 模板
jinja_template = """
apiVersion: example.com/v1
kind: Book
metadata:
name: {{name}}
namespace: {{namespace}}
spec:
title: "The Bad Book (from client)"
author: "John Doe"
publicationDate: "2021-01-01"
edition: 1
"""
# 建立模板並渲染
template = Template(jinja_template)
rendered_yaml = template.render(name="great-book", namespace="default")
# 解析渲染完成的 YAML
data = yaml.safe_load(rendered_yaml)
print(data)
```
- 概念類似 Python 字串的格式化
```python
# placeholder 要使用 {name} & {namespace}
rendered_yaml = yaml_template.format(
name="great-book", namespace="default")
```
<br>
## 討論
### Jinja 語法長相
- ### Jinja 範例
- [Llama3 chat template and eos_token #104](https://github.com/meta-llama/llama3/issues/104)
- [Mistral-7B-Instruct-v0.2 loopy text generation with custom chat template](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.2/discussions/68)
- ### Question
`jinja2
"{% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '' + message['role'] + '\n\n'+ message['content'] | trim + '' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}`
這是什麼 script ? 好像是 python 語法?
- ### ChatGPT4
這段代碼不是標準的 Python 語法,而是使用了 Jinja2 模板語言的一個例子。Jinja2 是一個流行的模板引擎,通常用於生成 HTML 文件,但也可以用於任何文本文件的動態生成。這段代碼的用途是將一系列消息格式化為特定格式的文本。
讓我來解釋一下這段代碼的各個部分:
1. **`{% set loop_messages = messages %}`**:這行代碼創建一個名為 `loop_messages` 的變量,並將其設置為 `messages` 的值。這裡的 `messages` 可能是包含多個消息的列表。
2. **`{% for message in loop_messages %}`**:開始一個循環,對 `loop_messages` 列表中的每個 `message` 進行迭代。
3. **`{% set content = '' + message['role'] + '\n\n'+ message['content'] | trim + '' %}`**:為每個消息創建一個名為 `content` 的變量,並將它設置為消息的角色(例如發送者或接收者),後面跟上兩個換行符和經過剪裁(去除首尾空白)的消息內容。
4. **`{% if loop.index0 == 0 %}`**:這是一個條件語句,檢查當前消息是否是列表中的第一個消息。
5. **`{% set content = bos_token + content %}`**:如果當前消息是列表中的第一個,則在 `content` 前面加上一個名為 `bos_token` 的開始標記。這可能用於標記消息序列的開始。
6. **`{{ content }}`**:將 `content` 變量的值輸出到模板的最終結果中。
7. **`{% endfor %}`**:結束 `for` 循環。
整體而言,這段代碼的目的是從一個消息列表中構造一個格式化的文本串,其中每個消息的角色和內容被格式化和分隔,第一個消息前還可能有一個特定的標記(`bos_token`)。這樣的腳本可能用於聊天應用或任何需要格式化顯示多條消息的情境。
<br>
### 使用 Jinga2 理由
- ### Question
為何不使用 python 腳本生成就好,非要引入另一種腳本 Jinga2 ?
- ### ChatGPT4
使用 Jinja2 而非直接用 Python 腳本進行文本生成的主要原因在於模板語言如 Jinja2 在設計上更適合於處理文本內容和格式化問題。以下是一些選擇使用 Jinja2 的優勢:
1. 分離邏輯和表示:Jinja2 允許開發者將業務邏輯(例如,處理數據的 Python 程序)與表示邏輯(如何展示這些數據)分開。這使得代碼更易於維護,也更容易由不同的團隊成員(例如開發人員和設計師)協作。
2. 重用和模塊化:使用模板語言,你可以輕鬆創建可重用的模板或模板片段,這些模板可以在多個地方使用而無需重複相同的代碼。這有助於保持代碼的乾淨和組織。
3. 強大的文本處理功能:Jinja2 專為文本生成設計,提供了許多有用的內置功能,如自動轉義(對於防止 HTML 注入尤其重要)、過濾器和測試,這些都是為了更方便地處理和格式化文本。
4. 靈活性:Jinja2 支持條件語句、迴圈、變量替換和繼承等功能,這些都是在動態生成文本時非常有用的特性。這使得它在動態生成 HTML、配置文件或任何其他結構化文本時非常靈活。
5. 易於整合:Jinja2 可以很容易地與各種 Python 框架(如 Flask 和 Django)整合,這使得它在 web 開發中特別有用。这种整合允许开发者在应用程序的不同部分中统一使用相同的模板技术。
總之,雖然 Python 本身可以處理字符串操作和文本生成,但 Jinja2 提供了一種更專業、更結構化的方式來處理這些任務,特別是當涉及到複雜的文本格式化或需要將表示層和邏輯層分離時。
<br>
### 常見的 Jinga2 語法
- ### Question
條件語句、迴圈、變量替換和繼承的 Jinja2 語法為何?
- ### ChatGPT4
Jinja2 模板引擎提供了豐富的語法功能,使得動態生成文本變得非常方便。下面是關於條件語句、迴圈、變量替換和繼承的一些基本用法:
1. **條件語句**
Jinja2 的條件語句非常類似於 Python 的 `if-elif-else` 結構,允許你根據條件執行不同的代碼塊。語法如下:
```jinja=
{% if user.is_authenticated %}
Hello, {{ user.name }}!
{% else %}
Please log in.
{% endif %}
```
這裡,`if` 和 `else` 標籤用於檢查條件,並根據條件的真假顯示不同的信息。
2. **迴圈**
迴圈語句允許對列表或字典進行迭代。Jinja2 主要使用 `{% for %}` 標籤來執行迴圈:
```jinja=
<ul>
{% for user in users %}
<li>{{ user.name }} - {{ user.email }}</li>
{% endfor %}
</ul>
```
這個例子將為每個用戶生成一個列表項目,顯示他們的名字和電子郵件地址。
3. **變量替換**
變量替換是 Jinja2 中最基本的功能,允許你將 Python 變量的值插入到文本中。變量使用兩個大括號 `{{ }}` 包圍:
```jinja
Hello, {{ name }}!
```
如果 `name` 的值是 "Alice",則上述模板將輸出 "Hello, Alice!"。
4. **繼承**
Jinja2 支持模板繼承,這使得你可以基於一個基本模板創建新的模板。基本模板定義了一個通用的結構,而其他模板可以繼承並覆蓋某些部分。
基本模板(base.html):
```jinja=
<html>
<head>
<title>{% block title %}My Website{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>
```
子模板:
```jinja=
{% extends "base.html" %}
{% block title %}Welcome to My Website{% endblock %}
{% block content %}
<p>This is an example page.</p>
{% endblock %}
```
在這個例子中,子模板繼承了基本模板並覆蓋了 `title` 和 `content` 塊,從而定制了頁面標題和內容。
這些功能使 Jinja2 成為一個非常強大且靈活的模板引擎,適用於各種文本生成和網頁渲染任務。
<br>
### 其他 Jinja2 語法
- ### Question
還有其他語法嗎?
- ### ChatGPT4
Jinja2 包含許多有用的語法特性和功能,除了基本的變量替換、條件語句、迴圈和繼承之外,還包括過濾器、測試器、宏和包含等。以下是一些進階功能的說明:
1. **過濾器**
過濾器用於對變量進行轉換,它們通過管道符號 | 應用於表達式。Jinja2 提供了許多內置過濾器,如 upper、lower、trim、replace 等:
```jinja=
{{ "hello" | capitalize }} // 輸出 "Hello"
{{ "HELLO" | lower }} // 輸出 "hello"
{{ 42 | string }} // 輸出 "42"
{{ " hello " | trim }} // 輸出 "hello"
```
2. **測試器**
測試器用於檢查變量的特定條件,例如檢查是否為空、是否包含特定元素等。它們通常在 `if` 語句中使用:
```jinja=
{% if "hello" is string %}
This is a string.
{% endif %}
```
3. **宏**
宏類似於函數,允許你定義可重用的模板片段。它們可以帶參數並在模板中多次呼叫:
```jinja=
{% macro input(name, value='', type='text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{{ input('username') }}
{{ input('email', type='email') }}
```
4. **包含**
包含允許你將一個模板的內容插入到另一個模板中。這對於組織大型模板或重用共同元素很有用:
```jinja=
{% include 'header.html' %}
Body content here.
{% include 'footer.html' %}
```
5. **設定變量**
你可以在模板中設定變量,用於稍後在同一模板中使用:
```jinja=
{% set user_count = users | length %}
There are {{ user_count }} users.
```
6. **自訂過濾器和測試器**
Jinja2 允許你定義自訂過濾器和測試器,這使得你可以擴展模板引擎以滿足特定的需求。這通常在 Python 代碼中完成,然後將這些自訂功能添加到 Jinja2 環境中。
這些高級功能組合起來,使得 Jinja2 不僅是一個強大的模板引擎,還是一個極具彈性的工具,適用於各種複雜的文本和 HTML 生成任務。
<br>
### 基本範例 for-loop + if-else
- ### Question
給一個基本的 for-loop + if-else 常見範例
- ### ChatGPT4
當然可以!這裡有一個示例,它展示了如何在 Jinja2 模板中使用 `for` 迴圈和 `if-else` 條件語句。在這個例子中,我們將生成一個 HTML 列表,其中包含一些數字,並對數字進行檢查,以確定它們是否是偶數或奇數:
```jinja
<ul>
{% for number in range(1, 6) %}
{% if number % 2 == 0 %}
<li>{{ number }} is even.</li>
{% else %}
<li>{{ number }} is odd.</li>
{% endif %}
{% endfor %}
</ul>
```
在這個範例中,我們做了以下事情:
1. `For Loop`:迴圈從 1 開始,到 6 結束(不包括 6)。迴圈內的每次迭代都會對 number 變量賦予當前迴圈的值。
2. `If-Else Statement`:在迴圈內部,我們使用了一個 `if-else` 條件語句來檢查當前的 `number` 是否是偶數(`number % 2 == 0`)。根據檢查的結果,我們將顯示不同的內容:
- 如果是偶數,則顯示例如 "2 is even."。
- 如果是奇數,則顯示例如 "1 is odd."。
3. `Output`:每個數字和它的狀態(偶數或奇數)將作為列表項目 (`<li>`) 被添加到 HTML 的無序列表 (`<ul>`) 中。
這個範例非常適合用來快速理解如何在 Jinja2 模板中結合使用迴圈和條件語句來處理和顯示資料。这种结合使用是生成动态内容时的常见做法。
<br>
### Jinga 運作方式
- ### Question
我要如何產生實際的結果,而不是只是展示 Jinja2 腳本?
- ### ChatGPT4
要生成實際的結果而不僅僅是展示 Jinja2 腳本,你需要有一個運行環境,能夠處理這些模板並結合實際的數據來渲染最終的輸出。這通常在 Python 程序中使用 Jinja2 模板庫來完成。以下是一個基本的步驟和示例代碼,說明如何在 Python 中使用 Jinja2:
1. **安裝 Jinja2**
首先,你需要安裝 Jinja2。這可以通過 pip 完成:
```bash
pip install jinja2
```
2. **創建 Jinja2 環境和模板**
接下來,你需要在 Python 腳本中創建一個 Jinja2 環境並定義模板。這裡是一個簡單的範例:
```python=
from jinja2 import Environment, FileSystemLoader
# 設置模板文件的目錄
env = Environment(loader=FileSystemLoader('templates'))
# 加載模板
template = env.get_template('my_template.html')
# 模板數據
data = {
'numbers': range(1, 6)
}
# 渲染模板
output = template.render(data)
print(output)
```
3. **創建模板文件**
你還需要在名為 `templates` 的文件夾中創建一個名為 `my_template.html` 的文件,並在其中寫入你的 Jinja2 模板。基於先前的例子,這個文件看起來應該是這樣的:
```jinja=
<ul>
{% for number in numbers %}
{% if number % 2 == 0 %}
<li>{{ number }} is even.</li>
{% else %}
<li>{{ number }} is odd.</li>
{% endif %}
{% endfor %}
</ul>
```
4. **執行 Python 腳本**
最後,執行你的 Python 腳本。它將會加載 Jinja2 模板,使用提供的數據渲染它,並打印出 HTML 輸出。
這個過程會結合你的數據和模板,生成實際的 HTML 輸出,可以用於 Web 頁面或其他需要動態生成內容的場景。這種方法非常適合於開發 Web 應用程序,讓你能夠靈活地設計用戶界面和展示層。
<br>
### ImmutableSandboxedEnvironment 參數
- ### Question
```
# pip install Jinja2
import jinja2
from jinja2.exceptions import TemplateError
from jinja2.sandbox import ImmutableSandboxedEnvironment
env = ImmutableSandboxedEnvironment(trim_blocks=True, lstrip_blocks=True)
```
trim_blocks=True, lstrip_blocks=True 參數用途為何?
- ### ChatGPT4
在 Jinja2 中,`trim_blocks` 和 `lstrip_blocks` 參數用於控制模板中空白字符的處理方式,這對於生成的輸出的格式有重要影響。
1. `trim_blocks=True`:
當設置為 `True` 時,如果塊結尾後面只有換行符,則會自動刪除這個換行符。這對於在模板中編寫控制結構(如 `for` 循環或 `if` 語句)時特別有用,因為它可以防止這些結構在生成的輸出中引入額外的換行符。舉例來說,沒有 `trim_blocks` 的情況下:
```jinja=
{% for item in sequence %}
{{ item }}
{% endfor %}
```
這將會在每個 `item` 的輸出之後產生一個額外的換行符。但是,使用 `trim_blocks=True`:
```jinja=
{% for item in sequence %}
{{ item }}
{% endfor %}
```
在這種情況下,每個 `item` 之後不會有額外的換行符。
2. lstrip_blocks=True:
當設置為 `True` 時,Jinja 將會從塊開始之前的行中刪除所有的空白字符(包括空格和制表符)。這允許你在模板中的塊控制結構前面放置空白,而不會影響輸出的格式。沒有 `lstrip_blocks` 的情況下:
```jinja=
{% if condition %}
True
{% else %}
False
{% endif %}
```
這將保留所有的縮進和空白。但是,使用 `lstrip_blocks=True`:
```jinja=
{% if condition %}
True
{% else %}
False
{% endif %}
```
在這種情況下,輸出中將不會有這些行前面的空白字符。
總結來說,這兩個設置有助於使模板更易於閱讀,同時仍然保持生成的輸出的整潔和預期的格式。
<br>
### Jinga2 縮排形式
- ### Question
`{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}`
改成有縮排形式
- ### ChatGPT4
將您提供的 Jinja 模板代碼進行適當的縮排可以提高可讀性。這裡是重構後的代碼:
```jinja=
{% if messages[0]['role'] == 'system' %}
{% set loop_messages = messages[1:] %}
{% set system_message = messages[0]['content'] %}
{% else %}
{% set loop_messages = messages %}
{% set system_message = false %}
{% endif %}
{% for message in loop_messages %}
{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}
{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}
{% endif %}
{% if loop.index0 == 0 and system_message != false %}
{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}
{% else %}
{% set content = message['content'] %}
{% endif %}
{% if message['role'] == 'user' %}
{{ bos_token + '[INST] ' + content.strip() + ' [/INST]' }}
{% elif message['role'] == 'assistant' %}
{{ ' ' + content.strip() + ' ' + eos_token }}
{% endif %}
{% endfor %}
```
這個代碼的重構使其更易於閱讀和理解。它將代碼分成了幾個部分,每個部分都有自己的縮排,這樣可以更清晰地顯示代碼的結構和控制流程。
<br>
### Jinga2 的 python 形式
- ### Question
`{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = false %}{% endif %}{% for message in loop_messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if loop.index0 == 0 and system_message != false %}{% set content = '<<SYS>>\\n' + system_message + '\\n<</SYS>>\\n\\n' + message['content'] %}{% else %}{% set content = message['content'] %}{% endif %}{% if message['role'] == 'user' %}{{ bos_token + '[INST] ' + content.strip() + ' [/INST]' }}{% elif message['role'] == 'assistant' %}{{ ' ' + content.strip() + ' ' + eos_token }}{% endif %}{% endfor %}`
改成 Python 程式碼
- ### ChatGPT4
要將您提供的 Jinja 模板代碼轉換成 Python 程式碼,我們需要對原始邏輯進行適當的修改,因為 Jinja 模板語言和 Python 在處理數據和流程控制方面有些差異。以下是相應的 Python 程式碼:
```python=
# 假設 messages 是已定義的列表,並且包含字典,字典中至少有 'role' 和 'content' 鍵
# 初始化
loop_messages = []
system_message = False
# 檢查第一條消息是否為系統消息
if messages[0]['role'] == 'system':
loop_messages = messages[1:]
system_message = messages[0]['content']
else:
loop_messages = messages
# 遍歷消息
for index, message in enumerate(loop_messages):
# 檢查對話角色是否交替
if (message['role'] == 'user') != (index % 2 == 0):
raise Exception('Conversation roles must alternate user/assistant/user/assistant/...')
# 設置內容
content = message['content']
if index == 0 and system_message:
content = '<<SYS>>\n' + system_message + '\n<</SYS>>\n\n' + content
# 根據角色添加前綴或後綴
if message['role'] == 'user':
content = bos_token + '[INST] ' + content.strip() + ' [/INST]'
elif message['role'] == 'assistant':
content = ' ' + content.strip() + ' ' + eos_token
# 可以在這裡處理 content,例如打印或追加到某個列表
print(content)
```
在這個代碼中,`messages` 應該是一個字典列表,其中每個字典至少包含 `'role'` 和 `'content'` 鍵。`bos_token` 和 `eos_token` 需要根據您的應用場景提前定義。這段代碼模仿了 Jinja 模板中的邏輯,但是以 Python 的語法和數據處理方式來實現。