网络版报告五之python包jinja2

网络版报告实现动态的关键 - python 包 jinja2 。

jinja2

Jinja2是一个基于Python的模板引擎,用于生成HTML或其他格式的文本文件。它能够让开发者将数据动态插入到静态模板文件中,从而生成动态Web页面。

安装

使用 pip 安装

1
pip install Jinja2 

或者 conda 安装

1
conda install -c conda-forge jinja2

基础用法

Jinja2 就是一个“填空工具”。

它允许你在 HTML 里挖坑,然后 Python 往里填土(数据)。

1️⃣ 准备一个模板(template/template.html

注意看 {{ project_name }} ,这就是坑。

1
2
3
4
5
6
<html>
<body>
<h1>{{ project_name }}</h1>
<p>负责人:{{ pi_name }}</p>
</body>
</html>

2️⃣ 准备 Python 数据(run.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from jinja2 import Environment, FileSystemLoader

# 1) 创建环境,告诉 Jinja2 去哪个文件夹找模板
env = Environment(loader=FileSystemLoader('template'))

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

# 3) 准备数据(字典格式)
data = {
"project_name": "小麦GWAS分析",
"pi_name": "张三"
}

# 4) 渲染 → 得到最终 HTML 字符串
# 把 data(字典)里的数据,塞进模板里对应的占位符中,填充完成后,把整份结果作为一个「字符串」交还给你,存进 html_content。
html_content = template.render(data)

# 5) 将最终 HTML 字符串写入文件
with open("report.html", "w", encoding="utf-8") as f:
f.write(html_content)

运行 python run.py,生成 report.html

1
2
<h1>小麦GWAS分析</h1>
<p>负责人:张三</p>

这就是 Jinja2 的全部核心:挖坑 -> 填土。

过滤器

过滤器就是管道处理:把左边的变量"喂"进去,经过函数处理,吐出一个新值。不改变原变量,只影响输出。

语法核心是管道符 |,像 Linux 的管道一样直观:

1
{{ 变量 | 过滤器 }}

可以链式串联,上一个的输出变成下一个的输入:

1
2
{{ name | striptags | trim | upper }}
<!-- 等价于 upper(trim(striptags(name))) -->

常用的字符串过滤器

1
2
3
4
5
6
7
8
9
10
11
{{ "hello" | upper }}         →  "HELLO"
{{ "HELLO" | lower }} → "hello"
{{ "hello world" | capitalize }} → "Hello world" (首字母大写,其余小写)
{{ "hello world" | title }} → "Hello World" (每个单词首字母大写)
{{ " hi " | trim }} → "hi" (去首尾空白)
{{ "hello" | reverse }} → "olleh"
{{ "hello" | length }} → 5

{{ "a<b>c" | e }} 或 {{ "a<b>c" | escape }} → 转义 HTML 尖括号
{{ html_content | safe }} → 标记为"安全",不转义(慎用!只用于可信 HTML)
{{ "<b>hi</b>" | striptags }} → "hi" (剥掉所有 HTML 标签)

常用的数值过滤器

1
2
3
4
5
6
{{ -5 | abs }}                 →  5
{{ "42" | int }} → 42
{{ "3.14" | float }} → 3.14
{{ 3.14159 | round(2) }} → 3.14
{{ 3.14159 | round(2, 'ceil') }} → 3.15 (向上取整)
{{ 3.14159 | round(2, 'floor') }} → 3.14 (向下取整)

也可以用来设定默认值

1
2
3
{{ username | default('匿名用户') }}
{{ bio | default('这家伙很懒,什么都没写', true) }}
<!-- 第二个参数 boolean=true 时,把 '' / none / false 也都视为"空",触发默认值 -->

不过不应该依赖这个过滤器来处理数据,这个只是用来补位。

凡是能在 Python 里干净处理的数据转换,就应该放在 Python 里。

所有变量都会转为字符串

这里要强调一点,HTML 是一种标记语言(Markup),不是编程语言。你写在 HTML 里的一切——标签名、属性值、文本内容——本质上都是字符序列。因此所有准备的python数据在 Jinja2 调用过程中都会把它变成字符串嵌入到 HTML 文本流里(调用 str() 函数)。甚至于逻辑值 True 会转为 “True”。

举个例子

1
2
3
4
data = {
"scores": [98, 87, 76],
"info": {"name": "小明", "vip": True}
}
1
2
3
4
5
6
7
8
<!-- 这里 scores 就是 Python 的 list,info 就是 Python 的 dict -->
<p>第一名:{{ data.scores[0] }}</p> {# 98 → 输出 "98" #}
<p>名字:{{ data.info.name }}</p> {# "小明" #}
<p>是否VIP:{{ data.info.vip }}</p> {# True → 输出 "True" #}

{% for s in data.scores %}
<li>{{ s }}</li>
{% endfor %}

变量可以放进属性值中

Jinja2 变量不仅能放进 HTML 文本内容里(文本节点),也完全可以放进属性值里,比如:

1
2
3
4
5
<p class="{{ cls }}">{{ text }}</p>

<input type="text" value="{{ username }}">

<img src="{{ avatar_url }}" alt="{{ alt_text }}">

渲染时发生在服务器端:Jinja2 先把 {{ … }} 替换为字符串值(并做 HTML 转义),生成一个纯 HTML 文本发给浏览器。浏览器收到后正常解析标签和属性。

缺少的变量会在HTML中显示为空字符串

如果传入的字典数据少了一个或几个 key ,那么在HTML中相应位置会显示为空字符串(""),并且不会报错。

为什么会这样?这是因为 Jinja2 的 context 本质上是个查表过程:context["score"]→ 找不到 → 不抛 KeyError,而是返回一个 Undefined对象

而这个默认的 Undefined类被设计为

操作 行为
str(undefined)/ {{ undefined }} 打印输出 ✅ 返回 空字符串 “”(静默,不报错)
not undefined True(所以 {% if missing % } 走进 else分支)
for x in undefined 可迭代(零次,静默跳过)
undefined + 1undefined.some_attr等其它操作 ❌ 抛 UndefinedError

也就是说:只是孤单地 {{ score }} 打印它 → 空白;但如果后续代码拿它做别的计算又不防备 → 可能炸。

下面有三种应对方式:

方案1|default()过滤器(最常用,推荐)

1
2
<p>分数:{{ score | default('未录入') }}</p>
<p>分数:{{ score | default('') }}</p> {# 保留原空白行为,但语义更明确 #}

方案 2:用 is defined判断

1
2
3
4
5
{% if score is defined %}
<p>分数:{{ score }}</p>
{% else %}
<p>分数:未录入</p>
{% endif %}

方案3:开严格模式(开发阶段强烈建议)

1
2
3
4
5
6
from jinja2 import Environment, StrictUndefined

env = Environment(
loader=FileSystemLoader("templates"),
undefined=StrictUndefined # ← 缺变量直接抛 UndefinedError,不让你蒙混过关
)

这样 {{ score }} 且 score 不在 data 里 → 立刻报错,逼你在 Python 侧把数据准备完整:

1
jinja2.exceptions.UndefinedError: 'score' is undefined

这在团队协作/正式项目里是最安全的做法——空白往往是 bug 的遮羞布,报错才是早发现早治疗。

for循环(画表格的神器)

你的 GWAS 报告里有很多性状,一个个写会累死。用 {% for %}

Python 数据

1
2
3
4
5
6
data = {
"traits": [
{"name": "株高", "snp": 120},
{"name": "产量", "snp": 98}
]
}

这里内容部分是一个“字典列表”,可以在pandas 里把 DataFrame 转成"字典列表" ,方法是 df1.to_dict('records')

HTML 模板(这里 item.namejinja2 的写法,也可以写成 item["name"] ,见下)。

1
2
3
4
5
6
7
8
9
10
11
12
<table border="1">
<tr>
<th>性状</th>
<th>SNP数</th>
</tr>
{% for item in traits %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.snp }}</td>
</tr>
{% endfor %}
</table>

渲染结果:自动生成两行表格。

这里 for 循环主体里面有 HTML 的标签,如 <tr> 等,看上去有些困惑。其实不用想那么多,所有的for 循环主体中的内容都是jinja2当作字符串处理,具体解释如下。

{% for %}不管里面写的是什么——不管是纯文字、{{ }}、还是 <tr><td>这种 HTML 标签源码——它只认一个规则:从 { % for %}{% endfor %}之间的一切,都是"模板体",每次迭代就原样展开一次。

Jinja2 工作在"文本/源码"层面,它根本不关心哪些是 HTML 标签、哪些是内容——它只认三样东西:

  • { % for … % } { % endfor % } → 控制结构
  • {{ … }} → 表达式,求值后替换成文本
  • 其余所有字符 → 原样保留,原样输出

df1.to_dict('records')

df1.to_dict('records') = 把"表格的每一行"变成一个字典,再把所有行拼成一个列表。

最终结果长这样👇

1
2
3
4
5
[
{'id': 1, 'name': '小明', 'score': 92},
{'id': 2, 'name': '小红', 'score': 88},
{'id': 3, 'name': '小刚', 'score': 105}
]

每一行变成一个字典,键值对是一个字段的名称和值。

为什么要用 records 这个参数?

因为 to_dict() 有很多种"形状",records 只是其中一种。

写法 结果长什么样 适合干嘛
df.to_dict('records') [{列:值}, …] 最适合 Flask / Jinja2 / 前端
df.to_dict() {列: {行:值}} ❌ 模板里很难用
df.to_dict('list') {列: [值列表]} ❌ 不适合逐行展示

而在 jinja2 中只能使用 records 模式。

有缺失值怎么办?

如果 df1 里有 NaN(空值)

1
{'fenxi': nan}

Jinja2 会直接显示成:

1
nan

✅ 推荐你在转之前清理一下:

1
2
df1 = df1.fillna('')
dup_info = df1.to_dict('records')

这样模板里就永远是空字符串,而不是 nan

数据框内容为空会怎么样?

df1.to_dict('records')在空数据框时,会返回一个空列表 []。包括有列标题但是没有数据,完全没有列的情况均会返回一个空列表。

两种访问变量属性语法比较

两种写法的查找顺序

foo.bar 的查找顺序(这里的 bar 只会视为字符串 "bar"):

  1. 先查 属性getattr(foo, 'bar') → foo 上有没有叫 bar 的属性?
  2. 没有 → 再查 foo.__getitem__('bar') → foo 里有没有键 'bar'
  3. 还没有 → 返回 undefined

foo['bar'] 的查找顺序(顺序相反,加引号'bar'是字符串,不加引号 bar就是变量):

  1. 先查 foo.__getitem__('bar')
  2. 没有 → 再查 属性getattr(foo, 'bar')
  3. 还没有 → 返回 undefined

两种写法比较和举例

写法 读法 适用场景
s.id / s.name dot notation key 是合法标识符(字母数字下划线、不以数字开头、无空格),最简洁好看 ✅
s[“id”] / s[“full name”] subscript / bracket key 含空格、横杠、中文(虽然中文也能点,但 []更稳),或 key 是动态的

举例如下

1
2
3
4
5
6
7
8
9
10
{% set item = {
"id": 1,
"product name": "iPhone 16", ← 空格!点号搞不定
"price": 5999
} %}

{{ item.id }} ✅ "iPhone 16" 里没空格,可以
{{ item.price }} ✅ 5999
{{ item["product name"] }} ✅ 必须中括号,因为点号认不了空格
{{ item.product name }} ❌ 语法层面就崩了(Jinja2 把 `name` 当独立 token 了)

如果 key 是动态的(存在另一个变量里),也必须使用 s[id] 写法(不加引号,表示变量):

1
2
3
{% set key = "price" %}
{{ item[key] }} ✅ 1000 用变量当 key
{{ item.key }} ❌ 这会去找字面量键 "key",不是 price

for - else 循环

这里和 python 的 for...else 语句不同,jinja2 中的 else 本身是一个空列表占位器。

Python 的 for…else **Jinja2 的 { % for % } { % else % } **
else 何时触发 循环正常跑完、没碰到 break 循环一次都没迭代(序列空 / 过滤后啥也没剩)
break 存在吗 ❌ Jinja2 没有 break / continue
else 的语义 “没被打断” “空列表兜底”(empty fallback)

基本语法骨架为

1
2
3
4
5
{% for item in sequence %}
...每次迭代渲染的内容...
{% else %}
...序列为空/没迭代任何一项时,才渲染这里...
{% endfor %}

三种会触发 { % else % } 的情况

① 列表本身就是空的

1
2
3
4
5
{% for u in users %}
<li>{{ u.name }}</li>
{% else %}
<li>(暂无用户)</li>
{% endfor %}
1
# users = []  →  else 分支渲染

② 列表本身不空,但 if 过滤把所有人踢掉了

1
2
3
4
5
{% for u in users if u.score >= 60 %}
<li>{{ u.name }} 及格</li>
{% else %}
<li>(全班全挂了,一个及格的都没有)</li>
{% endfor %}
1
# 假设所有人都 score < 60 → 过滤后有效序列长度为 0 → else 触发

③ 变量干脆没传 / 是 Undefined(取决于你的 undefined 配置)

1
2
3
4
5
{% for u in users %}
...
{% else %}
暂无数据
{% endfor %}

默认配置下,如果 users 压根没传进 context,Jinja2 会把它当一个 Undefined 对象——迭代它 = 零次,所以也会掉进 else(或报错,看你开了 StrictUndefined 没)。这也是为什么我之前建议你 Python 侧兜底成 data["users"] = users or []

for-else 示例

python示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from jinja2 import Environment, FileSystemLoader

env = Environment(
loader=FileSystemLoader("templates"),
keep_trailing_newline=True
)

template = env.get_template("table.html")

students = [
{"id": 1, "name": "小明", "score": 92, "vip": True},
{"id": 2, "name": "小红", "score": 88, "vip": False},
{"id": 3, "name": "小刚", "score": 105, "vip": True},
]

html = template.render(students=students)
print(html)

模板如下(这里的 loop.indexfor 循环的内部变量,应该是迭代次数,从1开始;但是这里逻辑错了,应该是使用 s.id 才对,这才是第一列)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>成绩表</title>
<style>
table { border-collapse: collapse; width: 420px; }
th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; }
th { background: #f3f3f3; }
.vip { color: #d00; font-weight: bold; }
</style>
</head>
<body>

<table>
<thead>
<tr>
<th>#</th>
<th>姓名</th>
<th>分数</th>
<th>状态</th>
</tr>
</thead>

<tbody>
{% for s in students %}
<tr>
<td>{{ loop.index }}</td> <!-- 序号(从1开始) -->
<td>{{ s.name }}</td>
<td>{{ s.score }}</td>
<td>
{% if s.vip %}
<span class="vip">VIP</span>
{% else %}
普通
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="4">暂无数据</td></tr>
{% endfor %}
</tbody>
</table>

</body>
</html>

⚠️ 注意 colspan=“4”:因为表头有 4 列,合并占位才不会表格缺格变形。

if 判断(有没有图)

有时候有图,有时候没图,用 { % if % }

Python 数据

1
2
3
data = {
"has_image": True
}

HTML 模板

1
2
3
4
5
{% if has_image %}
<p>✅ 图片已生成</p>
{% else %}
<p>❌ 没有图片</p>
{% endif %}

总结

符号 叫法 干啥用
{{ }} 变量 填文字、数字
{ % % } 逻辑 循环(for)、判断(if)
{ # # } 注释 给自己看的笔记(不会出现在最终的HTML中)

其他网络版报告的注意事项

5个特殊字符需要转义

字符 是否需要转义 转义后
< 必须(文本中) &lt;
> 必须(文本中) &gt;
& 必须 &amp;
" 仅在双引号属性值内必须转义 &quot;
' 仅在单引号属性值内必须转义 &apos;&#39;
其他所有字符(包括汉字、字母、数字、标点、空格、换行、Emoji等) 不需要 直接打

最佳实践:始终在文本内容中使用 <>& 的转义形式(前2个是元素的构成部分,&是转义的开始字符),而引号只在属性值中注意转义。其他字符放心直接输入。

参考文献

1.https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Core/Structuring_content

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2026 Vincere Zhou
  • 访问人数: | 浏览次数:

请我喝杯茶吧~

支付宝
微信