用 Python 撸一个 Web 服务器-第4章:动态渲染数据

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 用 Python 撸一个 Web 服务器-第4章:动态渲染数据

上一章中为了尽快让 Todo List 程序跑起来,并没有完全按照 MVC 模式编写程序。这一章就让我们一起实现一个完整的 MVC 模式 Todo List 程序首页。

使用模型操作数据

我们来分析下请求 Todo List 程序首页时,模型层需要做哪些事情。当一个请求到达首页视图函数 index 时,它需要做两件事情,首先调用模型层获取全部的 todo 数据,然后将 todo 数据动态填充到 index.html 模板中。

调用模型层获取全部的 todo 数据,只需要在模型层编写读取 todo/db/todo.json 文件数据的代码即可。在这之前,我们需要先确定 todo 在文件中存储的格式。

Todo List 程序中 todo 需要存储的数据只有一个,就是 todo 的内容。所以我们可以将 todo 以如下格式存储到 todo/db/todo.json 文件:

1
2
3
4
5
6
7
8
9
10
11
12
// todo_list/todo/db/todo.json
[
    {
"id": 1,
"content": "hello world"
    },
    {
"id": 2,
"content": "你好,世界!"
    }
]

这是一个标准的 JSON 格式,每一个对象代表了一条 todo,content 字段即为 todo 内容,id 作为每条数据的索引不会展示在页面中,方便我们对数据进行排序、快速查找等操作。

为了简化程序,我将数据存储在 JSON 文件中而不是数据库中。存储到文件的格式多种多样,但 JSON 格式是一种非常流行且友好的数据格式,在 Python 中也能够很方便的对 JSON 格式的文件进行读写操作。

注意:

  1. JSON 文件不支持注释,所以如果你打算直接从上面示例中复制数据到 todo.json 文件时,需要去掉顶部文件名注释。
  2. 如果 todo/db/todo.json 文件内容为空,使用 Python 读取时会抛出 JSONDecodeError 异常,起码要保证其内部有一个空数组 [] 存在,才能正常读取。

确定了 todo/db/todo.json 文件数据格式,就可以编写在模型层读取 todo 数据的代码了:

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
# todo_list/todo/models.py
import os
import json
from todo.config import BASE_DIR
classTodo(object):
"""
    Todo 模型类
    """
def__init__(self, **kwargs):
        self.id = kwargs.get('id')
        self.content = kwargs.get('content', '')
    @classmethod
def_db_path(cls):
"""获取存储 todo 数据文件的绝对路径"""
# 返回 'todo_list/todo/db/todo.json' 文件的绝对路径
        path = os.path.join(BASE_DIR, 'db/todo.json')
return path
    @classmethod
def_load_db(cls):
"""加载 JSON 文件中所有 todo 数据"""
        path = cls._db_path()
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
    @classmethod
defall(cls, sort=False, reverse=False):
"""获取全部 todo"""
# 这一步用来将所有从 JSON 文件中读取的 todo 数据转换为 Todo 实例化对象,方便后续操作
        todo_list = [cls(**todo_dict) for todo_dict in cls._load_db()]
# 对数据按照 id 进行排序
if sort:
            todo_list = sorted(todo_list, key=lambda x: x.id, reverse=reverse)
return todo_list

定义 Todo 模型类来操作 todo 数据。Todo 模型类的 all 方法用来读取全部的 todo 数据,在其内部将所有从 JSON 文件中读取的 todo 数据转换为 Todo 实例化对象并组装成 list 返回。all 方法还可以对数据进行排序,排序操作实际上转发给了 Python 内置的 sorted 函数来完成。

有了全部的 todo 数据,下一步操作就是将 todo 数据动态填充到 todo/templates/index.html 模板中。

使用模板引擎渲染 HTML

上一章实现的 Todo List 程序返回的首页数据都是固定写死在 todo/templates/index.html 代码中的。现在需要动态填充 todo 内容,我们需要学习一个新的概念叫作 模板渲染

首先我们编写的 HTML 页面不再是完全使用 HTML 的标签来编写,而需要使用一些占位变量来替换需要动态填充的部分,这样编写出来的 HTML 页面通常称为模板。将 HTML 模板读取到内存中,使用真实的 todo 数据来替换掉占位变量而获得最终将要返回的字符串数据,这个过程称为渲染。能够实现读取 HTML 中的占位变量并正确替换为真实值的代码称为模板引擎。

Todo List 程序首页主体部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
<h1class="container">Todo List</h1>
<divclass="container">
<ul>
<li>
<div>Hello World</div>
</li>
<li>
<div>你好,世界!</div>
</li>
</ul>
</div>

其中每一个 li 标签代表一条 todo,显然 todo 的条数是不确定的,所以每一个 li 标签都需要动态生成。根据这段 HTML 代码,可以编写出如下模板:

1
2
3
4
5
6
7
8
9
10
<h1class="container">Todo List</h1>
<divclass="container">
<ul>
        {% for todo in todo_list %}
<li>
<div>{{ todo.content }}</div>
</li>
        {% endfor %}
</ul>
</div>

这段模板代码中只保留了一对 li 标签,它被嵌套在 for 循环中,for 语句块从 结束。todo_list 变量是在模板渲染阶段传进来的由所有 todo 对象组成的 listlist 中有多少个元素就会渲染多少个 li 标签。for 循环内部使用了循环变量 todo 表示获取 todo 变量的 content 属性,这与 Python 中获取对象的属性语法相同。

了解了模板语法,我们还需要有一个能够读懂模板语法的模板引擎。Todo List 程序的 HTML 模板只会用到 for 循环和模板变量这两种语法,所以我们将要实现的模板引擎只需要能够解析这两种语法即可。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# todo_list/todo/utils.py
classTemplate(object):
"""模板引擎"""
def__init__(self, text, context):
# 保存最终结果
        self.result = []
# 保存从 HTML 中解析出来的 for 语句代码片段
        self.for_snippet = []
# 上下文变量
        self.context = context
# 使用正则匹配出所有的 for 语句、模板变量
        self.snippets = re.split('({{.*?}}|{%.*?%})', text, flags=re.DOTALL)
# 标记是否为 for 语句代码段
        is_for_snippet = False
# 遍历所有匹配出来的代码片段
for snippet in self.snippets:
# 解析模板变量
if snippet.startswith('{{'):
if is_for_snippet isFalse:
# 去掉花括号和空格,获取变量名
                    var = snippet[2:-2].strip()
# 获取变量的值
                    snippet = self._get_var_value(var)
# 解析 for 语句
elif snippet.startswith('{%'):
# for 语句开始代码片段 -> {% for todo in todo_list %}
if'in'in snippet:
                    is_for_snippet = True
                    self.result.append('{}')
# for 语句结束代码片段 -> {% endfor %}
else:
                    is_for_snippet = False
                    snippet = ''
if is_for_snippet:
# 如果是 for 语句代码段,需要进行二次处理,暂时保存到 for 语句片段列表中
                self.for_snippet.append(snippet)
else:
# 如果是模板变量,直接将变量值追加到结果列表中
                self.result.append(snippet)
def_get_var_value(self, var):
"""根据变量名获取变量的值"""
# 如果 '.' 不在变量名中,直接在上下文变量中获取变量的值
if'.'notin var:
            value = self.context.get(var)
# '.' 在变量名中(对象.属性),说明是要获取对象的属性
else:
            obj, attr = var.split('.')
            value = getattr(self.context.get(obj), attr)
# 保证返回的变量值为字符串
ifnot isinstance(value, str):
            value = str(value)
return value
def_parse_for_snippet(self):
"""解析 for 语句片段代码"""
# 保存 for 语句片段解析结果
        result = []
if self.for_snippet:
# 解析 for 语句开始代码片段
# '{% for todo in todo_list %}' -> ['for', 'todo', 'in', 'todo_list']
            words = self.for_snippet[0][2:-2].strip().split()
# 从上下文变量中获取 for 语句中的可迭代对象
            iter_obj = self.context.get(words[-1])
# 遍历可迭代对象
for i in iter_obj:
# 遍历 for 语句片段的代码块
for snippet in self.for_snippet[1:]:
# 解析模板变量
if snippet.startswith('{{'):
# 去掉花括号和空格,获取变量名
                        var = snippet[2:-2].strip()
# 如果 '.' 不在变量名中,直接将循环变量 i 赋值给 snippet
if'.'notin var:
                            snippet = i
# '.' 在变量名中(对象.属性),说明是要获取对象的属性
else:
                            obj, attr = var.split('.')
# 将对象的属性值赋值给 snippet
                            snippet = getattr(i, attr)
# 保证变量值为字符串
ifnot isinstance(snippet, str):
                        snippet = str(snippet)
# 将解析出来的循环变量结果追加到 for 语句片段解析结果列表中
                    result.append(snippet)
return result
defrender(self):
"""渲染"""
# 获取 for 语句片段解析结果
        for_result = self._parse_for_snippet()
# 将渲染结果组装成字符串并返回
return''.join(self.result).format(''.join(for_result))
defrender_template(template, **context):
"""渲染模板"""
# 读取 'todo_list/todo/templates' 目录下的 HTML 文件内容
    template_dir = os.path.join(BASE_DIR, 'templates')
    path = os.path.join(template_dir, template)
with open(path, 'r', encoding='utf-8') as f:
# 将从 HTML 中读取的内容传递给模板引擎
        t = Template(f.read(), context)
# 调用模板引擎的渲染方法,实现模板渲染
return t.render()

Template 类就是我们为 Todo List 程序实现的模板引擎。模板引擎的代码有些复杂,我写了比较详细的注释来帮助你理解。模板渲染的大概过程如下:

首先实例化 Template 对象,Template 对象的初始化方法 __init__ 需要传递两个参数,分别是 HTML 字符串和保存了模板所需变量的 dict,在初始化时会解析出 HTML 中所有的 for 语句和模板变量,模板变量直接被替换为对应的值,for 语句代码段则被暂存起来,等到需要真正渲染模板时,调用模板引擎实例对象的 render 方法,完成 for 语句的解析和值替换,最终将渲染结果组装成字符串并返回。

render_template 函数的代码也做了相应的调整,它的功能不再只是读取 HTML 内容,而是需要在内部调用模板引擎获取渲染结果。

对于基础薄弱的读者来说可能模板引擎部分的代码不太好理解,那么暂时先不必深究,你只需要知道模板引擎干了什么,明白它的原理无非是将 HTML 字符串中的模板语法全部找出来,然后根据语法规则将其替换成真正的变量值,最后渲染成正确的 HTML。本质上还是字符串的拼接,就像 Python 字符串的 format 方法一样,它能够找到字符串中的花括号 {},然后替换成传递给它的参数值。

MVC 模式的 Todo List 程序首页

我们已经介绍了使用模型操作数据和使用模板引擎渲染 HTML,现在就可以用动态渲染的 HTML 首页替换之前的静态首页了。

修改首页 todo/templates/index.html 的 HTML 代码为一个模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- todo_list/todo/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<metacharset="UTF-8">
<title>Todo List</title>
</head>
<body>
<h1class="container">Todo List</h1>
<divclass="container">
<ul>
        {% for todo in todo_list %}
<li>
<div>{{ todo.content }}</div>
</li>
        {% endfor %}
</ul>
</div>
</body>
</html>

这里我暂时去掉了 HTML 顶部的 CSS 样式,因为我们的模板引擎不支持这种直接将 CSS 嵌入在 HTML 中的写法,之后我会介绍如何通过 link 标签来引入外部样式。

我们还要对 index 视图函数做些修改,在视图函数内部调用 Todo 模型的 all 方法来获取所有 todo,然后传递给模板引擎对 HTML 进行渲染,得到最终结果。修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# todo_list/todo/controllers.py
from todo.utils import render_template
from todo.models import Todo
defindex():
"""首页视图函数"""
# 倒序排序,最近添加的 todo 排在前面
    todo_list = Todo.all(sort=True, reverse=True)
    context = {
'todo_list': todo_list,
    }
return render_template('index.html', **context)

在终端中进入项目根目录 todo_list/ 下,使用 Python 运行 server.py 文件,将得到经过动态渲染的 Todo List 程序首页:

Todo List 首页

现在 Todo List 程序首页已经是动态渲染的了,下一章我们就来解决样式问题。

本章源码:chapter4

相关文章
|
1月前
|
数据采集 JSON 数据处理
抓取和分析JSON数据:使用Python构建数据处理管道
在大数据时代,电商网站如亚马逊、京东等成为数据采集的重要来源。本文介绍如何使用Python结合代理IP、多线程等技术,高效、隐秘地抓取并处理电商网站的JSON数据。通过爬虫代理服务,模拟真实用户行为,提升抓取效率和稳定性。示例代码展示了如何抓取亚马逊商品信息并进行解析。
抓取和分析JSON数据:使用Python构建数据处理管道
|
16天前
|
数据采集 JavaScript 搜索推荐
服务器端渲染(SSR)(Nuxt+Next.js)
服务器端渲染(SSR)技术在服务器上生成页面HTML,提升首屏加载速度和SEO效果。Nuxt.js和Next.js分别是基于Vue.js和React.js的流行SSR框架。Nuxt.js提供自动化路由管理、页面级数据获取和布局系统,支持SSR和静态站点生成。Next.js支持SSR、静态生成和文件系统路由,通过`getServerSideProps`和`getStaticProps`实现数据获取。SSR的优点包括首屏加载快、SEO友好和适合复杂页面,但也会增加服务器压力、开发限制和调试难度。选择框架时,可根据项目需求和技术栈决定使用Nuxt.js或Next.js。
|
25天前
|
缓存 监控 Linux
Python 实时获取Linux服务器信息
Python 实时获取Linux服务器信息
|
16天前
|
图形学 Python
SciPy 空间数据2
凸包(Convex Hull)是计算几何中的概念,指包含给定点集的所有凸集的交集。可以通过 `ConvexHull()` 方法创建凸包。示例代码展示了如何使用 `scipy` 库和 `matplotlib` 绘制给定点集的凸包。
24 1
|
17天前
|
JSON 数据格式 索引
Python中序列化/反序列化JSON格式的数据
【11月更文挑战第4天】本文介绍了 Python 中使用 `json` 模块进行序列化和反序列化的操作。序列化是指将 Python 对象(如字典、列表)转换为 JSON 字符串,主要使用 `json.dumps` 方法。示例包括基本的字典和列表序列化,以及自定义类的序列化。反序列化则是将 JSON 字符串转换回 Python 对象,使用 `json.loads` 方法。文中还提供了具体的代码示例,展示了如何处理不同类型的 Python 对象。
|
17天前
|
数据采集 Web App开发 iOS开发
如何使用 Python 语言的正则表达式进行网页数据的爬取?
使用 Python 进行网页数据爬取的步骤包括:1. 安装必要库(requests、re、bs4);2. 发送 HTTP 请求获取网页内容;3. 使用正则表达式提取数据;4. 数据清洗和处理;5. 循环遍历多个页面。通过这些步骤,可以高效地从网页中提取所需信息。
|
29天前
|
存储 数据挖掘
服务器数据恢复—用RAID5阵列中部分盘重建RAID5如何恢复原raid5阵列数据?
服务器数据恢复环境: 一台服务器挂接一台存储,该存储中有一组由5块硬盘组建的RAID5阵列。 服务器故障: 存储raid5阵列中有一块硬盘掉线。由于RAID5的特性,阵列并没有出现问题。工作一段时间后,服务器出现故障,用户方请人维修。维修人员在没有了解故障磁盘阵列环境的情况下,用另外4块硬盘(除去掉线的硬盘)重新创建了一组全新的RAID5阵列并完成数据同步,导致原raid5阵列数据全部丢失。
|
29天前
|
数据可视化 算法 JavaScript
基于图论的时间序列数据平稳性与连通性分析:利用图形、数学和 Python 揭示时间序列数据中的隐藏模式
本文探讨了如何利用图论分析时间序列数据的平稳性和连通性。通过将时间序列数据转换为图结构,计算片段间的相似性,并构建连通图,可以揭示数据中的隐藏模式。文章介绍了平稳性的概念,提出了基于图的平稳性度量,并展示了图分区在可视化平稳性中的应用。此外,还模拟了不同平稳性和非平稳性程度的信号,分析了图度量的变化,为时间序列数据分析提供了新视角。
57 0
基于图论的时间序列数据平稳性与连通性分析:利用图形、数学和 Python 揭示时间序列数据中的隐藏模式
|
16天前
|
索引 Python
SciPy 空间数据1
SciPy 通过 `scipy.spatial` 模块处理空间数据,如判断点是否在边界内、计算最近点等。三角测量是通过测量角度来确定目标距离的方法。多边形的三角测量可将其分解为多个三角形,用于计算面积。Delaunay 三角剖分是一种常用方法,可以对一系列点进行三角剖分。示例代码展示了如何使用 `Delaunay()` 函数创建三角形并绘制。
24 0
|
1月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
113 3
下一篇
无影云桌面