Jinja2 详细使用文档(配合wkhtmltoimage生成html图片)

93 阅读8分钟

Jinja2 详细教程与完整语法文档

Jinja2 是 Python 生态中最流行的模板引擎之一,广泛应用于 FlaskDjangoAnsibleFastAPIMicroDot(单片机物联网框架) 等框架和工具中。

导言

最近在使用Jinja2+wkhtmltoimage,实现通过html生成图片的应用,其中需要Jinja2html中传入变量值。所以这里总结一下Jinja2 使用文档

wkhtmltoimage生成html图片

这里贴一下html生成图片核心代码,需要用户自行安装wkhtmltopdf,并配置好环境变量

image.png

import subprocess
import os
import sys


def generate_image(
    html, output_path, img_width="1400", img_quality="70", img_format="png"
):
    """
    将HTML转换为图片,依赖wkhtmltoimage

    参数:
        html (str): 要转换的HTML内容
        output_path (str): 输出图片路径

    返回:
        str: 成功返回输出路径,失败返回None

    异常:
        会捕获并处理各种可能的错误,打印错误信息
    """
    try:
        # 检查wkhtmltoimage是否可用
        try:
            subprocess.run(
                ["wkhtmltoimage", "--version"],
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
            )
        except FileNotFoundError:
            raise RuntimeError("wkhtmltoimage未安装或未加入系统PATH环境变量")
        except subprocess.CalledProcessError:
            raise RuntimeError("wkhtmltoimage版本检查失败")

        # 检查输出目录是否存在
        output_dir = os.path.dirname(output_path) or "."
        if not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)

        # 准备命令参数
        cmd = [
            "wkhtmltoimage",
            "--format",
            img_format,
            "--width",
            img_width,
            "--quality",
            img_quality,
            "--disable-smart-width",  # 避免内容被截断
            "-",  # 表示从标准输入读取HTML
            output_path,
        ]

        # 执行转换
        process = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            # stdout=subprocess.PIPE,
            # stderr=subprocess.PIPE,
            universal_newlines=False,
        )

        # 写入HTML内容并等待完成
        stdout, stderr = process.communicate(input=html.encode("utf-8"))

        # 检查执行结果
        if process.returncode != 0:
            error_msg = stderr.decode("utf-8") if stderr else "未知错误"
            raise RuntimeError(
                f"图片生成失败 (返回码: {process.returncode}): {error_msg}"
            )

        # 检查输出文件是否生成
        if not os.path.exists(output_path):
            raise RuntimeError("图片文件未生成")

        # 检查文件是否有效
        if os.path.getsize(output_path) == 0:
            os.remove(output_path)
            raise RuntimeError("生成的图片文件为空")
        return output_path

    except Exception as e:
        # 打印错误信息
        print(f"错误: {str(e)}", file=sys.stderr)

        # 尝试清理可能生成的不完整文件
        if os.path.exists(output_path) and os.path.getsize(output_path) == 0:
            try:
                os.remove(output_path)
            except:
                pass

        return None

一、Jinja2快速入门

1.1 安装与基础使用

pip install Jinja2
from jinja2 import Template

# 简单模板渲染
template = Template('Hello, {{ name }}!')
result = template.render(name='World')
print(result)  # 输出: Hello, World!

1.2 核心语法标记

Jinja2 使用三种基本语法标记:

  • 变量输出{{ variable }}
  • 控制语句{% statement %}
  • 注释{# comment #}

二、完整语法详解

2.1 变量与表达式

变量使用双大括号输出,支持 Python 所有数据类型:

<!-- 字符串 -->
<p>{{ username }}</p>

<!-- 字典访问 -->
<p>邮箱: {{ user.email }}</p>

<!-- 列表索引 -->
<p>第一个元素: {{ items[0] }}</p>

<!-- 对象方法调用 -->
<p>时间: {{ datetime.now().strftime('%Y-%m-%d') }}</p>

表达式支持:数学运算、比较运算、逻辑运算、三元表达式等:

{{ 1 + 2 * 3 }}           {# 输出: 7 #}
{{ "hello" if True else "world" }}  {# 输出: hello #}
{{ user.age > 18 }}       {# 输出: True #}

2.2 控制结构

条件语句 (if/elif/else)
{% if score >= 90 %}
    <p class="excellent">优秀</p>
{% elif score >= 60 %}
    <p class="pass">及格</p>
{% else %}
    <p class="fail">不及格</p>
{% endif %}
循环语句 (for)
<ul>
{% for user in users %}
    <li>{{ user.name }} - {{ user.email }}</li>
{% endfor %}
</ul>

循环控制变量loop对象提供循环状态信息

{% for item in items %}
    <p>
        当前索引: {{ loop.index }} (从1开始),
        当前索引0: {{ loop.index0 }} (从0开始),
        是否第一次: {{ loop.first }},
        是否最后一次: {{ loop.last }},
        剩余次数: {{ loop.revindex }}
    </p>
{% endfor %}
循环控制语句
{% for item in items %}
    {% if loop.index > 10 %}
        {% break %}  {# 跳出循环 #}
    {% endif %}
    
    {% if item.hidden %}
        {% continue %}  {# 跳过当前项 #}
    {% endif %}
    
    {{ item.name }}
{% endfor %}

2.3 过滤器 (Filters)

过滤器通过管道符 |应用,支持链式调用:

{{ "hello world" | upper }}          {# 转大写: HELLO WORLD #}
{{ "  hello  " | trim }}             {# 去空格: hello #}
{{ 3.14159 | round(2) }}             {# 四舍五入: 3.14 #}
{{ items | length }}                 {# 列表长度 #}
{{ html_content | safe }}            {# 禁用HTML转义 #}
{{ text | truncate(50) }}           {# 截断文本 #}
{{ list | join(', ') }}              {# 列表转字符串 #}
{{ value | default('N/A') }}         {# 默认值 #}

链式调用示例

{{ "  hello world  " | trim | upper | truncate(5) }}
{# 输出: HELLO #}

2.4 测试器 (Tests)

测试器用于条件判断,语法为 is

{% if number is even %}
    偶数
{% endif %}

{% if user is defined %}
    用户已定义
{% endif %}

{% if value is none %}
    值为空
{% endif %}

2.5 宏 (Macros)

宏是可重用的模板片段,类似函数:

{# 定义宏 #}
{% macro input_field(name, value='', type='text', class='') %}
    <input type="{{ type }}" name="{{ name }}" 
           value="{{ value }}" class="{{ class }}">
{% endmacro %}

{# 使用宏 #}
{{ input_field('username') }}
{{ input_field('password', type='password', class='form-control') }}

带默认参数的宏

{% macro card(title, content, color='primary') %}
    <div class="card card-{{ color }}">
        <h3>{{ title }}</h3>
        <p>{{ content }}</p>
    </div>
{% endmacro %}

2.6 模板继承

基础模板 (base.html)
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}默认标题{% endblock %}</title>
    {% block head %}{% endblock %}
</head>
<body>
    <header>{% block header %}导航栏{% endblock %}</header>
    
    <main>
        {% block content %}
            <p>默认内容</p>
        {% endblock %}
    </main>
    
    <footer>{% block footer %}页脚{% endblock %}</footer>
</body>
</html>
子模板继承
{% extends "base.html" %}

{% block title %}子页面标题{% endblock %}

{% block head %}
    {{ super() }}  {# 保留父模板内容 #}
    <style>
        .custom-style { color: red; }
    </style>
{% endblock %}

{% block content %}
    <h1>子页面内容</h1>
    <p>这是子模板的内容</p>
{% endblock %}

继承要点

  • extends必须是模板的第一个标签
  • block定义可覆盖的区域
  • super()调用父模板的块内容
  • 未覆盖的块使用父模板默认内容

2.7 模板包含

{# 包含其他模板文件 #}
{% include "header.html" %}

<main>主内容</main>

{% include "footer.html" %}

包含时可以传递变量:

{% include "user_card.html" with user=current_user %}

2.8 变量设置

在模板中定义变量:

{% set title = "页面标题" %}
{% set items = [1, 2, 3] %}

{{ title }}  {# 输出: 页面标题 #}

2.9 空白控制

控制模板渲染后的空白字符:

{# 删除前导空白 #}
{% for item in items -%}
    {{ item }}
{%- endfor %}

{# 输出: item1item2item3 #}

2.10 注释

注释不会出现在渲染结果中:

{# 这是单行注释 #}

{#
    这是多行注释
    不会输出到结果
#}

三、API 与高级用法

3.1 Environment 环境配置

from jinja2 import Environment, FileSystemLoader

# 创建环境
env = Environment(
    loader=FileSystemLoader('templates'),  # 模板加载路径
    autoescape=True,                      # 自动HTML转义
    trim_blocks=True,                     # 删除块后换行
    lstrip_blocks=True,                   # 删除块前空格
    undefined=StrictUndefined            # 严格未定义变量处理
)

# 加载模板
template = env.get_template('index.html')

# 渲染
result = template.render(title='首页', users=user_list)

3.2 自定义过滤器

from jinja2 import Environment

def reverse_string(value):
    """反转字符串"""
    return value[::-1]

def format_currency(amount):
    """格式化货币"""
    return f"¥{amount:.2f}"

# 注册过滤器
env = Environment()
env.filters['reverse'] = reverse_string
env.filters['currency'] = format_currency

# 使用
template = env.from_string('{{ text|reverse }} {{ price|currency }}')
result = template.render(text='hello', price=99.99)
print(result)  # 输出: olleh ¥99.99

3.3 自定义测试器

def is_even(value):
    """判断是否为偶数"""
    return value % 2 == 0

def is_positive(value):
    """判断是否为正数"""
    return value > 0

env.tests['even'] = is_even
env.tests['positive'] = is_positive

# 模板中使用
# {% if number is even %}...{% endif %}

3.4 全局函数

def get_current_time():
    """获取当前时间"""
    from datetime import datetime
    return datetime.now().strftime('%Y-%m-%d %H:%M:%S')

env.globals['current_time'] = get_current_time

# 模板中直接调用
# {{ current_time() }}

四、内置过滤器列表(常用)

Jinja2 常用内置过滤器列表

过滤器说明示例代码输出结果
abs绝对值{{ -5 | abs }}5
capitalize首字母大写{{ "hello" | capitalize }}Hello
default设置默认值{{ value | default("N/A") }}如果value未定义则显示"N/A"
escapeHTML转义{{ "<script>" | escape }}<script>
first获取第一个元素{{ [1,2,3] | first }}1
float转为浮点数{{ "3.14" | float }}3.14
int转为整数{{ "42" | int }}42
join列表连接为字符串{{ [1,2,3] | join(',') }}"1,2,3"
last获取最后一个元素{{ [1,2,3] | last }}3
length获取长度{{ "hello" | length }}5
lower转小写{{ "HELLO" | lower }}"hello"
map映射列表{{ users | map(attribute='name') }}获取所有用户的名称列表
random随机选择元素{{ [1,2,3] | random }}随机返回1,2或3
replace字符串替换{{ "hello" | replace('h', 'j') }}"jello"
reverse反转{{ "hello" | reverse }}"olleh"
round四舍五入{{ 3.14159 | round(2) }}3.14
safe标记为安全HTML{{ html_content | safe }}不转义HTML
slice切片操作{{ [1,2,3,4] | slice(1,3) }}[2,3]
sort排序{{ [3,1,2] | sort }}[1,2,3]
striptags去除HTML标签{{ "<b>hello</b>" | striptags }}"hello"
sum求和{{ [1,2,3] | sum }}6
title每个单词首字母大写{{ "hello world" | title }}"Hello World"
trim去除首尾空格{{ " hello " | trim }}"hello"
truncate截断文本{{ text | truncate(20) }}截断为20字符
unique去重{{ [1,2,2,3] | unique }}[1,2,3]
upper转大写{{ "hello" | upper }}"HELLO"
urlencodeURL编码{{ "hello world" | urlencode }}"hello%20world"

五、内置测试器列表

测试器说明示例
defined是否已定义{% if var is defined %}
divisibleby是否可被整除{% if 10 is divisibleby(5) %}
even是否为偶数{% if num is even %}
iterable是否可迭代{% if obj is iterable %}
lower是否小写{% if str is lower %}
mapping是否为映射类型{% if obj is mapping %}
none是否为None{% if val is none %}
number是否为数字{% if val is number %}
odd是否为奇数{% if num is odd %}
sameas是否相同对象{% if obj1 is sameas(obj2) %}
sequence是否为序列{% if obj is sequence %}
string是否为字符串{% if val is string %}
undefined是否未定义{% if var is undefined %}
upper是否大写{% if str is upper %}

六、实战示例

6.1 用户列表页面

Python 代码

from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template('user_list.html')

users = [
    {'name': '张三', 'age': 25, 'email': 'zhangsan@example.com'},
    {'name': '李四', 'age': 30, 'email': 'lisi@example.com'},
    {'name': '王五', 'age': 28, 'email': 'wangwu@example.com'}
]

result = template.render(users=users, title='用户列表')
print(result)

模板文件 (user_list.html)

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
    <style>
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        tr:nth-child(even) { background-color: #f9f9f9; }
    </style>
</head>
<body>
    <h1>{{ title }}</h1>
    
    <table>
        <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>年龄</th>
            <th>邮箱</th>
        </tr>
        
        {% for user in users %}
        <tr>
            <td>{{ loop.index }}</td>
            <td>{{ user.name }}</td>
            <td>{{ user.age }}</td>
            <td>{{ user.email }}</td>
        </tr>
        {% else %}
        <tr>
            <td colspan="4">暂无用户数据</td>
        </tr>
        {% endfor %}
    </table>
    
    <p>共 {{ users|length }} 条记录</p>
</body>
</html>

6.2 配置文件生成

数据文件 (config.yaml)

servers:
  - name: web-server
    ip: 192.168.1.10
    port: 80
    services: ['nginx', 'php-fpm']
  - name: db-server
    ip: 192.168.1.20
    port: 3306
    services: ['mysql']

模板文件 (server.conf.j2)

# 自动生成的服务器配置
# 生成时间: {{ now() }}

{% for server in servers %}
server {
    server_name {{ server.name }};
    listen {{ server.ip }}:{{ server.port }};
    
    {% for service in server.services %}
    # {{ service }} 服务配置
    {% endfor %}
}
{% endfor %}

生成脚本

import yaml
from jinja2 import Environment, FileSystemLoader
from datetime import datetime

def now():
    return datetime.now().strftime('%Y-%m-%d %H:%M:%S')

# 加载数据
with open('config.yaml', 'r') as f:
    data = yaml.safe_load(f)

# 配置环境
env = Environment(loader=FileSystemLoader('templates'))
env.globals['now'] = now

# 渲染模板
template = env.get_template('server.conf.j2')
config_content = template.render(servers=data['servers'])

# 保存结果
with open('nginx.conf', 'w') as f:
    f.write(config_content)

结尾

挺好用的