Flask-框架秘籍第三版-全-
Flask 框架秘籍第三版(全)
原文:
zh.annas-archive.org/md5/414c4d9bab5f024306f1ffe2cc266dc5译者:飞龙
前言
Flask,这个轻量级的 Python Web 框架,因其强大的模块化设计而受到欢迎,这使得你可以构建可扩展的 Web 应用程序。通过这本基于食谱的指南,你将探索使用 Flask 构建 Web 应用程序的现代解决方案和最佳实践。
本版更新至 Flask 2.2.x 和 Python 3.11.x 的最新版本,这是《Flask 框架食谱》的第三版,它摒弃了一些旧的和过时的库,并引入了关于前沿技术的食谱。你将发现使用 Flask 创建、部署和管理微服务的不同方法。
本书通过一系列食谱带你了解 Flask 及其扩展的强大功能。你将从探索 Flask 应用程序可以利用的不同配置开始。从这里,你将学习如何处理模板,然后了解 ORM 和视图层,它们是 Web 应用程序的基础。接着,你将学习如何使用 Flask 编写 RESTful API,在学习了各种认证技术之后。
随着你继续前进,你将学习如何编写管理界面,随后是 Flask 中的调试和错误记录。你还将学习如何使你的应用程序多语言,并深入了解各种测试技术。你将了解在 Apache、Tornado、NGINX、Gunicorn、Sentry、New Relic 和 Datadog 等平台上的不同部署和部署后技术。最后,你将了解一些流行的微服务工具,如 Docker、Kubernetes、Google Cloud Run 和 GitHub Actions,这些工具可以用来构建高度可扩展的服务。
新增了一章关于当前引起轰动的最新技术——那就是 GPT。在这里,你将了解一些简单、基本但强大的 GPT 实现,用于构建自动文本补全字段、聊天机器人和 AI 驱动的图像生成。在完成本书之前,你将了解一些额外的技巧和窍门,这些技巧和窍门将有助于处理特定用例,例如全文搜索、缓存、电子邮件和异步操作。
到本书结束时,你将拥有使用这个令人难以置信的微框架所需的所有信息,无论是编写小型还是大型应用程序,都可以使用行业标准进行扩展。
本书面向对象
如果你是一名希望学习更多关于在 Flask 中开发可扩展和可生产的应用程序的 Web 开发者,这本书就是为你准备的。即使你已经熟悉 Flask 的主要扩展并希望将它们用于更好的应用程序开发,这本书也会很有用。如果你需要快速查阅 Flask、其流行的扩展或某些特定用例的任何特定主题,这本书也会很有帮助。假设你具备基本的 Python 编程经验,并对 Web 开发及其相关术语有一定的了解。
本书涵盖内容
第一章,Flask 配置,解释了 Flask 可以以不同的方式配置,以适应任何项目的各种需求。它首先告诉我们如何设置开发环境,然后继续介绍不同的配置技术。
第二章,使用 Jinja 进行模板化,从 Flask 的角度介绍了 Jinja2 模板化的基础知识,并解释了如何使用模块化和可扩展的模板来创建应用程序。
第三章,Flask 中的数据建模,处理了任何应用程序最重要的部分之一——即它与数据库系统的交互。我们将看到 Flask 如何连接到不同的数据库系统,定义模型,以及查询数据库以检索和提供数据。
第四章,与视图一起工作,讨论了网络框架的核心。它讲述了如何与网络请求交互以及对这些请求的适当响应。它涵盖了处理请求的各种正确方法以及最佳设计方式。
第五章,使用 WTForms 进行 Web 表单,涵盖了表单处理,这是任何 Web 应用程序的重要部分。尽管表单很重要,但它们的验证同样重要,甚至更重要。以交互式方式向用户展示这些信息可以为应用程序增添很多价值。
第六章,在 Flask 中进行身份验证,讨论了身份验证,它是应用程序安全与不安全的红线。它详细介绍了多种社交和企业登录技术。身份验证是任何应用程序的重要部分,无论是基于网络的、桌面还是移动的。
第七章,构建 RESTful API,解释了 REST 作为协议,然后讨论了使用库以及完全自定义的 API 为 Flask 应用程序编写 RESTful API。API 可以概括为开发人员访问应用程序的接口。
第八章,Flask 应用的 Admin 界面,专注于为 Flask 应用程序编写管理视图。首先,我们将编写完全自定义的视图,然后借助扩展来编写它们。与非常流行的基于 Python 的网络框架 Django 不同,Flask 默认不提供管理界面。尽管这可能会被许多人视为缺点,但这给了开发者根据其需求创建管理界面的灵活性,并完全控制应用程序。
第九章,国际化与本地化,扩展了 Flask 应用的范围,并涵盖了如何启用对多种语言支持的基本知识。Web 应用通常不仅限于一个地理区域,也不限于服务于一个语言领域的人。例如,旨在为欧洲用户设计的 Web 应用将需要支持其他欧洲语言,如德语、法语、意大利语、西班牙语等,而不仅仅是英语。
第十章,调试、错误处理和测试,从完全面向开发转向测试我们的应用。有了更好的错误处理和测试,应用的健壮性会大大提高,调试使开发者的生活更加轻松。了解我们的应用有多健壮,以及它如何工作并表现,非常重要。当应用出现问题时,及时得到通知是非常必要的。测试本身是一个非常大的主题,有数本书籍专门讨论它。
第十一章,部署和部署后,介绍了应用可以部署的各种方式和工具。然后,你将了解应用监控,这有助于我们跟踪应用性能。应用的部署和管理部署后的应用与开发它一样重要。部署应用的方式有很多,选择最佳方式取决于需求。
第十二章,微服务和容器,探讨了如何使用 Docker 打包 Flask 应用,并使用 Kubernetes 进行部署。我们还将看到如何通过利用 Google Cloud Run 和 GitHub Actions 以无服务器的方式提供服务。微服务是现代软件技术中最热门的术语之一。它们既实用又受欢迎,使开发者的生活更加轻松。它们允许人们专注于开发,而不是将时间花在思考应用的部署上。
第十三章,使用 Flask 的 GPT,实现了 GPT 中最受欢迎、最常见且功能强大的 API,用于构建带有 Flask 的 AI 应用。我们将看到如何使用 GPT 自动化文本补全,以构建高度用户直观的搜索字段。然后,演示了一个简单的使用 ChatGPT 的聊天机器人实现,接着是 AI 驱动的图像生成。
第十四章,额外技巧和窍门,介绍了一些可以用来增加应用价值的额外食谱,如果需要的话。
要充分利用这本书
您需要在计算机上安装 Python 作为先决条件。本书中的所有代码都是在基于 UNIX 的操作系统 macOS 或 Ubuntu 上的 Python 3.11.x 上编写和测试的。本书使用 Flask 2.2.x,并非所有代码都适用于 Flask 和/或 Python 的早期版本。然而,除非引入了破坏性更改,否则大多数代码应该适用于未来的版本。对于其他相关的包或库,版本号在相关食谱中直接提及。
| 本书涵盖的软件/硬件 | 操作系统要求 |
|---|---|
| Python 3.11.x | Windows、macOS 或 Linux |
| Flask 2.2.x | Windows、macOS 或 Linux |
有一些食谱专注于一些付费的 SaaS 软件,例如 Sentry、New Relic、Datadog、亚马逊网络服务(AWS)、谷歌云平台(GCP)、GitHub 和 OpenAI (GPT)。尽管它们都存在试用版或免费版,但如果您超过了免费限制,可能需要购买它们。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub(github.com/PacktPublishing/Flask-Framework-Cookbook-Third-Edition)下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的图书和视频目录的代码包可供下载,请访问github.com/PacktPublishing/。查看它们!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表的彩色图像的 PDF 文件。您可以从这里下载:packt.link/KWUib。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在先前的代码中,首先从给定路径加载实例文件夹;然后,从给定实例文件夹中的 config.cfg 文件加载配置文件。”
代码块设置如下:
PRODUCTS = {
'iphone': {
'name': 'iPhone 5S',
'category': 'Phones',
'price': 699,
},
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
from flask_wtf.file import FileField, FileRequired
class Product(db.Model):
image_path = db.Column(db.String(255))
def __init__(self, name, price, category, image_path):
self.image_path = image_path
任何命令行输入或输出都按以下方式编写:
$ sudo apt update
$ sudo apt install python3-dev
$ sudo apt install apache2 apache2-dev
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“它还可以处理记住我功能、账户恢复功能等。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
customercare@packtpub.com,并在邮件主题中提及书籍标题。
勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问www.packtpub.com/support/errata并填写表格。
copyright@packt.com,并附上相关材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《Flask 框架食谱》,我们非常乐意听到您的想法!请点击此处直接转到该书的 Amazon 评论页面并分享您的反馈。
您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买这本书!
您喜欢在移动中阅读,但无法携带您的印刷书籍到任何地方吗?
您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不会就此停止,您还可以获得独家折扣、时事通讯和每日免费内容的每日邮箱访问权限。
按照以下简单步骤获取福利:
- 扫描二维码或访问以下链接

https://packt.link/free-ebook/9781804611104
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱
第一部分:Flask 基础
正如名称所示,本书的这一部分专注于任何 Flask 网络应用程序的基本构建块。对这些主题有清晰的理解对于使用 Flask 构建可扩展、可配置和可扩展的网络应用程序至关重要,随着我们转向更复杂的话题。
开发者通常在如何设置他们的 Flask 应用程序配置以适应不同环境(如开发和生产)方面感到困难。有时,他们可能会发现很难选择最佳的方式来构建他们的 Flask 应用程序。第一章有助于回答这些问题以及更多。
接下来的三章将重点介绍任何网络应用程序的基础支柱——即,模型、视图和模板。这些章节中涵盖的一个非常重要的概念是连接多种类型的数据库与您的应用程序。
这些章节的内容在复杂度上将是基础到中级,它将为本书下一部分的更复杂用例提供基础。
在本书的第一部分结束时,你将能够创建一个功能齐全的 Flask 应用程序,它将具有基本功能。
本部分包含以下章节:
-
第一章**,Flask 配置
-
第二章**,使用 Jinja 模板化
-
第三章**,Flask 中的数据建模
-
第四章**,与视图一起工作
第一章:Flask 配置
这章入门指南将帮助我们了解 Flask 可以以不同的方式配置,以满足项目的各种需求。Flask 是 “The Python micro framework for building web applications” (pallets/Flask, github.com/pallets/flask)。
那么,为什么 Flask 被称为 microframework?这难道意味着 Flask 缺乏功能,或者意味着您的网络应用程序的完整代码必须包含在一个文件中?并非如此!microframework 这个术语仅仅指的是 Flask 旨在保持其框架核心小而高度可扩展。这使得编写应用程序或扩展既容易又灵活,并赋予开发者选择他们希望为应用程序使用的配置的能力,而不对数据库、模板引擎、管理界面等选择施加任何限制。在本章中,您将学习几种设置和配置 Flask 的方法。
重要信息
这本书整个使用 Python 3 作为 Python 的默认版本。Python 2 在 2019 年 12 月 31 日停止了支持,因此本书不支持 Python 2。建议您在学习本书时使用 Python 3,因为许多配方可能在 Python 2 上无法工作。
同样,在编写本书时,Flask 2.2.x 是最新版本。尽管本书中的许多代码可以在 Flask 的早期版本上运行,但建议您使用 2.2.x 及以上版本。
开始使用 Flask 只需几分钟。设置一个简单的 Hello World 应用程序就像做饼一样简单。只需在您的计算机上任何可以访问 python 或 python3 的位置创建一个文件,例如 app.py,然后包含以下脚本:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello to the World of Flask!'
if __name__ == '__main__':
app.run()
现在,需要安装 Flask;这可以通过 pip 或 pip3 完成。如果您遇到访问问题,可能需要在基于 Unix 的机器上使用 sudo:
$ pip3 install Flask
重要
这里提供的代码和 Flask 安装示例只是为了展示 Flask 可以多么容易地使用。要设置适当的开发生态,请遵循本章中的配方。
前面的代码片段是一个完整的基于 Flask 的网络应用程序。在这里,导入的 Flask 类的实例在这个代码中成为 app,它成为我们的 WSGI 应用程序,并且由于这是一个独立模块,我们将 __name__ 字符串设置为 '__main__'。如果我们将其保存为名为 app.py 的文件,那么应用程序可以通过以下命令简单地运行:
$ python3 app.py
* Running on http://127.0.0.1:5000 (Press CTRL+C to quit)
现在,如果我们打开浏览器并输入 http://127.0.0.1:5000/,我们就可以看到我们的应用程序正在运行。
或者,可以通过使用 flask run 或 Python 的 -m 开关与 Flask 来运行应用程序。在遵循此方法时,可以跳过 app.py 的最后两行。请注意,以下命令仅在当前目录中存在名为 app.py 或 wsgi.py 的文件时才有效。如果没有,则包含 app 对象的文件应作为环境变量导出,即 FLASK_APP。作为最佳实践,在两种情况下都应这样做:
$ export FLASK_APP=app.py
$ flask run
* Running on http://127.0.0.1:5000/
或者,如果你决定使用 -m 开关,它将如下所示:
$ export FLASK_APP=app.py
$ python3 -m flask run
* Running on http://127.0.0.1:5000/
小贴士
不要将你的应用程序文件保存为 flask.py;如果你这样做,在导入时它将与 Flask 本身冲突。
在本章中,我们将介绍以下食谱:
-
使用
virtualenv设置我们的环境 -
处理基本配置
-
配置基于类的设置
-
组织静态文件
-
使用
instance文件夹进行特定于部署的操作 -
视图和模型的组合
-
使用蓝图创建模块化 Web 应用程序
-
使用
setuptools使 Flask 应用程序可安装
技术要求
在一般情况下,与 Flask 和 Python 一起工作相当简单,不需要很多依赖项和配置。在本书的大部分章节中,所有必需的软件包都将在相关食谱中提及。我将在相关章节中提及更具体的要求。一般来说,您需要以下内容:
-
一台不错的计算机,最好是基于 UNIX 的操作系统,如 Linux 或 macOS。您也可以使用 Windows,但这需要一些额外的设置,但这本书的范围之外。
-
选择一个代码编辑器作为 IDE。我使用 Vim 和 Visual Studio Code,但任何支持 Python 的编辑器都可以,只要它支持即可。
-
一个良好的互联网连接,因为您将下载软件包及其依赖项。
所有代码均可在 GitHub 上免费获取,网址为 github.com/PacktPublishing/Flask-Framework-Cookbook-Third-Edition。此 GitHub 仓库包含本书所有章节的代码,分别存放在相应的文件夹中。
设置虚拟环境
Flask 可以简单地使用 pip/pip3 或 easy_install 在全局范围内安装,但最好使用 venv 设置应用程序环境,它将管理在单独的环境中,并且不会让任何库的不正确版本影响任何应用程序。在本食谱中,我们将学习如何创建和管理这些环境。
如何操作...
使用 venv 模块创建虚拟环境。因此,只需在您选择的文件夹中创建一个名为 my_flask_env(或您选择的任何其他名称)的新环境即可,您希望您的开发环境所在的位置。这将创建一个具有相同名称的新文件夹,如下所示:
$ python3 -m venv my_flask_env
从 my_flask_env 文件夹内部运行以下命令:
$ source my_flask_env/bin/activate
$ pip3 install flask
这将激活我们的环境并在其中安装flask。现在,我们可以在该环境中对应用程序进行任何操作,而不会影响任何其他 Python 环境。
它是如何工作的...
到目前为止,我们已经多次使用pip3 install flask。正如其名所示,该命令指的是安装 Flask,就像安装任何 Python 包一样。如果我们稍微深入到通过pip3安装 Flask 的过程,我们会看到安装了几个包。以下是 Flask 包安装过程的概述:
$ pip3 install flask
Collecting Flask
...........
...........
Many more lines.........
...........
Installing collected packages: zipp, Werkzeug, MarkupSafe, itsdangerous, click, Jinja2, importlib-metadata, Flask
Successfully installed Flask-2.1.2 Jinja2-3.1.2 MarkupSafe-2.1.1 Werkzeug-2.1.2 click-8.1.3 importlib-metadata-4.11.4 itsdangerous-2.1.2 zipp-3.8.0
如果我们仔细查看前面的代码片段,我们会看到已经安装了多个包。在这些包中,有五个包,即Werkzeug、Jinja2、click、itsdangerous和markupsafe,是 Flask 所依赖的包,如果其中任何一个缺失,Flask 将无法工作。其他的是 Flask 依赖项所需的子依赖项。
还有更多...
在venv在Python 3.3中引入之前,virtualenv是用于创建和管理虚拟环境的标准库。venv是virtualenv的一个子集,并缺少virtualenv提供的某些高级功能。为了简化并保持本书的上下文,我将使用venv,但你也可以自由探索virtualenv和virtualenvwrapper。
参考信息
本节的相关参考资料如下:
更多关于virtualenv和virtualenvwrapper的信息,请参阅virtualenv.pypa.io/en/latest/和pypi.org/project/virtualenvwrapper/。
处理基本配置
Flask 的一个优点是它很容易根据项目的需求配置 Flask 应用程序。在这个菜谱中,我们将尝试了解 Flask 应用程序可以以哪些不同的方式配置,包括如何从环境变量、Python 文件或甚至config对象中加载配置。
准备工作
在 Flask 中,配置变量存储在名为config的字典样式的Flask对象属性中。config属性是 Python 字典的子类,我们可以像任何字典一样修改它。
如何做到这一点...
要以调试模式运行我们的应用程序,例如,我们可以编写以下代码:
app = Flask(__name__)
app.config['DEBUG'] = True
小贴士
debug布尔值也可以在 Flask对象级别而不是在config级别设置,如下所示:
app.debug = True
或者,我们可以将debug作为命名参数传递给app.run,如下所示:
app.run(debug=True)
在 Flask 的新版本中,调试模式也可以通过环境变量设置,FLASK_DEBUG=1。然后,我们可以使用flask run或 Python 的-m开关来运行应用程序:
$ export FLASK_DEBUG=1
启用调试模式会在代码发生任何更改时自动重新加载服务器,并且在出现问题时还提供了非常有用的Werkzeug调试器。
Flask 提供了许多配置值。在本章的相关食谱中,我们将遇到它们。
随着应用程序的增大,需要将应用程序的配置管理在一个单独的文件中,如下例所示。在您使用的多数操作系统和开发环境中,这个文件不太可能是版本控制系统的一部分。因此,Flask 为我们提供了多种获取配置的方法。最常用的方法如下:
-
从 Python 配置文件(
*.cfg),其中配置可以通过以下语句获取:app.config.from_pyfile('myconfig.cfg') -
从一个对象,其中配置可以通过以下语句获取:
app.config.from_object('myapplication.default_settings') -
或者,要从运行此命令的同一文件中加载,我们可以使用以下语句:
app.config.from_object(__name__) -
从环境变量,配置可以通过以下语句获取:
app.config.from_envvar('PATH_TO_CONFIG_FILE') -
Flask 版本2.0新增了从通用配置文件格式(如JSON或TOML)加载的能力:
app.config.from_file('config.json', load=json.load)
Alternatively, we can do the following:
app.config.from_file('config.toml', load=toml.load)
它是如何工作的...
Flask 设计为仅拾取以大写字母编写的配置变量。这允许我们在配置文件和对象中定义任何局部变量,其余的由 Flask 处理。
使用配置的最佳实践是在app.py中或通过应用程序中的任何对象设置一些默认设置,然后通过从配置文件中加载来覆盖它们。因此,代码将如下所示:
app = Flask(__name__)
DEBUG = True
TESTING = True
app.config.from_object(__name__)
app.config.from_pyfile('/path/to/config/file')
使用基于类的设置进行配置
对于不同的部署模式,如生产、测试、预发布等,使用类的继承模式来布局配置是一种有效的方法。随着项目的增大,您可以有不同部署模式,每种模式可以有不同的配置设置,或者有一些设置将保持不变。在本食谱中,我们将学习如何使用基于类的设置来实现这种模式。
如何实现...
我们可以有一个具有默认设置的基类;然后,其他类可以简单地从基类继承,并覆盖或添加特定于部署的配置变量,如下例所示:
class BaseConfig(object):
'Base config class'
SECRET_KEY = 'A random secret key'
DEBUG = True
TESTING = False
NEW_CONFIG_VARIABLE = 'my value'
class ProductionConfig(BaseConfig):
'Production specific config'
DEBUG = False
SECRET_KEY = open('/path/to/secret/file').read()
class StagingConfig(BaseConfig):
'Staging specific config'
DEBUG = True
class DevelopmentConfig(BaseConfig):
'Development environment specific config'
DEBUG = True
TESTING = True
SECRET_KEY = 'Another random secret key'
重要信息
在生产配置中,密钥通常存储在一个单独的文件中,因为出于安全原因,它不应成为版本控制系统的一部分。这应该在机器的本地文件系统中保留,无论是你的机器还是服务器。
它是如何工作的...
现在,我们可以在通过from_object()加载应用程序配置时使用任何前面的类。假设我们将前面的基于类的配置保存在一个名为configuration.py的文件中,如下所示:
app.config.from_object('configuration.DevelopmentConfig')
总体来说,这使得管理不同部署环境下的配置更加灵活和容易。
组织静态文件
高效地组织静态文件,如 JavaScript、样式表、图像等,一直是所有 Web 框架关注的焦点。在本教程中,我们将学习如何在 Flask 中实现这一点。
如何操作...
Flask 推荐一种特定的方式来组织应用程序中的静态文件,如下所示:
my_app/
app.py
config.py
__init__.py
static/
css/
js/
images/
logo.png
在模板中渲染时(比如,logo.png文件),我们可以使用以下代码引用静态文件:
<img src='/static/images/logo.png'>
它是如何工作的...
如果在应用程序的根级别存在一个名为static的文件夹——即与app.py在同一级别——那么 Flask 将自动读取该文件夹的内容,无需任何额外配置。
还有更多...
或者,我们可以在定义app.py中的应用程序时提供一个名为static_folder的参数,如下所示:
app = Flask(__name__,
static_folder='/path/to/static/folder')
在前面的代码行中,static指的是应用程序对象上的static_folder的值。这可以通过提供 URL 前缀来修改,如下所示:
app = Flask(
_name_, static_url_path='/differentstatic',
static_folder='/path/to/static/folder'
)
现在,要渲染静态文件,我们可以使用以下代码:
<img src='/differentstatic/logo.png'>
总是使用url_for为静态文件创建 URL,而不是显式定义它们,这是一个好习惯,如下所示:
<img src="img/{{ url_for('static', filename='logo.png') }}">
使用实例文件夹进行部署特定操作
Flask 还提供了一个用于配置的另一种方法,我们可以通过它有效地管理部署特定的部分。实例文件夹允许我们将部署特定的文件从受版本控制的应用程序中分离出来。我们知道配置文件可以针对不同的部署环境分开,例如开发和生产,但还有许多其他文件,如数据库文件、会话文件、缓存文件和其他运行时文件。在本教程中,我们将创建一个实例文件夹,它将充当此类文件的容器。按照设计,实例文件夹不会成为版本控制系统的一部分。
如何操作...
默认情况下,如果我们在应用程序级别有一个名为instance的文件夹,应用程序会自动选择实例文件夹,如下所示:
my_app/
app.py
instance/
config.cfg
我们还可以通过在应用程序对象上使用instance_path参数显式定义实例文件夹的绝对路径,如下所示:
app = Flask(
__name__,
instance_path='/absolute/path/to/instance/folder')
要从实例文件夹中加载配置文件,我们可以在应用程序对象上使用 instance_relative_config 参数,如下所示:
app = Flask(__name__, instance_relative_config=True)
这告诉应用程序从实例文件夹中加载配置文件。以下示例显示了如何配置此操作:
app = Flask(
__name__, instance_path='path/to/instance/folder',
instance_relative_config=True
)
app.config.from_pyfile('config.cfg', silent=True)
它是如何工作的...
在前面的代码中,首先从给定的路径加载实例文件夹;然后从给定的实例文件夹中的 config.cfg 文件加载配置文件。在这里,silent=True 是可选的,用于抑制如果实例文件夹中没有找到 config.cfg 时出现的错误。如果没有提供 silent=True 并且文件未找到,则应用程序将失败,并给出以下错误:
IOError: [Errno 2] Unable to load configuration file (No such file or directory): '/absolute/path/to/config/file'
信息
可能看起来使用 instance_relative_config 从实例文件夹加载配置是重复的工作,并且可以将其移动到配置方法之一。然而,这个过程的美妙之处在于实例文件夹的概念与配置完全独立,而 instance_relative_config 只是补充了配置对象。
视图和模型的组合
随着我们的应用程序变得更大,我们可能希望以模块化的方式对其进行结构化。在这个菜谱中,我们将通过重构我们的 Hello World 应用程序来实现这一点。
如何做到这一点...
首先,在应用程序中创建一个新的文件夹,并将所有文件移动到这个新文件夹中。然后,在这些文件夹中创建 __init__.py,这些文件夹将被用作模块。
之后,在顶级文件夹中创建一个名为 run.py 的新文件。正如其名所示,此文件将用于运行应用程序。
最后,创建单独的文件夹来充当模块。
参考以下文件结构以获得更好的理解:
flask_app/
run.py
my_app/
__init__.py
hello/
__init__.py
models.py
views.py
让我们看看前面的每个文件将如何看起来。
flask_app/run.py 文件将类似于以下代码行:
from my_app import app
app.run(debug=True)
flask_app/my_app/__init__.py 文件将类似于以下代码行:
from flask import Flask
app = Flask(__name__)
import my_app.hello.views
接下来,我们有一个空文件,只是为了使封装文件夹成为一个 Python 包,flask_app/my_app/hello/__init__.py:
# No content.
# We need this file just to make this folder a python module.
模型文件,flask_app/my_app/hello/models.py,有一个非持久的键值存储,如下所示:
MESSAGES = {
'default': 'Hello to the World of Flask!',
}
最后,以下是一个视图文件,flask_app/my_app/hello/views.py。在这里,我们获取与请求的键对应的消息,也可以创建或更新一个消息:
from my_app import app
from my_app.hello.models import MESSAGES
@app.route('/')
@app.route('/hello')
def hello_world():
return MESSAGES['default']
@app.route('/show/<key>')
def get_message(key):
return MESSAGES.get(key) or "%s not found!" % key
@app.route('/add/<key>/<message>')
def add_or_update_message(key, message):
MESSAGES[key] = message
return "%s Added/Updated" % key
它是如何工作的...
在这个菜谱中,我们有一个循环导入,在 my_app/__init__.py 和 my_app/hello/views.py 之间,在前者中,我们从后者导入 views,在后者中,我们从前者导入 app。尽管这使得两个模块相互依赖,但没有任何问题,因为我们不会在 my_app/__init__.py 中使用视图。请注意,最好在文件的底部导入视图,这样它们就不会在这个文件中使用。这确保了当你在视图中引用 app 对象时,不会导致空指针异常。
在这个配方中,我们使用一个非常简单的非持久内存中的键值存储来演示模型的布局结构。我们可以在 views.py 中直接编写 MESSAGES 哈希表的字典,但最佳实践是将模型和视图层分开。
因此,我们可以仅使用 run.py 来运行此应用程序,如下所示:
$ python run.py
Serving Flask app "my_app" (lazy loading)
Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
Debug mode: on
Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Restarting with stat
Debugger is active!
* Debugger PIN: 111-111-111
小贴士
注意块中的前一个 WARNING。这个警告发生是因为我们没有指定应用程序环境,默认情况下假设为 production。要在 development 环境中运行应用程序,请使用以下内容修改 run.py 文件:
from my_app import app
app.env="development"
app.run(debug=True)
信息
重载指示应用程序正在调试模式下运行,并且每当代码中发生更改时,应用程序将重新加载。
如我们所见,我们已经在 MESSAGES 中定义了一个默认消息。我们可以通过打开 http://127.0.0.1:5000/show/default 来查看它。要添加一条新消息,我们可以输入 http://127.0.0.1:5000/add/great/Flask%20is%20greatgreat!!。这将更新 MESSAGES 键值存储,使其看起来像这样:
MESSAGES = {
'default': 'Hello to the World of Flask!',
'great': 'Flask is great!!',
}
现在,如果我们在一个浏览器中打开 http://127.0.0.1:5000/show/great,我们将看到我们的消息,否则它将显示为一个未找到的消息。
参见
下一个配方 使用蓝图创建模块化 Web 应用 提供了一种组织 Flask 应用程序的更好方法,并且是循环导入的现成解决方案。
使用蓝图创建模块化 Web 应用
蓝图 是 Flask 中的一项功能,有助于使大型应用程序模块化。它通过提供一个中央位置来注册应用程序中的所有组件,从而简化了应用程序的分派。蓝图看起来像一个应用程序对象,但它不是一个应用程序。它也看起来像一个可插入的应用程序或更大应用程序的一个较小部分,但它不是。蓝图是一组可以在应用程序上注册的操作,它代表了如何构建或构建应用程序。另一个好处是,它允许我们在多个应用程序之间创建可重用的组件。
准备工作
在这个配方中,我们将以前一个配方 视图和模型的组合 中的应用程序作为参考,并修改它,使其使用蓝图工作。
如何做到这一点...
以下是一个使用 Blueprint 的简单 Hello World 应用程序的示例。它将像上一个配方中那样工作,但将更加模块化和可扩展。
首先,我们将从以下 flask_app/my_app/__init__.py 文件开始:
from flask import Flask
from my_app.hello.views import hello
app = Flask(__name__)
app.register_blueprint(hello)
接下来,我们将在视图文件 my_app/hello/views.py 中添加一些代码,它应该看起来如下:
from flask import Blueprint
from my_app.hello.models import MESSAGES
hello = Blueprint('hello', __name__)
@hello.route('/')
@hello.route('/hello')
def hello_world():
return MESSAGES['default']
@hello.route('/show/<key>')
def get_message(key):
return MESSAGES.get(key) or "%s not found!" % key
@hello.route('/add/<key>/<message>')
def add_or_update_message(key, message):
MESSAGES[key] = message
return "%s Added/Updated" % key
我们现在在flask_app/my_app/hello/views.py文件中定义了一个蓝图。我们不再需要这个文件中的应用程序对象,并且我们的完整路由定义在一个名为hello的蓝图中。我们使用@hello.route而不是@app.route。相同的蓝图被导入到flask_app/my_app/__init__.py中,并在应用程序对象上注册。
我们可以在应用程序中创建任意数量的蓝图,并完成我们通常会做的许多活动,例如提供不同的模板路径或不同的静态路径。我们甚至可以为我们的蓝图设置不同的 URL 前缀或子域名。
它是如何工作的...
此应用程序将以与上一个应用程序完全相同的方式运行。唯一的区别在于代码的组织方式。
使用 setuptools 使 Flask 应用程序可安装
我们现在有一个 Flask 应用程序,但如何像使用setuptools创建可安装的 Python 包一样安装它。
什么是 Python 包?
可以简单地将 Python 包视为一个程序,可以在虚拟环境或基于其安装范围全局使用 Python 的import语句导入。
如何做到...
使用setuptools Python 库可以轻松地安装 Flask 应用程序。为了实现这一点,在应用程序文件夹中创建一个名为setup.py的文件,并配置它以运行应用程序的设置脚本。这将处理任何依赖项、描述、加载测试包等。
以下是一个简单的setup.py脚本示例,用于之前配方中的Hello World应用程序:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import os
from setuptools import setup
setup(
name = 'my_app',
version='1.0',
license='GNU General Public License v3',
author='Shalabh Aggarwal',
author_email='contact@shalabhaggarwal.com',
description='Hello world application for Flask',
packages=['my_app'],
platforms='any',
install_requires=[
'Flask',
],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public
License v3',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development :: Libraries ::
Python Modules'
],
)
它是如何工作的...
在前面的脚本中,大部分配置都是不言自明的。分类器在应用程序在PyPI上提供时使用。这些将帮助其他用户使用相关分类器搜索应用程序。
现在,我们可以使用install关键字运行此文件,如下所示:
$ python setup.py install
前面的命令将安装应用程序以及install_requires中提到的所有依赖项——也就是说,Flask 及其所有依赖项。现在,该应用程序可以在 Python 环境中像任何 Python 包一样使用。
要验证包安装成功,在 Python 环境中导入它:
$ python
Python 3.8.13 (default, May 8 2022, 17:52:27)
>>> import my_app
>>>
相关内容
有效 trove 分类器的列表可以在pypi.python.org/pypi?%3Aaction=list_classifiers找到。
第二章:使用 Jinja 模板
本章将从 Flask 的角度介绍 Jinja 模板的基础知识。我们还将学习如何使用模块化和可扩展的模板设计和开发应用程序。
信息
如果你已经跟随 Flask 或 Jinja 或本书的前几版,你可能已经注意到,之前这个模板库被称为 Jinja2。本书撰写时 Jinja 的最新版本是 3 版,作者/社区决定将其称为 Jinja,而不是继续使用容易混淆的 Jinja2、Jinja3 等名称。
在 Flask 中,我们可以编写一个完整的 Web 应用程序,无需第三方模板引擎。例如,看看以下代码;这是一个包含一些 HTML 样式的独立、简单的 Hello World 应用程序:
from flask import Flask
app = Flask(__name__)
@app.route('/')
@app.route('/hello')
@app.route('/hello/<user>')
def hello_world(user=None):
user = user or 'Shalabh'
return '''
<html>
<head>
<title>Flask Framework Cookbook</title>
</head>
<body>
<h1>Hello %s!</h1>
<p>Welcome to the world of Flask!</p>
</body>
</html>''' % user
if __name__ == '__main__':
app.run()
在涉及数千行 HTML、JS 和 CSS 代码的大型应用程序中,前面提到的应用程序编写模式可行吗?在我看来,不可行!
幸运的是,模板提供了一种解决方案,因为它允许我们通过保持模板的可扩展性和分离来结构化我们的视图代码。Flask 默认支持 Jinja,尽管我们可以使用任何适合我们的模板引擎。此外,Jinja 提供了许多额外的功能,使模板非常强大和模块化。
本章将介绍以下食谱:
-
启动标准布局
-
实现块组合和布局继承
-
创建自定义上下文处理器
-
创建自定义 Jinja 过滤器
-
为表单创建自定义宏
-
高级日期和时间格式化
技术要求
Jinja 是作为 Flask 标准安装的一部分安装的。无需单独安装。有关更多详细信息,请参阅 第一章 中的 设置虚拟环境 部分。
启动标准布局
Flask 中的大多数应用程序遵循特定的模板布局模式。在本食谱中,我们将实现 Flask 应用程序中模板布局的结构化推荐方式。
准备工作
默认情况下,Flask 预期模板应放置在应用程序根级别的名为 templates 的文件夹中。如果该文件夹存在,那么 Flask 将自动读取其内容,通过使该文件夹的内容可用于 render_template() 方法来使用,我们将在此书中广泛使用该方法。
如何做...
让我们用一个小的应用程序来演示这一点。这个应用程序与我们开发的 第一章 中 Flask 配置 部分非常相似。
首先要做的是在 my_app 下添加一个名为 templates 的新文件夹。应用程序结构应如下目录结构:
flask_app/
run.py
my_app/
__init__.py
hello/
__init__.py
views.py
templates
我们现在需要对应用程序进行一些修改。views 文件中的 hello_world() 方法,位于 my_app/hello/views.py,应如下所示:
from flask import render_template, request
@hello.route('/')
@hello.route('/hello')
def hello_world():
user = request.args.get('user', 'Shalabh')
return render_template('index.html', user=user)
在前面的方法中,我们寻找一个 URL 查询参数user。如果找到了,我们就使用它;如果没有找到,我们就使用默认参数Shalabh。然后,这个值被传递到要渲染的模板的上下文中——即index.html——然后渲染出结果模板。
my_app/templates/index.html模板可以简单地是以下内容:
<html>
<head>
<title>Flask Framework Cookbook</title>
</head>
<body>
<h1>Hello {{ user }}!</h1>
<p>Welcome to the world of Flask!</p>
</body>
</html>
它是如何工作的...
现在,如果我们在一个浏览器中打开http://127.0.0.1:5000/hello URL,我们应该看到类似于以下截图的响应:

图 2.1 – 第一个渲染的模板
如果我们传递一个包含user键值的 URL 参数,例如http://127.0.0.1:5000/hello?user=John,我们应该看到以下响应:

图 2.2 – 在模板中提供自定义内容
如我们在views.py中看到的,传递到 URL 的参数是通过request.args.get('user')从request对象中获取的,然后通过render_template传递到正在渲染的模板的上下文中。然后,使用 Jinja 占位符{{ user }}解析该参数,以从模板上下文中的当前user变量值获取内容。这个占位符会根据模板上下文评估其内部放置的所有表达式。
信息
Jinja 文档可以在jinja.pocoo.org/找到。当编写模板时,这将非常有用。
实现块组合和布局继承
通常,任何 Web 应用程序都将有许多不同的网页。然而,像页眉和页脚这样的代码块几乎在网站的所有页面上都会以相同的方式出现;同样,菜单也会保持不变。实际上,通常只是中心容器块会改变。为此,Jinja 提供了确保模板之间继承的绝佳方式。
考虑到这一点,有一个基础模板是一个好习惯,其中可以构建网站的基本布局,包括页眉和页脚。
准备工作
在这个菜谱中,我们将创建一个小型应用程序,它将有一个主页和一个产品页(例如我们在电子商务网站上看到的)。我们将使用 Bootstrap 框架为我们的模板提供简约的设计和主题。可以从getbootstrap.com/下载 Bootstrap v5。
信息
写作本文时,Bootstrap 的最新版本是 v5。不同的 Bootstrap 版本可能会导致应用程序的 UI 以不同的方式表现,但 Bootstrap 的核心本质保持不变。
为了简单和专注于当前的主题,我们创建了一个硬编码的产品数据存储,可以在 models.py 文件中找到。这些在 views.py 中被导入和读取,并通过 render_template() 方法作为模板上下文变量发送到模板。其余的解析和显示由模板语言处理,在我们的例子中是 Jinja。
如何做...
看看下面的布局:
flask_app/
- run.py
- my_app/
- __init__.py
- product/
- __init__.py
- views.py
- models.py
- templates/
- base.html
- home.html
- product.html
- static/
- js/
- bootstrap.bundle.min.js
- jquery.min.js
- css/
- bootstrap.min.css
- main.css
在前面的布局中,static/css/bootstrap.min.css 和 static/js/bootstrap.bundle.min.js 是可以从 准备就绪 部分提到的 Bootstrap 网站下载的标准文件。run.py 文件保持不变,就像往常一样。其余的应用程序构建过程如下。
首先,在 my_app/product/models.py 中定义模型。在本章中,我们将处理一个简单、非持久的键值存储。我们将从提前编写的几个硬编码的产品记录开始,如下所示:
PRODUCTS = {
'iphone': {
'name': 'iPhone 5S',
'category': 'Phones',
'price': 699,
},
'galaxy': {
'name': 'Samsung Galaxy 5',
'category': 'Phones',
'price': 649,
},
'ipad-air': {
'name': 'iPad Air',
'category': 'Tablets',
'price': 649,
},
'ipad-mini': {
'name': 'iPad Mini',
'category': 'Tablets',
'price': 549
}
}
接下来是视图——即 my_app/product/views.py。在这里,我们将遵循蓝图风格来编写应用程序,如下所示:
from werkzeug.exceptions import abort
from flask import render_template
from flask import Blueprint
from my_app.product.models import PRODUCTS
product_blueprint = Blueprint('product', __name__)
@product_blueprint.route('/')
@product_blueprint.route('/home')
def home():
return render_template('home.html', products=PRODUCTS)
@product_blueprint.route('/product/<key>')
def product(key):
product = PRODUCTS.get(key)
if not product:
abort(404)
return render_template('product.html', product=product)
信息
当你需要用一个特定的错误信息来终止一个请求时,abort() 方法会很有用。Flask 提供了基本的错误信息页面,可以根据需要自定义。我们将在 第四章 的 创建自定义 404 和 500 处理器 菜谱中查看它们,与视图一起工作。
在 Blueprint 构造函数中传入的蓝图名称(product)将被附加到在此蓝图定义的端点上。请查看 base.html 代码以获得清晰度。
现在,创建应用程序的配置文件,my_app/__init__.py,它应该如下几行代码所示:
from flask import Flask
from my_app.product.views import product_blueprint
app = Flask(__name__)
app.register_blueprint(product_blueprint)
总是可能创建自己的自定义 CSS,这可以为 Bootstrap 提供的标准 CSS 添加更多风味。为此,在 my_app/static/css/main.css 中添加一些自定义 CSS 代码,如下所示:
body {
padding-top: 50px;
}
.top-pad {
padding: 10px 15px;
text-align: center;
}
在考虑模板时,始终创建一个基本模板来存放所有其他模板可以继承的通用代码。将此模板命名为 base.html 并将其放置在 my_app/templates/base.html 中,如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,
initial-scale=1">
<title>Flask Framework Cookbook</title>
<link href="{{ url_for('static',
filename='css/bootstrap.min.css') }}"
rel="stylesheet">
<link href="{{ url_for('static',
filename='css/main.css') }}" rel="stylesheet">
<script src="{{ url_for('static',
filename='js/moment.min.js') }}"></script>
</head>
<body>
<nav class="navbar navbar-dark bg-dark fixed-top"
role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="{{
url_for('product.home') }}">Flask Cookbook</a>
</div>
</nav>
</div>
<div class="container">
{% block container %}{% endblock %}
</div>
<script src="{{ url_for('static',
filename='js/jquery.min.js') }}"></script>
<script src="{{ url_for('static',
filename='js/bootstrap.bundle.min.js') }}"></script>
</body>
</html>
之前的大部分代码包含正常的 HTML 和 Jinja 评估占位符,这是我们之前在 引导推荐布局 菜谱中介绍的。然而,需要注意的是 url_for() 方法是如何用于蓝图 URL 的。蓝图名称被附加到所有端点上。当应用程序中存在多个蓝图时,这非常有用,因为其中一些可能具有相似的 URL。
在主页上,my_app/templates/home.html,遍历所有产品并显示它们,如下所示:
{% extends 'base.html' %}
{% block container %}
<div class="top-pad">
{% for id, product in products.items() %}
<div class="top-pad offset-1 col-sm-10">
<div class="card text-center">
<div class="card-body">
<h2>
<a href="{{ url_for('product.product', key=id)
}}">{{ product['name'] }}</a>
<small>$ {{ product['price'] }}</small>
</h2>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}
然后,创建单个产品页面,my_app/templates/product.html,它应该如下几行代码所示:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<h1>{{ product['name'] }}</h1>
<h3><small>{{ product['category'] }}</small></h3>
<h5>$ {{ product['price'] }}</h5>
</div>
{% endblock %}
它是如何工作的...
在上述模板结构中,遵循了一个继承模式。base.html 文件作为所有其他模板的基础模板。home.html 文件从 base.html 继承,而 product.html 从 home.html 继承。在 product.html 中,container 块被覆盖,它最初是在 home.html 中填充的。当运行此应用程序时,我们应该看到类似于以下截图所示的输出:

图 2.3 – 遍历块以创建可重用内容
上述截图显示了主页的外观。注意浏览器中的 URL。产品页面应如下所示:

图 2.4 – 继承模板块以创建干净的代码
创建自定义上下文处理器
有时,我们可能希望在模板中直接计算或处理一个值。Jinja 维持逻辑处理应在视图中而不是在模板中进行的观点,从而保持模板的简洁。在这种情况下,上下文处理器成为一个方便的工具。使用上下文处理器,我们可以将我们的值传递给一个方法,然后该方法将在 Python 方法中处理,并返回我们的结果值。这可以通过简单地向模板上下文中添加一个函数来实现,因为 Python 允许用户像传递任何其他对象一样传递函数。在本食谱中,我们将了解如何编写自定义上下文处理器。
如何操作...
要编写自定义上下文处理器,请按照以下步骤操作。
让我们先以 类别 / 产品名称 格式显示产品的描述性名称。之后,将方法添加到 my_app/product/views.py 中,如下所示:
@product_blueprint.context_processor
def some_processor():
def full_name(product):
return '{0} / {1}'.format(product['category'],
product['name'])
return {'full_name': full_name}
上下文是一个简单的字典,可以修改以添加或删除值。任何使用 @product_blueprint.context_processor 装饰的方法都应该返回一个更新实际上下文的字典。我们可以使用前面的上下文处理器如下:
{{ full_name(product) }}
我们可以将前面的代码以以下方式添加到我们的应用程序中,用于产品列表(在 flask_app/my_app/templates/product.html 文件中):
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<h4>{{ full_name(product) }}</h4>
<h1>{{ product['name'] }}</h1>
<h3><small>{{ product['category'] }}</small></h3>
<h5>$ {{ product['price'] }}</h5>
</div>
{% endblock %}
生成的解析后的 HTML 页面应类似于以下截图:

图 2.5 – 产品名称的自定义上下文处理器
信息
请参考本章前面关于实现块组合和布局继承的食谱,以更好地理解本食谱中关于产品和类别逻辑的上下文。
创建自定义 Jinja 过滤器
在查看前面的菜谱后,经验丰富的开发者可能会想知道为什么我们使用上下文处理器来创建一个格式良好的产品名称。嗯,我们也可以为同一目的编写一个过滤器,这将使事情更加简洁。可以编写一个过滤器来显示产品的描述性名称,如下例所示:
@product_blueprint.app_template_filter('full_name')
def full_name_filter(product):
return '{0} / {1}'.format(product['category'],
product['name'])
这也可以按如下方式使用:
{{ product|full_name }}
前面的代码将产生与前面菜谱中类似的结果。接下来,让我们通过使用外部库来格式化货币,将事情提升到更高的层次。
如何做...
首先,让我们创建一个过滤器,根据当前本地语言格式化货币。将以下代码添加到my_app/__init__.py中:
import ccy
from flask import request
@app.template_filter('format_currency')
def format_currency_filter(amount):
currency_code =
ccy.countryccy(request.accept_languages.best[-2:])
return '{0} {1}'.format(currency_code, amount)
信息
request.accept_languages可能在请求没有ACCEPT-LANGUAGES头的情况下不起作用。
前面的代码片段需要安装一个新的包,ccy,如下所示:
$ pip install ccy
在这个例子中创建的过滤器将选择与当前浏览器区域设置最佳匹配的语言(在我的情况下是en-US),然后从区域字符串中取出最后两个字符,并按照 ISO 国家代码生成货币,国家代码由两个字符表示。
小贴士
在这个菜谱中需要注意的一个有趣点是,Jinja 过滤器可以在蓝图级别以及应用级别创建。如果过滤器在蓝图级别,装饰器将是app_template_filter;否则,在应用级别,装饰器将是template_filter。
它是如何工作的...
该过滤器可以按如下方式用于我们的产品模板:
<h3>{{ product['price']|format_currency }}</h3>
前面的代码将产生以下截图所示的结果:

图 2.6 – 用于显示货币的自定义 Jinja 过滤器
相关内容
本章前面关于实现块组合和布局继承的菜谱将有助于你理解本菜谱中关于产品和类别逻辑的上下文。
为表单创建自定义宏
宏允许我们编写可重用的 HTML 块。它们类似于常规编程语言中的函数。我们可以像在 Python 中的函数一样向宏传递参数,然后我们可以使用它们来处理 HTML 块。宏可以被调用任意次数,输出将根据它们内部的逻辑而变化。在这个菜谱中,让我们了解如何在 Jinja 中编写宏。
准备工作
Jinja 中的宏是一个非常常见的话题,并且有很多用例。在这里,我们只看看如何创建宏并在导入后使用它。
如何做...
HTML 中最冗余的代码之一是定义表单中输入字段的代码。这是因为大多数字段具有相似的代码,可能只是进行了一些样式修改。
以下是一个宏示例,当调用时会创建输入字段。最佳实践是将宏创建在单独的文件中以提高可重用性——例如,_helpers.html:
{% macro render_field(name, class='', value='',
type='text') -%}
<input type="{{ type }}" name="{{ name }}" class="{{ class
}}" value="{{ value }}"/>
{%- endmacro %}
信息
% 前后的减号 (-) 会去除这些块前后空白,使 HTML 代码更易于阅读。
现在,应将宏导入到要使用的文件中,如下所示:
{% from '_helpers.html' import render_field %}
现在可以使用以下代码调用它:
<fieldset>
{{ render_field('username', 'icon-user') }}
{{ render_field('password', 'icon-key', type='password') }}
</fieldset>
将宏定义在不同的文件中是一种良好的实践,这样可以保持代码整洁并提高代码的可读性。
小贴士
如果你需要编写一个不能从当前文件外部访问的私有宏,请使用名称前带有下划线的宏名称。
高级日期和时间格式化
在网络应用程序中处理日期和时间格式化是一件痛苦的事情。在 Python 中使用 datetime 库进行此类格式化通常会增加开销,并且当涉及到正确处理时区时相当复杂。当在数据库中存储时间戳时,将时间戳标准化为 UTC 是一种最佳实践,但这意味着每次向全球用户展示时间戳时都需要处理它。
相反,将此处理推迟到客户端——即浏览器——会更智能。浏览器始终知道其用户的当前时区,因此将能够正确地操作日期和时间信息。这种方法还可以减少应用程序服务器上的任何不必要的开销。在本食谱中,我们将了解如何实现这一点。我们将使用 Moment.js 来完成此目的。
准备工作
Moment.js 可以像任何 JS 库一样包含在我们的应用程序中。我们只需下载并将 JS 文件 moment.min.js 放置在 static/js 文件夹中。Moment.js 文件可以从 momentjs.com/ 下载。然后,可以通过在 HTML 文件中添加以下语句以及其他 JavaScript 库来使用此文件:
<script src="{{ url_for('static',
filename='js/moment.min.js') }}"></script>
Moment.js 的基本用法如下所示。这可以在浏览器控制台中为 JavaScript 执行:
>>> moment().calendar(); "Today at 11:09 AM"
>>> moment().endOf('day').fromNow(); "in 13 hours"
>>> moment().format('LLLL'); "Sunday, July 24, 2022 11:10
AM"
如何操作...
要在应用程序中使用 Moment.js,请遵循所需的步骤。
首先,在 Python 中编写一个包装器,并通过 Jinja 环境变量使用它,如下所示:
from markupsafe import Markup
class momentjs(object):
def __init__(self, timestamp):
self.timestamp = timestamp
# Wrapper to call moment.js method
def render(self, format):
return Markup(
"<script>\ndocument
.write(moment(\"%s\").%s);\n</script>" % (
self.timestamp.strftime("%Y-%m-%dT%H:%M:%S"),
format
)
)
# Format time
def format(self, fmt):
return self.render("format(\"%s\")" % fmt)
def calendar(self):
return self.render("calendar()")
def fromNow(self):
return self.render("fromNow()")
你可以根据需要添加任意多的 Moment.js 方法到前面的类中。现在,在你的 app.py 文件中,将以下创建的类设置为 Jinja 环境变量:
# Set jinja template global
app.jinja_env.globals['momentjs'] = momentjs
你现在可以在模板中使用这个类,如下面的示例所示。确保 timestamp 是一个 JavaScript date 对象的实例:
<p>Current time: {{ momentjs(timestamp).calendar() }}</p>
<br/>
<p>Time: {{momentjs(timestamp).format('YYYY-MM-DD
HH:mm:ss')}}</p>
<br/>
<p>From now: {{momentjs(timestamp).fromNow()}}</p>
它是如何工作的…
以下截图显示了前面 HTML 的输出。注意每个语句的格式如何不同。

图 2.7 – 使用 momentjs 格式化日期时间
还有更多…
您可以在momentjs.com/了解更多关于Moment.js库的信息。
第三章:Flask 中的数据建模
本章涵盖了任何应用程序最重要的方面之一,即与数据库系统的交互。在本章中,您将了解 Flask 如何连接到数据库系统,定义模型,以及查询数据库以检索和传输数据。Flask 被设计得足够灵活,以支持任何数据库。最简单的方法是使用直接的SQLite3包,这是一个DB-API 2.0接口,并不提供实际的对象关系映射(ORM)。在这里,我们需要编写 SQL 查询来与数据库通信。这种方法不推荐用于大型项目,因为它最终可能成为维护应用程序的噩梦。此外,使用这种方法,模型几乎不存在,所有操作都在视图函数中完成,将数据库查询写入视图函数不是一种好的做法。
在本章中,我们将讨论如何使用 SQLAlchemy 为我们的 Flask 应用程序创建 ORM 层,这对于任何大小的应用程序都是推荐和广泛使用的。此外,我们还将简要了解如何使用 NoSQL 数据库系统编写 Flask 应用程序。
信息
ORM 表示我们的应用程序数据模型在概念层面上如何存储和处理数据。一个强大的 ORM 使得设计和查询业务逻辑变得简单且流畅。
在本章中,我们将介绍以下菜谱:
-
创建一个 SQLAlchemy 数据库实例
-
创建基本的产品模型
-
创建关系型类别模型
-
使用 Alembic 和 Flask-Migrate 迁移数据库
-
使用 Redis 索引模型数据
-
选择 MongoDB 的 NoSQL 方式
创建一个 SQLAlchemy 数据库实例
SQLAlchemy 是一个 Python SQL 工具包,它提供了 ORM,它结合了 SQL 的灵活性和强大功能以及 Python 面向对象的特性。在本例中,我们将了解如何创建一个 SQLAlchemy 数据库实例,该实例可用于执行未来菜谱中将要涵盖的任何数据库操作。
准备工作
Flask-SQLAlchemy 是提供 Flask 的 SQLAlchemy 接口的扩展。此扩展可以通过以下方式简单地使用pip安装:
$ pip install flask-sqlalchemy
在使用 Flask-SQLAlchemy 时,首先要记住的是应用程序配置参数,它告诉 SQLAlchemy 要使用数据库的位置:
app.config['SQLALCHEMY_DATABASE_URI'] =
os.environ('DATABASE_URI')
SQLALCHEMY_DATABASE_URI是数据库协议、所需的任何身份验证以及数据库名称的组合。在 SQLite 的情况下,它可能看起来如下所示:
sqlite:////tmp/test.db
在 PostgreSQL 的情况下,它看起来如下所示:
postgresql://yourusername:yourpassword@localhost/yournewdb
此扩展提供了一个名为Model的类,它帮助我们为应用程序定义模型。有关数据库 URL 的更多信息,请参阅docs.sqlalchemy.org/en/14/core/engines.html#database-urls。
小贴士
SQLite 数据库 URI 是操作系统特定的,这意味着 URI 对于 Unix/macOS/Linux 和 Windows 会有所不同。请参阅docs.sqlalchemy.org/en/14/core/engines.html#sqlite以获取更多详细信息。
信息
对于 SQLite 以外的所有数据库系统,都需要单独的库。例如,要使用 PostgreSQL,你需要psycopg2。
如何做到这一点...
让我们在本食谱中创建一个小型应用程序,以了解 Flask 的基本数据库连接。我们将在接下来的几个食谱中构建这个应用程序。在这里,我们只需看看如何创建一个db实例并验证其存在。文件的结构看起来如下:
flask_catalog/
run.py
my_app/
__init__.py
首先,我们从flask_app/run.py开始。这是我们在这本书的多个食谱中之前读到的常规run文件:
from my_app import app
app.run(debug=True)
然后,我们配置我们的应用程序配置文件,flask_app/my_app/__init__.py:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =
'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
with app.app_context():
db.create_all()
在这里,我们首先配置我们的应用程序,将SQLALCHEMY_DATABASE_URI指向一个特定位置。然后,我们创建一个名为db的SQLAlchemy对象。正如其名所示,这个对象将处理我们所有的 ORM 相关活动。如前所述,这个对象有一个名为Model的类,它为 Flask 中的模型创建提供了基础。任何类都可以通过继承Model类来创建模型,这些模型将作为数据库表。
现在,如果我们在一个浏览器中打开http://127.0.0.1:5000 URL,我们将什么也看不到。这是因为我们刚刚为这个应用程序配置了数据库连接,浏览器上没有东西可以显示。然而,你总是可以前往app.config中指定的数据库位置,以查看新创建的test.db文件。
更多...
有时,你可能希望单个 SQLAlchemy db实例在多个应用程序中使用,或者动态创建应用程序。在这种情况下,将db实例绑定到单个应用程序不是首选的。在这里,你必须与应用程序上下文一起工作以实现预期的结果。
在这种情况下,注册应用程序与 SQLAlchemy 的方式如下:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
db.init_app(app)
return app
小贴士
在初始化应用程序时,可以使用任何 Flask 扩展采取这种方法,这在处理实际应用程序时非常常见。
现在,所有之前通过db实例在全局范围内可能进行的操作都需要始终在 Flask 应用程序上下文中进行。
Flask 应用程序上下文如下:
>>> from my_app import create_app
>>> app = create_app()
>>> app.test_request_context().push()
>>> # Do whatever needs to be done
>>> app.test_request_context().pop()
或者,你可以使用上下文管理器,如下所示:
with app():
# We have flask application context now till we are inside the with block
参见
接下来的几个食谱将扩展当前应用程序以使其成为一个完整的应用程序,这将帮助我们更好地理解 ORM 层。
创建基本的产品模型
在这个菜谱中,我们将创建一个应用程序,帮助我们存储要在网站目录部分显示的产品。应该能够添加产品到目录,并在需要时删除它们。正如你在上一章中看到的,这也可以使用非持久存储来完成。然而,在这里,我们将数据存储在数据库中以实现持久存储。
如何做到这一点…
新的目录布局如下所示:
flask_catalog/
run.py
my_app/
__init__.py
catalog/
__init__.py
views.py
models.py
首先,从修改应用程序配置文件flask_catalog/my_app/__init__.py开始:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =
'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
from my_app.catalog.views import catalog
app.register_blueprint(catalog)
with app.app_context():
db.create_all()
文件中的最后一个语句是db.create_all(),它告诉应用程序创建指定数据库中的所有表。因此,一旦应用程序运行,如果表中还没有数据,所有表都将被创建。由于你现在不在应用程序请求中,请使用with app.app_context():手动创建上下文。
现在是时候创建位于flask_catalog/my_app/catalog/models.py中的模型了:
from my_app import db
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
price = db.Column(db.Float)
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return '<Product %d>' % self.id
在这个文件中,我们创建了一个名为Product的模型,它有三个字段,即id、name和price。id是数据库中自动生成的字段,它将存储记录的 ID,是主键。name是string类型的字段,而price是float类型的字段。
现在,为视图添加一个新文件,该文件位于lask_catalog/my_app/catalog/views.py。在这个文件中,我们有多个视图方法,它们控制我们如何处理产品模型和整个 Web 应用程序。
from flask import request, jsonify, Blueprint
from my_app import db
from my_app.catalog.models import Product
catalog = Blueprint('catalog', __name__)
@catalog.route('/')
@catalog.route('/home')
def home():
return "Welcome to the Catalog Home."
上述方法处理主页或应用程序着陆页的外观或对用户的响应。你可能会想在你的应用程序中使用模板来渲染它。我们将在下一章中介绍。
看看下面的代码:
@catalog.route('/product/<id>')
def product(id):
product = Product.query.get_or_404(id)
return 'Product - %s, $%s' % (product.name,
product.price)
上述方法控制当用户使用其 ID 查找特定产品时显示的输出。我们使用 ID 过滤产品,如果找到产品,则返回其信息,否则以404错误终止。
考虑以下代码:
@catalog.route('/products')
def products():
products = Product.query.all()
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': str(product.price)
}
return jsonify(res)
上述方法以 JSON 格式返回数据库中所有产品的列表。如果没有找到产品,它将简单地返回一个空的 JSON:{}。
考虑以下代码:
@catalog.route('/product-create', methods=['POST',])
def create_product():
name = request.form.get('name')
price = request.form.get('price')
product = Product(name, price)
db.session.add(product)
db.session.commit()
return 'Product created.'
上述方法控制数据库中产品的创建。我们首先从request对象中获取信息,然后根据这些信息创建一个Product实例。
然后,我们将这个Product实例添加到数据库会话中,最后使用commit将记录保存到数据库中。
它是如何工作的...
在开始时,数据库是空的,没有产品。这可以通过在浏览器中打开http://127.0.0.1:5000/products来确认。这将导致一个空的 JSON 响应,或{}。
现在,首先,我们想要创建一个产品。为此,我们需要发送一个POST请求,这可以通过使用requests库从 Python 提示符轻松发送:
>>> import requests
>>> requests.post('http://127.0.0.1:5000/product-create', data={'name': 'iPhone 5S', 'price': '549.0'})
为了确认产品是否现在在数据库中,我们可以在浏览器中再次打开 http://127.0.0.1:5000/products。这次,它将显示产品详情的 JSON 输出,看起来像这样:
{
"1": {
"name": "iPhone 5S",
"price": "549."
}
}
创建一个关系型分类模型
在我们之前的菜谱中,我们创建了一个简单的产品模型,它有几个字段。然而,在实践中,应用程序要复杂得多,并且它们之间的表之间存在各种关系。这些关系可以是一对一、一对多、多对一或多对多。在这个菜谱中,我们将通过一个例子来尝试理解这些关系之一。
如何做到这一点...
假设我们想要有产品分类,其中每个分类可以拥有多个产品,但每个产品只能有一个分类。让我们通过修改上一道菜谱中的应用程序中的某些文件来实现这一点。我们将对模型和视图进行修改。在模型中,我们将添加一个 Category 模型,在视图中,我们将添加处理分类相关调用的新方法,并修改现有方法以适应新添加的功能。
首先,修改 models.py 文件以添加 Category 模型并对 Product 模型进行一些修改:
from my_app import db
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
price = db.Column(db.Float)
category_id = db.Column(db.Integer,
db.ForeignKey('category.id'))
category = db.relationship(
'Category', backref=db.backref('products',
lazy='dynamic')
)
def __init__(self, name, price, category):
self.name = name
self.price = price
self.category = category
def __repr__(self):
return '<Product %d>' % self.id
在先前的 Product 模型中,检查新添加的 category_id 和 category 字段。category_id 是到 Category 模型的外键,而 category 代表关系表。从定义本身可以看出,其中一个是关系,另一个使用这个关系在数据库中存储外键值。这是一个从 product 到 category 的简单多对一关系。注意 category 字段中的 backref 参数;这个参数允许我们通过在视图中写入 category.products 这样简单的语句来从 Category 模型访问产品。这就像从另一端的一个一对多关系。
重要信息
仅在模型中添加字段并不会立即反映到数据库中。你可能需要删除整个数据库然后重新运行应用程序,或者运行迁移,这将在下一道菜谱中介绍,即 使用 Alembic 和 Flask-Migrate 迁移数据库。
对于 SQLite,你可以简单地删除在初始化应用程序时创建的数据库文件。
创建一个只有一个名为 name 字段的 Category 模型:
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
def __init__(self, name):
self.name = name
def __repr__(self):
return '<Category %d>' % self.id
现在,修改 views.py 以适应模型中的更改。首先在 products() 方法中进行第一次更改:
from my_app.catalog.models import Product, Category
@catalog.route('/products')
def products():
products = Product.query.all()
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name
}
return jsonify(res)
在这里,我们只做了一项修改,即在产品的 JSON 数据中发送 category 名称,该数据在向先前的端点发出请求时生成并作为响应返回。
修改 create_product() 方法,在创建产品之前先查找分类:
@catalog.route('/product-create', methods=['POST',])
def create_product():
name = request.form.get('name')
price = request.form.get('price')
categ_name = request.form.get('category')
category =
Category.query.filter_by(name=categ_name).first()
if not category:
category = Category(categ_name)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
return 'Product created.'
在这里,我们首先将在请求中的类别名称中搜索现有的类别。如果找到现有类别,我们将使用它在产品创建中;如果没有找到,我们将创建一个新的类别。
创建一个新的方法,create_category(),用于处理类别的创建:
@catalog.route('/category-create', methods=['POST',])
def create_category():
name = request.form.get('name')
category = Category(name)
db.session.add(category)
db.session.commit()
return 'Category created.'
上述代码是一个相对简单的方法,用于使用请求中提供的名称创建一个类别。
创建一个新的方法,categories(),用于处理所有类别及其对应产品的列表:
@catalog.route('/categories')
def categories():
categories = Category.query.all()
res = {}
for category in categories:
res[category.id] = {
'name': category.name
}
for product in category.products:
res[category.id]['products'] = {
'id': product.id,
'name': product.name,
'price': product.price
}
return jsonify(res)
上述方法做了一些稍微复杂的事情。在这里,我们从数据库中检索了所有类别,然后对每个类别,我们检索了所有产品,然后以 JSON 格式返回所有数据。
工作原理...
本食谱的工作原理与上一个食谱,创建基本 产品模型,非常相似。
要创建一个具有类别的产品,向 /product-create 端点发送一个 POST 请求:
>>> import requests
>>> requests.post('http://127.0.0.1:5000/product-create', data={'name': 'iPhone 5S', 'price': '549.0', 'category': 'Phones'})
要查看从数据库中检索到的数据现在看起来如何,请在您的浏览器中打开 http://127.0.0.1:5000/categories:
{
"1": {
"name": "Phones",
"products": {
"id": 1,
"name": "iPhone 5S",
"price": 549.0
}
}
}
参见
参考创建基本产品模型的食谱,以了解本食谱的上下文以及本食谱如何为浏览器工作,因为其工作原理与上一个食谱非常相似。
使用 Alembic 和 Flask-Migrate 迁移数据库
更新数据库模式是所有应用程序的重要用例,因为它涉及添加或删除表和/或列,或更改列类型。一种方法是通过 db.drop_all() 和 db.create_all() 删除数据库,然后创建一个新的数据库。然而,这种方法不能用于生产环境或预发布环境。我们希望将数据库迁移到与最新更新的模型匹配,同时保留所有数据完整。
对于此,我们拥有 Alembic,这是一个基于 Python 的数据库迁移管理工具,它使用 SQLAlchemy 作为底层引擎。Alembic 在很大程度上提供了自动迁移,但也存在一些限制(当然,我们不可能期望任何工具都能无缝运行)。作为锦上添花的部分,我们还有一个名为 Flask-Migrate 的 Flask 扩展,它进一步简化了迁移过程。在本食谱中,我们将介绍使用 Alembic 和 Flask-Migrate 进行数据库迁移的基本技术。
准备工作
首先,运行以下命令来安装 Flask-Migrate:
$ pip install Flask-Migrate
这也将安装 Alembic 以及其他许多依赖项。
如何操作...
要启用迁移,我们需要稍微修改我们的应用程序定义。让我们了解如果我们为我们的 catalog 应用程序进行相同的修改,这样的配置将如何出现:
以下代码行显示了 my_app/__init__.py 的外观:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =
'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from my_app.catalog.views import catalog
app.register_blueprint(catalog)
with app.app_context():
db.create_all()
如果我们在以脚本方式运行 flask 命令时传递 --help,终端将显示所有可用选项,如下面的截图所示:

图 3.1 – 数据库迁移选项
要初始化迁移,运行 init 命令:
$ flask db init
重要信息
要使迁移命令生效,Flask 应用程序应该是可定位的;否则,您将收到以下错误:
错误:无法定位 Flask 应用程序。使用'flask --app'选项,'FLASK_APP'环境变量,或在当前目录中的'wsgi.py'或'app.py'文件。
在我们的情况下,只需将 Flask 应用程序导出到环境变量中:
export FLASK_APP="my_app.__init__.py"
或者,简单地使用以下命令:
export FLASK_APP=my_app
一旦对模型进行了更改,请调用migrate命令:
$ flask db migrate
要使更改反映在数据库中,请调用upgrade命令:
$ flask db upgrade
它是如何工作的...
现在,假设我们修改了product表的模型,添加了一个名为
company,如下所示:
class Product(db.Model):
# Same Product model as last recipe
# ...
company = db.Column(db.String(100))
migrate的结果将类似于以下片段:
$ flask db migrate
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added column 'product.company'
Generating
<path/to/application>/flask_catalog/migrations/versions/2c08f71f9253_.py
... done
在前面的代码中,我们可以看到 Alembic 将新模型与数据库表进行比较,并检测到product表(由Product模型创建)中新增的company列。
类似地,upgrade的输出将类似于以下片段:
$ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade None -> 2c08f71f9253, empty message
在这里,Alembic 对之前检测到的迁移进行数据库升级。我们可以看到前一个输出中的十六进制代码。这代表了迁移执行的修订版本。这是 Alembic 内部用于跟踪数据库表更改的。
参见
请参考创建基本产品模型配方,了解此配方中关于product的目录模型的上下文。
使用 Redis 索引模型数据
可能有一些我们想要实现但不想为它们提供持久存储的功能。在这种情况下,将这些功能暂时存储在类似缓存存储中是一个很好的方法 - 例如,当我们想在网站上向访客显示最近查看的产品列表时。在这个配方中,我们将了解如何使用 Redis 作为有效的缓存来存储非持久数据,这些数据可以以高速访问。
准备工作
我们将使用 Redis 来完成此操作,可以使用以下命令安装:
$ pip install redis
确保您运行 Redis 服务器以建立连接。有关安装和运行 Redis 服务器的信息,请参阅redis.io/topics/quickstart。
然后,我们需要保持与 Redis 的连接打开。这可以通过在my_app/__init__.py中添加以下代码行来实现:
from redis import Redis
redis = Redis()
我们可以在应用程序文件中这样做,在那里我们将定义应用程序,或者在视图文件中,我们将使用它。建议您在应用程序文件中这样做,因为这样,连接将贯穿整个应用程序,并且只需导入所需的redis对象即可使用。
如何操作...
我们将在 Redis 中维护一个set,它将存储最近访问过的产品。每当访问产品时,它将被填充。条目将在 10 分钟后过期。此更改在views.py中:
@catalog.route('/product/<id>')
def product(id):
product = Product.query.get_or_404(id)
product_key = 'product-%s' % product.id
redis.set(product_key, product.name)
redis.expire(product_key, 600)
return 'Product - %s, $%s' % (product.name,
product.price)
在前面的方法中,注意redis对象上的set()和expire()方法。首先,使用 Redis 存储中的product_key值设置产品 ID。然后,将键的expire时间设置为600秒。
小贴士
最好从配置值中获取expire时间——即600。这可以在my_app/__init__.py中的应用程序对象上设置,然后从那里获取。
现在,我们将查找缓存中仍然存在的键,然后获取与这些键对应的产品并返回它们:
@catalog.route('/recent-products')
def recent_products():
keys_alive = redis.keys('product-*')
products = [redis.get(k).decode('utf-8') for k in
keys_alive]
return jsonify({'products': products})
它是如何工作的...
每当用户访问产品时,都会向存储中添加一个条目,并且该条目将保留 600 秒(10 分钟)。现在,除非再次访问,否则此产品将在接下来的 10 分钟内列在最近产品列表中,这将会再次将时间重置为 10 分钟。
要测试这一点,向您的数据库添加一些产品:
>>> requests.post('http://127.0.0.1:5000/product-create', data={'name': 'iPhone 5S', 'price': '549.0', 'category': 'Phones'})
>>> requests.post('http://127.0.0.1:5000/product-create', data={'name': 'iPhone 13', 'price': '799.0', 'category': 'Phones'})
>>> requests.post('http://127.0.0.1:5000/product-create', data={'name': 'iPad Pro', 'price': '999.0', 'category': 'Tablets'})
>>> requests.post('http://127.0.0.1:5000/product-create', data={'name': 'iPhone 5S', 'price': '549.0', 'category': 'Phones'})
然后,通过在浏览器中打开产品 URL 来简单地访问一些产品——例如,http://127.0.0.1:5000/product/1和http://127.0.0.1:5000/product/3。
现在,在浏览器中打开http://127.0.0.1:5000/recent-products以查看最近产品的列表:
{
"products": [
"iPad Pro",
"iPhone 5S"
]
}
选择 MongoDB 的 NoSQL 方式
有时,我们在构建的应用程序中要使用的数据可能根本不是结构化的;它可能是半结构化的,或者可能有一些数据其模式随时间频繁变化。在这种情况下,我们会避免使用 RDBMS,因为它增加了痛苦,并且难以扩展和维护。对于这种情况,最好使用 NoSQL 数据库。
此外,由于当前流行的开发环境中的快速和快速开发,设计完美的模式并不总是可能的。NoSQL 提供了修改模式而无需太多麻烦的灵活性。
在生产环境中,数据库通常会在一段时间内增长到巨大的规模。这极大地影响了整个系统的性能。有垂直和水平扩展技术可用,但有时它们可能非常昂贵。在这种情况下,可以考虑使用 NoSQL 数据库,因为它从头开始就是为了类似的目的而设计的。NoSQL 数据库能够在大型多个集群上运行并处理以高速生成的大量数据,这使得它们在处理传统 RDBMS 的扩展问题时成为一个不错的选择。
在这个菜谱中,我们将使用 MongoDB 来学习如何将 NoSQL 与 Flask 集成。
准备工作
有许多扩展可用于使用 Flask 与 MongoDB。我们将使用Flask-MongoEngine,因为它提供了良好的抽象级别,这使得它易于理解。可以使用以下命令进行安装:
$ pip install flask-mongoengine
记得运行 MongoDB 服务器以建立连接。有关安装和运行 MongoDB 的更多详细信息,请参阅docs.mongodb.org/manual/installation/。
如何操作...
首先,使用命令行在 MongoDB 中手动创建一个数据库。让我们把这个数据库命名为 my_catalog:
>>> mongosh
Current Mongosh Log ID: 62fa8dtfd435df654150997b
Connecting to: mongodb://127.0.0.1:27017/?directConnection
=true&serverSelectionTimeoutMS=2000&appName=mongosh+1.5.4
Using MongoDB: 6.0.0
Using Mongosh: 1.5.4
test> use my_catalog
switched to db my_catalog
以下是我们使用 MongoDB 重写的目录应用程序。第一个变化出现在我们的配置文件 my_app/__init__.py 中:
from flask import Flask
from flask_mongoengine import MongoEngine
app = Flask(__name__)
app.config['MONGODB_SETTINGS'] = {'DB': 'my_catalog'}
app.debug = True
db = MongoEngine(app)
from my_app.catalog.views import catalog
app.register_blueprint(catalog)
信息
注意,我们不再使用通常以 SQLAlchemy 为中心的设置,我们现在有 MONGODB_SETTINGS。在这里,我们只需指定要使用的数据库名称,在我们的例子中是 my_catalog。
接下来,我们将使用 MongoDB 字段创建一个 Product 模型。这通常在模型文件 my_app/catalog/models.py 中完成:
import datetime
from my_app import db
class Product(db.Document):
created_at = db.DateTimeField(
default=datetime.datetime.now, required=True
)
key = db.StringField(max_length=255, required=True)
name = db.StringField(max_length=255, required=True)
price = db.DecimalField()
def __repr__(self):
return '<Product %r>' % self.id
重要信息
现在是查看用于创建前面模型的 MongoDB 字段及其与之前配方中使用的 SQLAlchemy 字段相似性的好时机。在这里,我们有一个 key 字段而不是 ID 字段,它存储将用于唯一标识记录的唯一标识符。此外,请注意在创建模型时 Product 继承的类。在 SQLAlchemy 的情况下,它是 db.Model,而在 MongoDB 的情况下,它是 db.Document。这符合这些数据库系统的工作方式。SQLAlchemy 与传统的 RDBMS 一起工作,但 MongoDB 是一个 NoSQL 文档数据库系统。
以下是我们文件,即 my_app/catalog/views.py:
from decimal import Decimal
from flask import request, Blueprint, jsonify
from my_app.catalog.models import Product
catalog = Blueprint('catalog', __name__)
@catalog.route('/')
@catalog.route('/home')
def home():
return "Welcome to the Catalog Home."
@catalog.route('/product/<key>')
def product(key):
product = Product.objects.get_or_404(key=key)
return 'Product - %s, $%s' % (product.name,
product.price)
@catalog.route('/products')
def products():
products = Product.objects.all()
res = {}
for product in products:
res[product.key] = {
'name': product.name,
'price': str(product.price),
}
return jsonify(res)
@catalog.route('/product-create', methods=['POST',])
def create_product():
name = request.form.get('name')
key = request.form.get('key')
price = request.form.get('price')
product = Product(
name=name,
key=key,
price=Decimal(price)
)
product.save()
return 'Product created.'
你会注意到它与为基于 SQLAlchemy 的模型创建的视图非常相似。只是从 MongoEngine 扩展调用的方法有一些细微的差别,这些应该很容易理解。
它是如何工作的...
首先,通过使用 /product-create 端点将产品添加到数据库中:
>>> res = requests.post('http://127.0.0.1:5000/product-create', data={'key': 'iphone-5s', 'name': 'iPhone 5S', 'price': '549.0'})
现在,通过在浏览器中访问 http://127.0.0.1:5000/products 端点来验证产品添加。以下是将得到的 JSON 值:
{
"iphone-5s": {
"name": "iPhone 5S",
"price": "549.00"
}
}
参见
参考创建基本产品模型的配方,了解此应用程序的结构。
第四章:与视图一起工作
对于任何 Web 应用程序,控制你如何与 Web 请求交互以及为这些请求提供适当的响应是非常重要的。本章将引导我们通过正确处理请求和以最佳方式设计它们的各种方法。
Flask 为我们提供了几种设计和布局 URL 路由的方法。它还提供了灵活性,使我们能够保持视图的架构仅为函数,或者创建类,这些类可以根据需要继承和修改。在早期版本中,Flask 只有基于函数的视图。然而,后来在 0.7 版本中,受到 Django 的启发,Flask 引入了可插拔视图的概念,这允许我们拥有类,并在这些类中编写方法。这也使得构建 RESTful API 的过程非常直接,每个 HTTP 方法都由相应的类方法处理。此外,我们总是可以更深入地了解 Werkzeug 库,并使用更灵活但稍微复杂的概念——URL 映射。实际上,大型应用程序和框架更喜欢使用 URL 映射。
在本章中,我们将介绍以下菜谱:
-
编写基于函数的视图和 URL 路由
-
编写基于类的视图
-
实现 URL 路由和基于产品的分页
-
渲染到模板
-
处理 XHR 请求
-
使用装饰器优雅地处理请求
-
创建自定义 4xx 和 5xx 错误处理器
-
显示消息以提供更好的用户反馈
-
实现基于 SQL 的搜索
编写基于函数的视图和 URL 路由
这是编写 Flask 中视图和 URL 路由的最简单方法。我们只需编写一个方法,并用端点装饰它。在这个菜谱中,我们将为GET和POST请求编写几个 URL 路由。
准备工作
要通过这个菜谱,我们可以从任何 Flask 应用程序开始。该应用程序可以是新的、空的或复杂的。我们只需要理解这个菜谱中概述的方法。
如何做到...
以下部分通过小示例解释了三种最广泛使用的不同类型的请求。
简单的 GET 请求
以下是一个简单的GET请求示例:
@app.route('/a-get-request')
def get_request():
bar = request.args.get('foo', 'bar')
return 'A simple Flask request where foo is %s' % bar
在这里,我们只是检查 URL 查询中是否有名为foo的参数。如果有,我们在响应中显示它;否则,默认为bar。
简单的 POST 请求
POST请求与GET请求类似,但有一些不同:
@app.route('/a-post-request', methods=['POST'])
def post_request():
bar = request.form.get('foo', 'bar')
return 'A simple Flask request where foo is %s' % bar
路由现在包含一个额外的参数,称为methods。此外,我们不再使用request.args,而是现在使用request.form,因为POST假设数据是以表单的形式提交的。
简单的 GET/POST 请求
将GET和POST合并到一个单独的view函数中,可以像下面这样编写:
@app.route('/a-request', methods=['GET', 'POST'])
def some_request():
if request.method == 'GET':
bar = request.args.get('foo', 'bar')
else:
bar = request.form.get('foo', 'bar')
return 'A simple Flask request where foo is %s' % bar
它是如何工作的...
让我们尝试理解前面方法的这种玩法。
默认情况下,任何 Flask 视图 函数仅支持 GET 请求。为了支持或处理任何其他类型的请求,我们必须明确告诉我们的 route() 装饰器我们想要支持的方法。这正是我们在上一个 POST 和 GET/POST 方法中做的事情。
对于 GET 请求,request 对象将寻找 args(即 request.args.get()),而对于 POST,它将寻找 form(即 request.form.get())。
此外,如果我们尝试对一个仅支持 POST 的方法发起 GET 请求,请求将因 405 HTTP 错误而失败。对所有方法都适用。请参考以下截图:

图 4.1 – 方法不允许错误页面
还有更多...
有时,我们可能希望有一个类似 URL 映射的模式,我们更愿意在一个地方定义所有 URL 规则,而不是让它们散布在整个应用程序中。为此,我们需要在不使用 route() 装饰器的情况下定义我们的方法,并在我们的应用程序对象上定义路由,如下所示:
def get_request():
bar = request.args.get('foo', 'bar')
return 'A simple Flask request where foo is %s' % bar
app = Flask(__name__)
app.add_url_rule('/a-get-request', view_func=get_request)
确保你给出了分配给 view_func 的方法的正确相对路径。
编写基于类的视图
Flask 在 版本 0.7 中引入了可插拔视图的概念;这为现有的实现增加了许多灵活性。我们可以以类的形式编写视图;这些视图可以以通用方式编写,并允许易于理解和继承。在本例中,我们将探讨如何创建此类基于类的视图。
准备工作
请参考之前的配方,编写基于函数的视图和 URL 路由,以首先了解基本的基于函数的视图。
如何实现...
Flask 提供了一个名为 View 的类,可以继承以添加我们的自定义行为。以下是一个简单的 GET 请求示例:
from flask.views import View
class GetRequest(View):
def dispatch_request(self):
bar = request.args.get('foo', 'bar')
return 'A simple Flask request where foo is %s' %
bar
app.add_url_rule(
'/a-get-request',
view_func=GetRequest.as_view('get_request')
)
在 as_view 中提供的视图名称(即 get_request)表示在 url_for() 中引用此端点时将使用的名称。
为了同时支持 GET 和 POST 请求,我们可以编写以下代码:
class GetPostRequest(View):
methods = ['GET', 'POST']
def dispatch_request(self):
if request.method == 'GET':
bar = request.args.get('foo', 'bar')
if request.method == 'POST':
bar = request.form.get('foo', 'bar')
return 'A simple Flask request where foo is %s' %
bar
app.add_url_rule(
'/a-request',
view_func=GetPostRequest.as_view('a_request')
)
它是如何工作的...
我们知道默认情况下,任何 Flask 视图 函数仅支持 GET 请求。基于类的视图也是如此。为了支持或处理任何其他类型的请求,我们必须通过一个名为 methods 的类属性,明确告诉我们的类我们想要支持的 HTTP 方法。这正是我们在上一个 GET/POST 请求示例中所做的。
对于 GET 请求,request 对象将寻找 args(即 request.args.get()),而对于 POST,它将寻找 form(即 request.form.get())。
此外,如果我们尝试对一个仅支持 POST 的方法发起 GET 请求,请求将因 405 HTTP 错误而失败。对所有方法都适用。
还有更多...
现在,你们中的许多人可能正在考虑是否可以在一个View类内部仅声明GET和POST方法,并让 Flask 处理其余的事情。这个问题的答案是MethodView。让我们用MethodView重写之前的代码片段:
from flask.views import MethodView
class GetPostRequest(MethodView):
def get(self):
bar = request.args.get('foo', 'bar')
return 'A simple Flask request where foo is %s' %
bar
def post(self):
bar = request.form.get('foo', 'bar')
return 'A simple Flask request where foo is %s' %
bar
app.add_url_rule(
'/a-request',
view_func=GetPostRequest.as_view('a_request')
)
参见
请参考之前的配方,基于函数的视图和 URL 路由的编写,以了解基于类和基于函数的视图之间的区别。
实现 URL 路由和基于产品的分页
有时,我们可能会遇到需要以不同方式解析 URL 各个部分的问题。例如,一个 URL 可以有一个整数部分,一个字符串部分,一个特定长度的字符串部分,以及 URL 中的斜杠。我们可以使用 URL 转换器在我们的 URL 中解析所有这些组合。在这个配方中,我们将看到如何做到这一点。此外,我们还将学习如何使用Flask-SQLAlchemy扩展实现分页。
准备工作
我们已经在本书中看到了几个基本 URL 转换器的实例。在这个配方中,我们将查看一些高级 URL 转换器,并学习如何使用它们。
如何做到...
假设我们有一个如下定义的 URL 路由:
@app.route('/test/<name>')
def get_name(name):
return name
在这里,URL http://127.0.0.1:5000/test/Shalabh 将导致Shalabh被解析并传递给get_name方法的name参数。这是一个 Unicode 或字符串转换器,它是默认的,不需要明确指定。
我们还可以有特定长度的字符串。假设我们想要解析一个可能包含国家代码或货币代码的 URL。国家代码通常是两个字符长,货币代码通常是三个字符长。可以这样做:
@app.route('/test/<string(minlength=2,maxlength=3):code>')
def get_name(code):
return code
这将匹配 URL 中的US和USD – 即,http://127.0.0.1:5000/test/USD 和 http://127.0.0.1:5000/test/US 将被类似处理。我们也可以使用length参数而不是minlength和maxlength来匹配确切的长度。
我们也可以以类似的方式解析整数值:
@app.route('/test/<int:age>')
def get_age(age):
return str(age)
我们还可以指定可以接受的最小和最大值。例如,为了限制可接受年龄在 18 到 99 岁之间,URL 可以被结构化为@app.route('/test/<int(min=18,max=99):age>')。我们也可以使用float代替先前的例子中的int来解析浮点值。
让我们了解分页的概念。在第三章的Flask 中的数据建模配方创建基本产品模型中,我们创建了一个处理程序来列出我们数据库中的所有产品。如果我们有数千个产品,那么一次性生成所有这些产品的列表可能会花费很多时间。此外,如果我们需要在模板中渲染这些产品,我们就不想在一次显示 10-20 个产品。分页在构建优秀应用程序时证明是一个很大的帮助。
让我们修改products()方法以列出产品并支持分页:
@catalog.route('/products')
@catalog.route('/products/<int:page>')
def products(page=1):
products = Product.query.paginate(page, 10).items
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name
}
return jsonify(res)
在前面的处理器中,我们添加了一个新的 URL 路由,将 page 参数添加到 URL 中。现在,http://127.0.0.1:5000/products URL 将与 http://127.0.0.1:5000/products/1 相同,并且两者都将返回数据库中的前 10 个产品列表。http://127.0.0.1:5000/products/2 URL 将返回下一个 10 个产品,依此类推。
信息
paginate() 方法接受四个参数,并返回一个 Pagination 类的对象。这四个参数如下:
• page: 这是将要列出当前页。
• per_page: 这是每页要列出的项目数量。
• error_out: 如果页面没有找到任何项目,则将使用 404 错误终止。为了防止这种行为,将此参数设置为 False,然后它将只返回一个空列表。
• max_per_page: 如果指定了此值,则 per_page 将限制为相同的值。
参见
参考第 3 章,Flask 中的数据建模中的 创建基本产品模型 菜谱,以了解此菜谱中分页的上下文,因为此菜谱是在其基础上构建的。
渲染到模板
在编写视图之后,我们当然希望在一个模板中渲染内容,并从底层数据库中获取信息。
准备工作
要渲染模板,我们将使用 Jinja 作为模板语言。请参阅第 2 章,使用 Jinja2 进行模板化,以深入了解模板化。
如何做...
我们将再次参考之前菜谱中的现有目录应用程序。让我们修改我们的视图以渲染模板,并在这些模板中显示数据库中的数据。
以下是对 views.py 代码和模板的修改。完整的应用程序可以从本书提供的代码包或 GitHub 仓库中下载。
我们将首先修改我们的视图——即 flask_catalog_template/my_app/catalog/views.py ——以在特定的处理器上渲染模板:
from flask import request, Blueprint, render_template
from my_app import db
from my_app.catalog.models import Product, Category
catalog = Blueprint('catalog', __name__)
@catalog.route('/')
@catalog.route('/home')
def home():
return render_template('home.html')
注意 render_template() 方法。当调用 home 处理器时,此方法将渲染 home.html。
以下方法处理在模板上下文中使用 product 对象渲染 product.html:
@catalog.route('/product/<id>')
def product(id):
product = Product.query.get_or_404(id)
return render_template('product.html', product=product)
要获取所有产品的分页列表,请参阅以下方法:
@catalog.route('/products')
@catalog.route('/products/<int:page>')
def products(page=1):
products = Product.query.paginate(page, 10)
return render_template('products.html',
products=products)
在这里,products.html 模板将使用上下文中的分页 product 对象列表进行渲染。
要在创建新产品时渲染产品模板,可以修改 create_product() 方法,如下所示:
@catalog.route('/product-create', methods=['POST',])
def create_product():
# ...Same code as before ...
return render_template('product.html', product=product)
这也可以使用 redirect() 来完成,但我们将在稍后阶段介绍。请看以下代码:
@catalog.route('/category-create', methods=['POST',])
def create_category():
# ...Same code as before ...
return render_template('category.html',
category=category)
@catalog.route('/category/<id>')
def category(id):
category = Category.query.get_or_404(id)
return render_template('category.html',
category=category)
@catalog.route('/categories')
def categories():
categories = Category.query.all()
return render_template(
'categories.html', categories=categories)
前面的代码中的三个处理器都以类似的方式工作,正如之前在渲染与产品相关的模板时讨论的那样。
以下都是作为应用程序一部分创建和渲染的模板。有关这些模板的编写方式和它们的工作原理的更多信息,请参阅第二章,使用 Jinja2 进行模板化。
第一个模板文件是 flask_catalog_template/my_app/templates/base.html,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,
initial-scale=1">
<title>Flask Framework Cookbook</title>
<link href="{{ url_for('static', filename
='css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename
='css/main.css') }}" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top"
role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="{{ url_for
('catalog.home') }}">Flask Cookbook</a>
</div>
</div>
</div>
<div class="container">
{% block container %}{% endblock %}
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript
plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs
/jquery/2.0.0/jquery.min.js"></script>
<script src="{{ url_for('static', filename
='js/bootstrap.min.js') }}"></script>
</body>
</html>
文件 flask_catalog_template/my_app/templates/home.html 的内容如下:
{% extends 'base.html' %}
{% block container %}
<h1>Welcome to the Catalog Home</h1>
<a href="{{ url_for('catalog.products') }}">Click here to
see the catalog</a>
{% endblock %}
文件 flask_catalog_template/my_app/templates/product.html 的内容如下:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<h1>{{ product.name }}<small> {{ product.category.name
}}</small></h1>
<h4>{{ product.company }}</h4>
<h3>{{ product.price }}</h3>
</div>
{% endblock %}
文件 flask_catalog_template/my_app/templates/products.html 的内容如下:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
{% for product in products.items %}
<div class="well">
<h2>
<a href="{{ url_for('catalog.product', id
=product.id) }}">{{ product.name }}</a>
<small>$ {{ product.price }}</small>
</h2>
</div>
{% endfor %}
{% if products.has_prev %}
<a href="{{ url_for(request.endpoint, page
=products.prev_num) }}">
{{"<< Previous Page"}}
</a>
{% else %}
{{"<< Previous Page"}}
{% endif %} |
{% if products.has_next %}
<a href="{{ url_for(request.endpoint, page
=products.next_num) }}">
{{"Next page >>"}}
</a>
{% else %}
{{"Next page >>"}}
{% endif %}
</div>
{% endblock %}
注意如何为 上一页 和 下一页 链接创建 URL。我们使用 request.endpoint 以确保分页适用于当前 URL,这将使模板与 search 一起可重用。我们将在本章后面看到这一点。
文件 flask_catalog_template/my_app/templates/category.html 的内容如下:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<h2>{{ category.name }}</h2>
<div class="well">
{% for product in category.products %}
<h3>
<a href="{{ url_for('catalog.product', id
=product.id) }}">{{ product.name }}</a>
<small>$ {{ product.price }}</small>
</h3>
{% endfor %}
</div>
</div>
{% endblock %}
文件 flask_catalog_template/my_app/templates/categories.html 的内容如下:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
{% for category in categories %}
<a href="{{ url_for('catalog.category',
id=category.id) }}">
<h2>{{ category.name }}</h2>
</a>
{% endfor %}
</div>
{% endblock %}
工作原理...
我们的 view 方法在最后调用了一个 render_template 方法。这意味着在方法操作成功完成后,我们将渲染一个模板,并在上下文中添加一些参数。
信息
注意在 products.html 文件中如何实现了分页。可以进一步改进,在两个导航链接之间显示页码。你应该自己完成这项工作。
参考信息
请参阅实现 URL 路由和基于产品的分页食谱,了解分页和本食谱中使用的应用程序的其余部分。
处理 XHR 请求
异步 JavaScript,通常称为Ajax,在过去十年左右的时间里已经成为网络应用程序的一个重要部分。浏览器中内置的XMLHttpRequest(XHR)对象用于在网页上执行 Ajax。随着单页应用程序和 JavaScript 应用程序框架如Angular、Vue和React的出现,这种网络开发技术呈指数级增长。在本食谱中,我们将实现一个 Ajax 请求,以促进后端和前端之间的异步通信。
注意
在这本书中,我选择使用 Ajax 来演示 async 请求,因为它更容易理解和演示,并且使本书的焦点保持在 Flask 上。你可以选择使用任何 JavaScript 平台/框架。Flask 代码将保持不变,而 JavaScript 代码将需要根据你使用的框架进行更改。
准备工作
Flask 提供了一个简单的方法来处理视图处理程序中的 XHR 请求。我们甚至可以为正常网络请求和 XHR 提供通用方法。我们只需检查 request 对象中的 XMLHttpRequest 标头,以确定调用类型并相应地操作。
我们将更新目录应用程序,从之前的食谱中添加一个功能来演示 XHR 请求。
如何做...
Flask 的 request 对象提供了一个检查浏览器发送的请求头部的功能。我们可以检查 X-Requested-With 头部以确定是否为 XMLHttpRequest,这告诉我们发出的请求是 XHR 请求还是简单的网页请求。通常,当我们有一个 XHR 请求时,调用者期望结果以 JSON 格式返回,这样就可以在网页上正确地渲染内容,而无需重新加载页面。
假设我们在主页本身发送一个 Ajax 调用来获取数据库中的产品数量。一种获取产品的方法是发送产品计数与 render_template() 上下文一起。另一种方法是作为 Ajax 调用的响应发送这些信息。我们将实现后者,看看 Flask 如何处理 XHR:
from flask import request, render_template, jsonify
@catalog.route('/')
@catalog.route('/home')
def home():
if request.headers.get("X-Requested-With") ==
"XMLHttpRequest":
products = Product.query.all()
return jsonify({
'count': len(products)
})
return render_template('home.html')
在前面的方法中,我们首先检查这是否是一个 XHR。如果是,我们返回 JSON 数据;否则,我们只渲染 home.html,就像我们之前所做的那样。
小贴士
当应用程序规模增长时,这种在一个方法中同时处理 XHR 和常规请求的设计可能会变得有些臃肿,因为在这种情况下,需要执行不同的逻辑处理,与常规请求相比。在这种情况下,这两种类型的请求可以被分离到不同的方法中,其中 XHR 的处理与常规请求分开。这甚至可以扩展到我们有不同的蓝图来使 URL 处理更加清晰。
接下来,修改 flask_catalog_template/my_app/templates/base.html,将其修改为 scripts 块。这个空块,如这里所示,可以放在包含 Bootstrap.js 脚本的行之后:
{% block scripts %}
{% endblock %}
接下来,我们查看 flask_catalog_template/my_app/templates/home.html,在这里我们向 home() 处理器发送一个 Ajax 调用,该处理器检查请求是否为 XHR 请求。如果是,它从数据库中获取产品计数并将其作为 JSON 对象返回。检查 scripts 块内的代码:
{% extends 'base.html' %}
{% block container %}
<h1>Welcome to the Catalog Home</h1>
<a href="{{ url_for('catalog.products') }}"
id="catalog_link">
Click here to see the catalog
</a>
{% endblock %}
{% block scripts %}
<script>
$(document).ready(function(){
$.getJSON("/home", function(data) {
$('#catalog_link').append('<span class="badge">' +
data.count + '</span>');
});
});
</script>
{% endblock %}
它是如何工作的...
现在,我们的主页包含一个徽章,显示数据库中的产品数量。这个徽章只有在整个页面加载完毕后才会加载。当数据库中有大量产品时,徽章与其他页面内容的加载差异将非常明显。
以下是一个截图,展示了当前主页的样子:

图 4.2 – 使用 AJAX 调用加载计数的主页
使用装饰器优雅地处理请求
有些人可能认为,像上一个菜谱中展示的那样,每次都检查请求是否为 XHR 会降低代码的可读性。为了解决这个问题,我们有一个简单的解决方案。在这个菜谱中,我们将编写一个简单的装饰器,可以为我们处理这些冗余代码。
准备工作
在这个菜谱中,我们将编写一个装饰器。对于一些 Python 初学者来说,这可能会感觉像是陌生的领域。如果是这样,请阅读legacy.python.org/dev/peps/pep-0318/以更好地理解装饰器。
如何做到...
以下是我们为这个菜谱编写的装饰器方法:
from functools import wraps
def template_or_json(template=None):
""""Return a dict from your view and this will either
pass it to a template or render json. Use like:
@template_or_json('template.html')
"""
def decorated(f):
@wraps(f)
def decorated_fn(*args, **kwargs):
ctx = f(*args, **kwargs)
if request.headers.get("X-Requested-With") ==
"XMLHttpRequest" or not template:
return jsonify(ctx)
else:
return render_template(template, **ctx)
return decorated_fn
return decorated
这个装饰器只是做了我们在上一个菜谱中处理 XHR 所做的事情——即检查我们的请求是否为 XHR,并根据结果,要么渲染模板,要么返回 JSON 数据。
现在,让我们将这个装饰器应用到我们的home()方法上,这个方法在上一个菜谱中处理了 XHR 调用:
@catalog.route('/')
@catalog.route('/home')
@template_or_json('home.html')
def home():
products = Product.query.all()
return {'count': len(products)}
参考以下内容
参考处理 XHR 请求的菜谱,了解这个菜谱如何改变编码模式。这个菜谱的参考来源于justindonato.com/notebook/template-or-json-decorator-for-flask.html。
创建自定义 4xx 和 5xx 错误处理程序
每个应用程序在某个时间点都会向用户抛出错误。这些错误可能是由于用户输入了不存在的 URL(404)、应用程序过载(500)或者某些用户无法访问的内容(403)。一个好的应用程序会以用户交互的方式处理这些错误,而不是显示一个丑陋的空白页面,这对大多数用户来说毫无意义。Flask 提供了一个易于使用的装饰器来处理这些错误。在这个菜谱中,我们将了解如何利用这个装饰器。
准备工作
Flask app对象有一个名为errorhandler()的方法,它使我们能够以更美观和高效的方式处理我们的应用程序的错误。
如何做到...
创建一个带有errorhandler()装饰器的方法,当发生404 Not Found错误时,渲染404.html模板:
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
以下代码行表示flask_catalog_template/my_app/templates/404.html模板,如果有任何404错误,则会渲染:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<h3>Hola Friend! Looks like in your quest you have
reached a location which does not exist yet.</h3>
<h4>To continue, either check your map location (URL)
or go back <a href="{{ url_for('catalog.home')
}}">home</a></h4>
</div>
{% endblock %}
它是如何工作的...
因此,现在,如果我们打开一个错误的 URL——例如,http://127.0.0.1:5000/i-am-lost——那么我们将得到以下截图所示的屏幕:

图 4.3 – 自定义错误处理页面
同样,我们也可以为其他错误代码添加更多的错误处理程序。
还有更多...
根据应用程序需求创建自定义错误,并将它们绑定到错误代码和自定义错误屏幕也是可能的。这可以按照以下方式完成:
class MyCustom404(Exception):
pass
@app.errorhandler(MyCustom404)
def special_page_not_found(error):
return render_template("errors/custom_404.html"), 404
闪存消息以提供更好的用户反馈
所有优秀 Web 应用程序的一个重要方面是向用户提供关于各种活动的反馈。例如,当用户创建一个产品并被重定向到新创建的产品时,告诉他们产品已经创建是一个好的做法。在这个菜谱中,我们将了解如何使用闪存消息作为用户良好的反馈机制。
准备工作
我们将首先将闪现消息功能添加到现有的目录应用程序中。我们还必须确保向应用程序添加一个密钥,因为会话依赖于它,如果没有密钥,应用程序在闪现时将出错。
如何操作...
为了演示消息的闪现,我们将在产品创建时闪现消息。
首先,我们将向flask_catalog_template/my_app/__init__.py中的应用配置添加一个密钥:
app.secret_key = 'some_random_key'
现在,我们将修改位于flask_catalog_template/my_app/catalog/views.py中的create_product()处理器,以便向用户显示有关产品创建的消息。
此外,对这一处理器也进行了另一项更改;现在,可以通过表单从网络界面创建产品。这一更改将使展示该菜谱的工作方式变得更加容易。
@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
if request.method == 'POST':
name = request.form.get('name')
price = request.form.get('price')
categ_name = request.form.get('category')
category = Category.query.filter_by(
name=categ_name).first()
if not category:
category = Category(categ_name)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
flash('The product %s has been created' % name,
'success')
return redirect(
url_for('catalog.product', id=product.id))
return render_template('product-create.html')
在前面的方法中,我们首先检查请求类型是否为POST。如果是,则像往常一样继续产品创建,或者渲染带有表单的新产品创建页面。请注意flash语句,它将在产品成功创建时提醒用户。flash()的第一个参数是要显示的消息,第二个参数是消息的类别。我们可以在message类别中使用任何合适的标识符。这可以用于以后确定要显示的警告消息类型。
添加了一个新的模板;该模板包含产品表单的代码。模板的路径将是flask_catalog_template/my_app/templates/product-create.html:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<form
class="form-horizontal"
method="POST"
action="{{ url_for('catalog.create_product') }}"
role="form">
<div class="form-group">
<label for="name" class="col-sm-2 control-
label">Name</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="name"
name="name">
</div>
</div>
<div class="form-group">
<label for="price" class="col-sm-2 control-
label">Price</label>
<div class="col-sm-10">
<input type="number" class="form-control"
id="price" name="price">
</div>
</div>
<div class="form-group">
<label for="category" class="col-sm-2 control-
label">Category</label>
<div class="col-sm-10">
<input type="text" class="form-control"
id="category" name="category">
</div>
</div>
<button type="submit" class="btn
btn-default">Submit</button>
</form>
</div>
{% endblock %}
我们还将修改我们的基本模板 – 即flask_catalog_template/my_app/templates/base.html – 以容纳闪现的消息。只需在container块之前的<div>容器内添加以下代码行:
<br/>
<div>
{% for category, message in
get_flashed_messages(with_categories=true) %}
<div class="alert alert-{{category}}
alert-dismissable">
<button type="button" class="close" data-dismiss
="alert" aria-hidden="true">×</button>
{{ message }}
</div>
{% endfor %}
</div>
信息
注意,在<div>容器中,我们已添加了一个机制来显示闪现的消息,该机制使用get_flashed_messages()在模板中检索闪现的消息。
它是如何工作的...
当访问http://127.0.0.1:5000/product-create时,将显示如下截图中的表单:

图 4.4 – 创建产品
填写表单并点击提交。这将导致带有顶部警告消息的常规产品页面:

图 4.5 – 成功创建产品时的闪现消息
实现基于 SQL 的搜索
在任何 Web 应用程序中,能够根据某些标准在数据库中搜索记录非常重要。在本菜谱中,我们将介绍如何在 SQLAlchemy 中实现基于 SQL 的基本搜索。相同的原理可以用于搜索任何其他数据库系统。
准备工作
我们从开始就在目录应用程序中实现了一些搜索功能。每次我们显示产品页面时,我们都会使用其 ID 搜索特定的产品。现在我们将将其提升到一个稍微高级的水平,并基于名称和类别进行搜索。
如何做到这一点...
以下是一种在目录应用程序中搜索名称、价格、公司和类别的搜索方法。我们可以搜索任何单一标准,或者多个标准(除了类别搜索,它只能单独搜索)。请注意,我们为不同的值有不同的表达式。对于price中的浮点值,我们可以进行等值搜索,而对于字符串,我们可以使用like进行搜索。此外,请注意category情况下join的实现。将此方法放在views文件中——即flask_catalog_template/my_app/catalog/views.py:
from sqlalchemy.orm import join
@catalog.route('/product-search')
@catalog.route('/product-search/<int:page>')
def product_search(page=1):
name = request.args.get('name')
price = request.args.get('price')
company = request.args.get('company')
category = request.args.get('category')
products = Product.query
if name:
products = products.filter(Product.name.like('%' +
name + '%'))
if price:
products = products.filter(Product.price == price)
if company:
products = products.filter(Product.company.like('%'
+ company + '%'))
if category:
products = products.select_from(join(Product,
Category)).filter(
Category.name.like('%' + category + '%')
)
return render_template(
'products.html', products=products.paginate(page,
10)
)
它是如何工作的...
我们可以通过输入一个 URL 来搜索产品,例如http://127.0.0.1:5000/product-search?name=iPhone。这将搜索名为iPhone的产品,并在products.html模板上列出结果。同样,我们可以根据需要搜索价格和/或公司或类别。尝试各种组合以帮助理解。
信息
我们使用了相同的产品列表页面来渲染我们的搜索结果。使用 Ajax 实现搜索将很有趣。我将把这个留给你自己来实现。
第二部分:Flask 深入探讨
一旦构建了基本的 Flask Web 应用程序,下一个问题就是创建美观且可重用的 Web 表单和身份验证。本部分的前两章专门讨论了这些主题。
作为一名开发者,你可以始终使用纯 HTML 构建 Web 表单,但这通常是一个繁琐的任务,并且难以维护一致的、可重用的组件。这就是 Jinja 发挥作用的地方,它提供了更好的表单定义和超级简单的验证,同时具有可扩展性和可定制性。
认证是任何应用程序最重要的部分之一,无论是 Web、移动还是桌面。第六章专注于各种认证技术,这些技术从社交到完全内部管理。
下一章将讨论 API,它是任何 Web 应用程序的一个基本组成部分,Flask 的主要优势之一就是以非常清晰、简洁和可读的格式构建 API。接下来是添加支持多种语言的能力到你的 Flask 应用程序中。
Flask 默认不包含像 Django 这样的管理界面,Django 是另一种流行的 Python 编写的 Web 框架。然而,通过利用一些扩展,可以在 Flask 中快速创建一个完全定制的管理界面。本部分的最后一章讨论了这个主题。
本书本部分包括以下章节:
-
第五章**,使用 WTForms 的 Web 表单
-
第六章**,在 Flask 中进行身份验证
-
第七章**,RESTful API 构建
-
第八章**,国际化与本地化
-
第九章**,Flask 应用的 Admin 界面
第五章:使用 WTForms 的 Web 表单
表单处理是任何 Web 应用程序的一个基本组成部分。可能会有无数的情况使得在任何 Web 应用程序中存在表单非常重要。一些情况可能包括用户需要登录或提交某些数据,或者应用程序可能需要从用户那里获取输入。尽管表单很重要,但它们的验证同样重要,甚至更重要。以交互式方式向用户展示这些信息将为应用程序增添很多价值。
我们有各种方法可以在 Web 应用程序中设计和实现表单。随着 Web 应用的成熟,表单验证和向用户传达正确信息变得非常重要。客户端验证可以通过 JavaScript 和 HTML5 在前端实现。服务器端验证在增加应用程序安全性方面扮演着更重要的角色,而不是用户交互。服务器端验证阻止任何错误数据进入数据库,从而遏制欺诈和攻击。
WTForms 默认提供许多带有服务器端验证的字段,因此提高了开发速度并减少了整体工作量。它还提供了灵活性,可以根据需要编写自定义验证和自定义字段。
在本章中,我们将使用一个 Flask 扩展。这个扩展被称为 Flask-WTF (flask-wtf.readthedocs.io/en/latest/);它提供了 WTForms 和 Flask 之间的集成,负责处理重要和琐碎的事情,否则我们可能需要重新发明以使我们的应用程序安全有效。我们可以使用以下命令来安装它:
$ pip install Flask-WTF
在本章中,我们将介绍以下菜谱:
-
将 SQLAlchemy 模型数据表示为表单
-
在服务器端验证字段
-
创建一个通用的表单集
-
创建自定义字段和验证
-
创建自定义小部件
-
通过表单上传文件
-
保护应用程序免受 跨站请求伪造 (CSRF)的侵害
将 SQLAlchemy 模型数据表示为表单
首先,让我们使用 SQLAlchemy 模型构建一个表单。在本例中,我们将从本书之前使用的目录应用程序中获取产品模型,并添加功能,通过 Web 表单从前端创建产品。
准备工作
我们将使用来自 第四章 的目录应用程序,与视图一起工作,并将为 Product 模型开发一个表单。
如何做到这一点...
如果你还记得,Product 模型在 models.py 文件中的代码如下:
class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255))
price = db.Column(db.Float)
category_id = db.Column(db.Integer,
db.ForeignKey('category.id'))
category = db.relationship(
'Category', backref=db.backref('products',
lazy='dynamic')
)
首先,我们将在 models.py 中创建一个 ProductForm 类;这将继承 FlaskForm 类,它由 flask_wtf 提供,以表示 Web 表单上所需的字段:
from wtforms import StringField, DecimalField, SelectField
from flask_wtf import FlaskForm
class ProductForm(FlaskForm):
name = StringField('Name')
price = DecimalField('Price')
category = SelectField('Category', coerce=int)
我们从flask-wtf扩展中导入FlaskForm。其他所有内容,如fields和validators,都直接从wtforms导入。Name字段是StringField类型,因为它需要文本数据,而Price是DecimalField类型,它将数据解析为 Python 的Decimal数据类型。我们将Category保持为SelectField类型,这意味着在创建产品时,我们只能从之前创建的类别中选择。
信息
注意,我们在Category字段(这是一个选择列表)的定义中有一个名为coerce的参数;这意味着在验证或任何其他处理之前,来自 HTML 表单的传入数据将被强制转换为整数值。在这里,强制转换简单地说就是将特定数据类型中提供的值转换为不同的数据类型。
views.py中的create_product()处理程序现在应该适应之前创建的表单:
from my_app.catalog.models import ProductForm
@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
form = ProductForm(meta={'csrf': False})
categories = [(c.id, c.name) for c in
Category.query.all()]
form.category.choices = categories
if request.method == 'POST':
name = request.form.get('name')
price = request.form.get('price')
category = Category.query.get_or_404(
request.form.get('category')
)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
flash('The product %s has been created' % name,
'success')
return redirect(url_for('catalog.product',
id=product.id))
return render_template('product-create.html',
form=form)
create_product()方法接受POST请求上的表单值。这个方法将在GET请求上渲染一个空表单,并在Category字段中预先填充选择。在POST请求上,表单数据将用于创建一个新的产品,当产品创建完成后,将显示新创建的产品页面。
信息
注意,在创建form对象时,form = ProductForm(meta={'csrf': False}),我们将csrf设置为False。CSRF 是任何安全 Web 应用程序的重要组成部分。我们将在本章的保护应用程序免受 CSRF 攻击菜谱中详细讨论这一点。
templates/product-create.html模板也需要一些修改。WTForms 创建的form对象提供了一个简单的方法来创建 HTML 表单并保持代码可读性:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<form method="POST" action="{{
url_for('catalog.create_product') }}" role="form">
<div class="form-group">{{ form.name.label }}: {{
form.name() }}</div>
<div class="form-group">{{ form.price.label }}: {{
form.price() }}</div>
<div class="form-group">{{ form.category.label }}: {{
form.category() }}</div>
<button type="submit" class="btn btn-
default">Submit</button>
</form>
</div>
{% endblock %}
它是如何工作的...
在一个GET请求上——也就是说,在打开http://127.0.0.1:5000/product-create时——我们将看到一个类似于以下截图中的表单:

图 5.1 - 使用 WTForms 创建产品表单
您可以填写此表单以创建新产品。
参见
参考以下在服务器端验证字段菜谱,了解如何验证我们刚刚学习创建的字段。
在服务器端验证字段
我们已经创建了表单和字段,但我们需要验证它们,以确保只有正确的数据通过到数据库,并且错误在之前得到处理,而不是破坏数据库。这些验证还可以保护应用程序免受跨站脚本攻击(XSS)和 CSRF 攻击。WTForms 提供了一大堆字段类型,这些类型本身就有默认的验证。除此之外,还有一些可以根据选择和需要使用的验证器。在这个菜谱中,我们将使用其中的一些来理解这个概念。
如何操作...
向我们的 WTForm 字段添加验证非常简单。我们只需要传递一个 validators 参数,它接受要实现的验证器列表。每个验证器都可以有自己的参数,这使得我们能够极大地控制验证。
让我们修改 models.py 类中的 ProductForm 对象以包含验证:
from decimal import Decimal
class ProductForm(FlaskForm):
name = StringField('Name',
validators=[InputRequired()])
price = DecimalField('Price', validators=[
InputRequired(), NumberRange(min=Decimal('0.0'))
])
category = SelectField(
'Category', validators=[InputRequired()],
coerce=int
)
在这里,我们在所有三个字段上都有 InputRequired 验证器;这意味着这些字段是必需的,除非我们为这些字段提供了值,否则表单将不会提交。
Price 字段有一个额外的验证器 NumberRange,其 min 参数设置为 0.0。这意味着产品的价格不能小于 0。为了补充这些更改,我们还需要修改 views.py 中的 create_product() 方法:
@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
form = ProductForm(meta={'csrf': False})
categories = [(c.id, c.name) for c in
Category.query.all()]
form.category.choices = categories
if form.validate_on_submit():
name = form.name.data
price = form.price.data
category = Category.query.get_or_404(
form.category.data
)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
flash('The product %s has been created' % name,
'success')
return redirect(url_for('catalog.product',
id=product.id))
if form.errors:
flash(form.errors, 'danger')
return render_template('product-create.html',
form=form)
小贴士
form.errors 的闪现将仅以 JSON 对象的形式显示错误。这可以被格式化以使用户看到令人愉悦的格式。这留给你自己尝试。
在这里,我们修改了 create_product() 方法,以便在提交时验证表单的输入值。一些验证将被翻译并应用到前端,就像 InputRequired 验证将添加一个 required 属性到表单字段的 HTML 中。在 POST 请求中,首先将验证表单数据。如果由于某些原因验证失败,将再次渲染相同的页面,并在其上闪现错误消息。如果验证成功并且产品创建完成,将显示新创建的产品页面。
注意
注意非常方便的 validate_on_submit() 方法。这个方法会自动检查请求是否为 POST 以及其是否有效。它本质上是将 request.method == 'POST' 和 form.validate() 的组合。
它是如何工作的...
现在,尝试提交一个没有任何字段填写(即空表单)的表单。将显示一个带有错误信息的警告消息,如下所示:

图 5.2 – WTForms 中的内置错误处理
如果你尝试提交一个带有负价格值的表单,闪现的错误将类似于以下截图:

图 5.3 – WTForms 中的自定义错误处理
尝试不同的表单提交组合,这些组合将违反定义的验证器,并注意出现的不同错误消息。
参见
参考之前的配方,将 SQLAlchemy 模型数据表示为表单,以了解使用 WTForms 创建基本表单的方法。
创建公共表单集
一个应用程序可以有多个表单,这取决于设计和目的。其中一些表单将具有共同的字段和共同的验证器。你可能会想,“为什么不创建通用的表单部分并在需要时重用它们呢?”在这个菜谱中,我们将看到使用 WTForms 提供的表单定义类结构,这确实是可能的。
如何实现...
在我们的目录应用程序中,我们可以有两个表单,每个分别对应 Product 和 Category 模型。这些表单将有一个共同的字段称为 Name。我们可以为这个字段创建一个通用的表单,然后 Product 和 Category 模型的单独表单可以使用这个表单,而不是在每个模型中都有一个 Name 字段。
这可以在 models.py 中如下实现:
class NameForm(FlaskForm):
name = StringField('Name',
validators=[InputRequired()])
class ProductForm(NameForm):
price = DecimalField('Price', validators=[
InputRequired(), NumberRange(min=Decimal('0.0'))
])
category = SelectField(
'Category', validators=[InputRequired()],
coerce=int
)
class CategoryForm(NameForm):
pass
我们创建了一个名为 NameForm 的通用表单,其他表单 ProductForm 和 CategoryForm 从这个表单继承,默认包含一个名为 Name 的字段。然后,我们可以根据需要添加更多字段。
我们可以修改 views.py 中的 create_category() 方法,使用 CategoryForm 来创建分类:
@catalog.route('/category-create', methods=['GET', 'POST'])
def create_category():
form = CategoryForm(meta={'csrf': False})
if form.validate_on_submit():
name = form.name.data
category = Category(name)
db.session.add(category)
db.session.commit()
flash(
'The category %s has been created' % name,
'success'
)
return redirect(url_for('catalog.category',
id=category.id))
if form.errors:
flash(form.errors)
return render_template('category-create.html',
form=form)
还需要添加一个新的模板,templates/category-create.html,用于创建分类:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<form method="POST" action="{{
url_for('catalog.create_category') }}" role="form">
<div class="form-group">{{ form.name.label }}: {{
form.name() }}</div>
<button type="submit" class="btn btn-
default">Submit</button>
</form>
</div>
{% endblock %}
它是如何工作的...
在您的浏览器中打开 http://127.0.0.1:5000/category-create URL。新创建的分类表单将看起来像以下截图:

图 5.4 – 用于创建分类的通用表单
小贴士
这只是一个如何实现通用表单集的小例子。这种方法的实际好处可以在电子商务应用程序中看到,在那里我们可以有通用的地址表单,然后它们可以扩展为具有单独的账单和发货地址。
创建自定义字段和验证
除了提供一系列字段和验证之外,Flask 和 WTForms 还提供了创建自定义字段和验证的灵活性。有时,我们可能需要解析一些无法使用当前可用字段处理的数据格式。在这种情况下,我们可以实现自己的字段。
如何实现...
在我们的目录应用程序中,我们使用 SelectField 来处理分类,并在 GET 请求中通过查询 Category 模型来填充这个字段的值。如果我们不需要关心这一点,并且这个字段的填充可以自动完成,那就方便多了。
现在,让我们在 models.py 中实现一个自定义字段来完成这个功能:
class CategoryField(SelectField):
def iter_choices(self):
categories = [(c.id, c.name) for c in
Category.query.all()]
for value, label in categories:
yield (value, label, self.coerce(value) ==
self.data)
def pre_validate(self, form):
for v, _ in [(c.id, c.name) for c in
Category.query.all()]:
if self.data == v:
break
else:
raise ValueError(self.gettext('Not a valid
choice'))
class ProductForm(NameForm):
price = DecimalField('Price', validators=[
InputRequired(), NumberRange(min=Decimal('0.0'))
])
category = CategoryField(
'Category', validators=[InputRequired()],
coerce=int
)
SelectField 实现了一个名为 iter_choices() 的方法,它使用提供给 choices 参数的值列表来填充表单。我们重写了 iter_choices() 方法,直接从数据库中获取分类的值,这样就消除了每次使用这个表单时都需要填充这个字段的必要性。
信息
CategoryField在这里创建的行为也可以使用QuerySelectField实现。有关更多信息,请参阅wtforms-sqlalchemy.readthedocs.io/en/stable/wtforms_sqlalchemy/#wtforms_sqlalchemy.fields.QuerySelectField。
由于本节中描述的更改,我们的views.py中的create_product()方法将需要进行修改。为此,只需删除以下两个填充表单中类别的语句:
categories = [(c.id, c.name) for c in Category.query.all()]
form.category.choices = categories
工作原理...
应用程序上不会有任何视觉上的影响。唯一的变化将是类别在表单中的填充方式,如前节所述。
更多内容...
我们刚刚看到了如何编写自定义字段。同样,我们也可以编写自定义验证。假设我们不想允许重复的类别。我们可以在模型中轻松实现这一点,但让我们使用表单上的自定义验证器来完成:
def check_duplicate_category(case_sensitive=True):
def _check_duplicate(form, field):
if case_sensitive:
res = Category.query.filter(
Category.name.like('%' + field.data + '%')
).first()
else:
res = Category.query.filter(
Category.name.ilike('%' + field.data + '%')
).first()
if res:
raise ValidationError(
'Category named %s already exists' %
field.data
)
return _check_duplicate
class CategoryForm(NameForm):
name = StringField('Name', validators=[
InputRequired(), check_duplicate_category()
])
因此,我们以工厂模式创建了我们的验证器,这样我们就可以根据是否需要大小写敏感的比较来获取单独的验证结果。我们甚至可以编写基于类的设计,这使得验证器更加通用和灵活,但我会把这个留给你去探索。
现在,如果你尝试创建一个与已存在的类别同名的新类别,将会显示以下错误:

图 5.5 – 创建重复类别时的错误
创建自定义组件
就像我们可以创建自定义字段和验证器一样,我们也可以创建自定义组件。这些组件允许我们控制字段在前端显示的方式。每个字段类型都关联着一个组件,WTForms 本身提供了大量的基本和 HTML5 组件。在本例中,为了理解如何编写自定义组件,我们将我们的自定义选择字段Category转换为单选字段。我同意那些认为我们可以直接使用 WTForms 提供的单选字段的人。在这里,我们只是在尝试理解如何自己完成它。
信息
WTForms 默认提供的组件可以在wtforms.readthedocs.io/en/3.0.x/widgets/找到。
如何操作...
在我们之前的示例中,我们创建了CategoryField。这个字段使用了Select组件,它是由Select超类提供的。让我们在models.py中将Select组件替换为单选输入:
from wtforms.widgets import html_params, Select
from markupsafe import Markup
class CustomCategoryInput(Select):
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)
html = []
for val, label, selected in field.iter_choices():
html.append(
'<input type="radio" %s> %s' % (
html_params(
name=field.name, value=val,
checked=selected, **kwargs
), label
)
)
return Markup(' '.join(html))
class CategoryField(SelectField):
widget = CustomCategoryInput()
# Rest of the code remains same as in last recipe
Creating custom field and validation
在这里,我们向CategoryField类添加了一个名为widget的类属性。这个组件指向CustomCategoryInput,它负责生成要渲染的字段的 HTML 代码。这个类有一个__call__()方法,它被重写以返回与CategoryField的iter_choices()方法提供的值相对应的单选输入。
工作原理...
当你打开产品创建页面,http://127.0.0.1:5000/product-create,它看起来如下所示:

图 5.6 – 用于类别选择的自定义小部件
参见
参考之前的菜谱,创建自定义字段和验证,以了解可以对 WTForms 的组件进行多少定制。
通过表单上传文件
通过表单上传文件,并且正确地做这件事,通常是许多网络框架关注的问题。在这个菜谱中,我们将看到 Flask 和 WTForms 如何以简单和流畅的方式为我们处理这个问题。
如何做...
在这个菜谱中,我们将实现一个在创建产品时存储产品图片的功能。首先,我们将从配置部分开始。我们需要向应用程序配置提供一个参数——即 UPLOAD_FOLDER。这个参数告诉 Flask 我们上传的文件将被存储的位置。
小贴士
存储产品图片的一种方法是将图片存储在数据库的二进制类型字段中,但这种方法效率非常低,在任何应用程序中都不推荐使用。我们应该始终在文件系统中存储图片和其他上传文件,并使用 string 字段在数据库中存储它们的路径。
将以下语句添加到 my_app/__init__.py 的配置中:
import os
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg',
'jpeg', 'gif'])
app.config['UPLOAD_FOLDER'] = os.path.realpath('.') +
'/my_app/static/uploads'
小贴士
注意 app.config['UPLOAD_FOLDER'] 语句,我们在 static 文件夹内的一个子文件夹中存储图片。这将使渲染图片的过程更加容易。同时,注意 ALLOWED_EXTENSIONS 语句,它用于确保只有特定格式的文件通过。这里的列表实际上只是为了演示目的,对于图像类型,我们还可以进一步过滤这个列表。确保在 app.config['UPLOAD_FOLDER'] 语句中指定的文件夹路径存在;否则,应用程序将出错。
在 models 文件中——即 my_app/catalog/models.py ——在其指定位置添加以下突出显示的语句:
from flask_wtf.file import FileField, FileRequired
class Product(db.Model):
image_path = db.Column(db.String(255))
def __init__(self, name, price, category, image_path):
self.image_path = image_path
class ProductForm(NameForm):
image = FileField('Product Image',
validators=[FileRequired()])
在 ProductForm 中检查 image 的 FileField 和 Product 模型的 image_path 字段。在这里,上传的文件将被存储在配置中定义的路径上的文件系统中,生成的路径将被存储在数据库中。
现在,修改 create_product() 方法,将文件保存在 my_app/catalog/views.py 中的 my_app/catalog/views.py:
import os
from werkzeug.utils import secure_filename
from my_app import ALLOWED_EXTENSIONS
@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
form = ProductForm(meta={'csrf': False})
if form.validate_on_submit():
name = form.name.data
price = form.price.data
category = Category.query.get_or_404(
form.category.data
)
image = form.image.data
if allowed_file(image.filename):
filename = secure_filename(image.filename)
image.save(os.path.join(app.config
['UPLOAD_FOLDER'], filename))
product = Product(name, price, category, filename)
db.session.add(product)
db.session.commit()
flash('The product %s has been created' % name,
'success')
return redirect(url_for('catalog.product',
id=product.id))
if form.errors:
flash(form.errors, 'danger')
return render_template('product-create.html',
form=form)
将新字段添加到 template templates/product-create.html 中的产品创建表单。修改 form 标签定义以包含 enctype 参数,并在 提交 按钮之前(或您认为表单内任何必要的位置)添加字段:
<form method="POST"
action="{{ url_for('catalog.create_product') }}"
role="form"
enctype="multipart/form-data">
<!-- The other field definitions as always -->
<div class="form-group">{{ form.image.label }}: {{
form.image(style='display:inline;') }}</div>
<button type="submit" class="btn btn-
default">Submit</button>
</form>
表单应该有 enctype="multipart/form-data" 语句来告诉应用程序表单输入将包含多部分数据。
渲染图像非常简单,因为我们把文件存储在 static 文件夹本身。只需在 templates/product.html 中需要显示图像的地方添加 img 标签:
<img src="{{ url_for('static', filename='uploads/' +
product.image_path) }}"/>
它是如何工作的...
上传图像的字段看起来可能如下面的截图所示:

图 5.7 – 上传产品图像的文件
在创建产品后,图像将显示,如下面的截图所示:

图 5.8 – 上传文件的产品页面
保护应用程序免受 CSRF 攻击
在本章的第一个配方中,我们了解到 CSRF 是网络表单安全的重要组成部分。现在我们将详细讨论这个问题。CSRF 基本上意味着有人可以黑入携带 cookie 的请求并使用它来触发破坏性操作。我们不会在这里详细讨论 CSRF,因为互联网上有大量资源可以学习它。我们将讨论 WTForms 如何帮助我们防止 CSRF。Flask 默认不提供 CSRF 的安全措施,因为这需要在表单验证级别处理,而这不是 Flask 作为框架的核心功能。然而,在本配方中,我们将看到 Flask-WTF 扩展如何为我们完成这项工作。
信息
更多关于 CSRF 的信息可以在 owasp.org/www-community/attacks/csrf 找到。
如何做到这一点...
Flask-WTF 默认提供的是一个 CSRF-受保护的表单。如果我们查看到目前为止的配方,我们可以看到我们已经明确告诉我们的表单不要进行 CSRF-保护。我们只需要移除相应的语句来启用 CSRF。
因此,form = ProductForm(meta={'csrf': False}) 将变为 form = ProductForm()。
一些配置位也需要在我们的应用程序中进行设置:
app.config['WTF_CSRF_SECRET_KEY'] = 'random key for form'
默认情况下,CSRF 密钥与我们的应用程序的密钥相同。
启用 CSRF 后,我们将在我们的表单中提供一个额外的字段;这是一个隐藏字段,包含 CSRF 令牌。WTForms 会为我们处理隐藏字段,我们只需要在我们的表单中添加 {{ form.csrf_token }}:
<form method="POST" action="/some-action-like-create-
product">
{{ form.csrf_token }}
</form>
这很简单!现在,这并不是我们唯一提交的表单提交类型。我们还提交 AJAX 表单帖子;自从基于 JavaScript 的网络应用程序出现以来,这实际上比普通表单发生得更多,因为它们正在取代传统的网络应用程序。
为了做到这一点,我们需要在我们的应用程序配置中包含另一个步骤:
from flask_wtf.csrf import CSRFProtect
#
# Add configurations #
CSRFProtect(app)
之前的配置将允许我们在模板的任何地方使用 {{ csrf_token() }} 访问 CSRF 令牌。现在,有两种方法可以将 CSRF 令牌添加到 AJAX POST 请求中。
一种方法是在我们的 script 标签中获取 CSRF 令牌并在 POST 请求中使用它:
<script type="text/javascript">
var csrfToken = "{{ csrf_token() }}";
</script>
另一种方法是在 meta 标签中渲染令牌并在需要时使用它:
<meta name="csrf-token" content="{{ csrf_token() }}"/>
两种方法之间的区别在于,第一种方法可能需要根据应用程序中 script 标签的数量在多个地方重复。
现在,为了将 CSRF 令牌添加到 AJAX POST 请求中,我们必须向其中添加 X-CSRFToken 属性。此属性的值可以从这里提到的两种方法中的任何一种获取。我们将采用第二种方法作为我们的示例:
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i
.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
})
这将确保 CSRF 令牌被添加到所有发出的 AJAX POST 请求中。
它是如何工作的...
以下截图显示了 WTForms 在我们的表单中添加的 CSRF 令牌的外观:

图 5.9 – CSRF 令牌
令牌是完全随机的,对于所有请求都是不同的。有多种实现 CSRF 令牌生成的方法,但这超出了本书的范围,尽管我鼓励你自己探索一些替代实现来了解它是如何完成的。
第六章:在 Flask 中进行身份验证
身份验证是任何应用程序的重要组成部分,无论是基于 Web、桌面还是移动应用程序。每种类型的应用程序在处理用户身份验证方面都有一定的最佳实践。在基于 Web 的应用程序中,特别是 软件即服务(SaaS)应用程序,这个过程至关重要,因为它充当着应用程序安全与不安全的薄红线。
为了保持简单和灵活,Flask 默认不提供任何身份验证机制。它必须由我们,即开发者,根据我们的需求和应用程序的需求来实现。
为您的应用程序进行用户身份验证可以有多种方式。它可以是一个简单的基于会话的实现,或者是一个更安全的通过 Flask-Login 扩展的方法。我们还可以通过集成流行的第三方服务,如 轻量级目录访问协议(LDAP)或社交登录,如 Facebook、Google 等,来实现身份验证。在本章中,我们将介绍所有这些方法。
在本章中,我们将涵盖以下食谱:
-
创建简单的基于会话的身份验证
-
使用 Flask-Login 扩展进行身份验证
-
使用 Facebook 进行身份验证
-
使用 Google 进行身份验证
-
使用 Twitter 进行身份验证
-
使用 LDAP 进行身份验证
创建简单的基于会话的身份验证
在基于会话的身份验证中,当用户首次登录时,用户详细信息被设置在应用程序服务器的会话中,并存储在浏览器的 cookie 中。
之后,当用户打开应用程序时,存储在 cookie 中的详细信息将用于与会话进行核对,如果会话仍然活跃,用户将自动登录。
信息
SECRET_KEY 是一个应用程序配置设置,应始终在您的应用程序配置中指定;否则,存储在 cookie 中的数据以及服务器端的会话都将以纯文本形式存在,这非常不安全。
我们将实现一个简单的机制来自行完成。
小贴士
本食谱中实现的方案旨在解释身份验证在较低级别是如何工作的。这种方法不应在任何生产级应用程序中采用。
准备工作
我们将从 Flask 应用程序配置开始,正如在 第五章 中所看到的,使用 WTForms 的 Web 表单。
如何操作...
配置应用程序以使用 SQLAlchemy 和 WTForms 扩展(有关详细信息,请参阅上一章)。按照以下步骤了解如何操作:
-
在开始进行身份验证之前,首先创建一个模型来存储用户详细信息。这通过在
flask_authentication/my_app/auth/models.py中创建模型来实现,如下所示:from werkzeug.security import generate_password_hash, check_password_hashfrom flask_wtf import FlaskFormfrom wtforms import StringField, PasswordFieldfrom wtforms.validators import InputRequired, EqualTofrom my_app import dbclass User(db.Model):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(100))pwdhash = db.Column(db.String())def __init__(self, username, password):self.username = usernameself.pwdhash = generate_password_hash(password)def check_password(self, password):return check_password_hash(self.pwdhash, password)
上述代码是User模型,它有两个字段:username和pwdhash。username字段正如其名所示。pwdhash字段存储密码的加盐散列,因为直接在数据库中存储密码是不推荐的。
-
然后,在
flask_authentication/my_app/auth/models.py中创建两个表单 – 一个用于用户注册,另一个用于登录。在RegistrationForm中创建两个PasswordField类型的字段,就像任何其他网站的注册一样;这是为了确保用户在两个字段中输入相同的密码,如下面的代码片段所示:class RegistrationForm(FlaskForm):username = StringField('Username', [InputRequired()])password = PasswordField('Password', [InputRequired(), EqualTo('confirm',message='Passwords must match')])confirm = PasswordField('Confirm Password',[InputRequired()])class LoginForm(FlaskForm):username = StringField('Username', [InputRequired()])password = PasswordField('Password', [InputRequired()]) -
接下来,在
flask_authentication/my_app/auth/views.py中创建视图来处理用户对注册和登录的请求,如下所示:from flask import request, render_template, flash, redirect, url_for, session, Blueprintfrom my_app import app, dbfrom my_app.auth.models import User, RegistrationForm, LoginFormauth = Blueprint('auth', __name__)@auth.route('/')@auth.route('/home')def home():return render_template('home.html')@auth.route('/register', methods=['GET', 'POST'])def register():if session.get('username'):flash('Your are already logged in.', 'info')return redirect(url_for('auth.home'))form = RegistrationForm()if form.validate_on_submit():username = request.form.get('username')password = request.form.get('password')existing_username = User.query.filter(User.username.like('%' + username + '%')).first()if existing_username:flash('This username has been already taken. Tryanother one.','warning')return render_template('register.html', form=form)user = User(username, password)db.session.add(user)db.session.commit()flash('You are now registered. Please login.','success')return redirect(url_for('auth.login'))if form.errors:flash(form.errors, 'danger')return render_template('register.html', form=form)
前面的方法处理用户注册。在GET请求中,向用户显示注册表单;这个表单要求输入username和password。然后,在POST请求中,在表单验证完成后检查username的唯一性。如果username不唯一,用户会被要求选择一个新的username;否则,在数据库中创建一个新的用户,并将其重定向到登录页面。
注册成功后,用户会被重定向到登录页面,这由以下代码处理:
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
username = request.form.get('username')
password = request.form.get('password')
existing_user = User.query.filter_by(username=username).
first()
if not (existing_user and existing_user.check_
password(password)):
flash('Invalid username or password. Please try again.',
'danger')
return render_template('login.html', form=form)
session['username'] = username
flash('You have successfully logged in.', 'success')
return redirect(url_for('auth.home'))
if form.errors:
flash(form.errors, 'danger')
return render_template('login.html', form=form)
前面的方法处理用户登录。在表单验证后,它首先检查数据库中是否存在username。如果不存在,它会要求用户输入正确的用户名。同样,它检查密码是否正确。如果不正确,它会要求用户输入正确的密码。如果所有检查都通过,会话中就会填充一个username键,它包含用户的用户名。这个键在会话中的存在表示用户已登录。考虑以下代码:
@auth.route('/logout')
def logout():
if 'username' in session:
session.pop('username')
flash('You have successfully logged out.', 'success')
return redirect(url_for('auth.home'))
一旦我们理解了login()方法,前面的方法就变得不言而喻了。在这里,我们只是从会话中弹出了username键,用户就自动登出了。
接下来,创建由之前创建的注册和登录处理程序渲染的模板。
flask_authentication/my_app/templates/base.html模板几乎与第五章中“使用 WTForms 的 Web 表单”相同。唯一的改变将是路由,其中catalog将被替换为auth。
首先,创建一个简单的首页,flask_authentication/my_app/templates/home.html,如下代码所示。这反映了用户是否已登录,如果用户未登录,则显示注册和登录链接:
{% extends 'base.html' %}
{% block container %}
<h1>Welcome to the Authentication Demo</h1>
{% if session.username %}
<h3>Hey {{ session.username }}!!</h3>
<a href="{{ url_for('auth.logout') }}">Click here to logout</a>
{% else %}
Click here to <a href="{{ url_for('auth.login') }}">login</a> or <a
href="{{ url_for('auth.register') }}">register</a>
{% endif %}
{% endblock %}
现在创建一个注册页面,flask_authentication/my_app/templates/register.html,如下:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<form
method="POST"
action="{{ url_for('auth.register') }}"
role="form">
{{ form.csrf_token }}
<div class="form-group">{{ form.username.label }}: {{ form.
username() }}</div>
<div class="form-group">{{ form.password.label }}: {{ form.
password() }}</div>
<div class="form-group">{{ form.confirm.label }}: {{ form.
confirm() }}</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
{% endblock %}
最后,创建一个简单的登录页面,flask_authentication/my_app/templates/login.html,以下代码:
{% extends 'home.html' %}
{% block container %}
<div class="top-pad">
<form
method="POST"
action="{{ url_for('auth.login') }}"
role="form">
{{ form.csrf_token }}
<div class="form-group">{{ form.username.label }}: {{ form.
username() }}</div>
<div class="form-group">{{ form.password.label }}: {{ form.
password() }}</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
</div>
{% endblock %}
它是如何工作的...
本节通过截图展示了该应用程序的工作原理。
以下截图显示了在打开 http://127.0.0.1:5000/home 时出现的首页:

图 6.1 – 对未登录用户可见的首页
在打开 http://127.0.0.1:5000/register 时出现的注册页面如下截图所示:

图 6.2 – 注册表单
注册后,在打开 http://127.0.0.1:5000/login 时将显示登录页面,如下截图所示:

图 6.3 – 成功注册后渲染的登录页面
最后,在 http://127.0.0.1:5000/home 上向已登录用户展示首页,如下截图所示:

图 6.4 – 展示给已登录用户的首页
参见
下一个菜谱,使用 Flask-Login 扩展进行身份验证,将介绍一种更安全和适用于生产的用户身份验证方法。
使用 Flask-Login 扩展进行身份验证
在我们之前的菜谱中,我们学习了如何自己实现基于会话的身份验证。Flask-Login 是一个流行的扩展,以有用且高效的方式处理了许多相同的事情,因此我们不必再次从头开始重新发明轮子。此外,Flask-Login 不会将我们绑定到任何特定的数据库,也不会限制我们使用任何特定的字段或方法进行身份验证。它还可以处理 Flask-Login 与我们的应用程序。
准备中
修改上一道菜谱中创建的应用程序,以适应 Flask-Login 扩展所做的更改。
在此之前,我们必须使用以下命令安装该扩展本身:
$ pip install Flask-Login
如何操作...
按照以下步骤了解如何将 Flask-Login 集成到 Flask 应用程序中:
-
要使用
Flask-Login,首先,修改应用程序的配置,该配置位于flask_authentication/my_app/__init__.py中,如下所示:from flask_login import LoginManager## Do other application configurations#login_manager = LoginManager()login_manager.init_app(app)login_manager.login_view = 'auth.login'
在前面的代码片段中,在从扩展中导入 LoginManager 类之后,我们创建了该类的对象。然后,我们使用 init_app() 配置 app 对象以与 LoginManager 一起使用。在 login_manager 对象中,可以根据需要执行多个配置。在这里,我们只演示了一个基本且必需的配置,即 login_view,它指向登录请求的处理视图。此外,我们还可以配置显示给用户的消息,例如会话将持续多长时间,使用请求头处理登录等。有关更多详细信息,请参阅 flask-login.readthedocs.org/en/latest/#customizing-the-login-process 的 Flask-Login 文档。
-
Flask-Login要求在my_app/auth/models.py中的User模型/类中添加一些额外的方法,如下面的代码片段所示:@propertydef is_authenticated(self):return True@propertydef is_active(self):return True@propertydef is_anonymous(self):return Falsedef get_id(self):return str(self.id)
在前面的代码中,我们添加了四个方法,下面将逐一解释:
-
is_authenticated(): 这个属性返回True。只有在不想让用户认证的情况下,它才应该返回False。 -
is_active(): 这个属性返回True。只有在用户被阻止或禁止的情况下,它才应该返回False。 -
is_anonymous(): 这个属性用于指示不应登录到系统并应作为匿名用户访问应用程序的用户。对于常规登录用户,它应该返回False。 -
get_id(): 这个方法代表用于识别用户的唯一ID。这应该是一个 Unicode 值。
信息
在实现用户类时,没有必要实现所有讨论的方法和属性。为了简化操作,你可以始终从flask_login中的UserMixin类进行子类化,该类已经为我们提到的方法和属性提供了默认实现。有关更多信息,请访问flask-login.readthedocs.io/en/latest/#flask_login.UserMixin。
-
接下来,对
my_app/auth/views.py中的视图进行以下修改:from flask import gfrom flask_login import current_user, login_user, logout_user, \login_requiredfrom my_app import login_manager@login_manager.user_loaderdef load_user(id):return User.query.get(int(id))@auth.before_requestdef get_current_user():g.user = current_user
在前面的方法中,@auth.before_request装饰器意味着每当收到请求时,该方法将在视图函数之前被调用。
-
在以下代码片段中,我们已经记录了我们的登录用户:
@auth.route('/login', methods=['GET', 'POST'])def login():if current_user.is_authenticated:flash('You are already logged in.', 'info')return redirect(url_for('auth.home'))form = LoginForm()if form.validate_on_submit():username = request.form.get('username')password = request.form.get('password')existing_user = User.query.filter_by(username=username).first()if not (existing_user and existing_user.check_password(password)):flash('Invalid username or password. Please tryagain.', 'danger')return render_template('login.html', form=form)login_user(existing_user)flash('You have successfully logged in.', 'success')return redirect(url_for('auth.home'))if form.errors:flash(form.errors, 'danger')return render_template('login.html', form=form)@auth.route('/logout')@login_requireddef logout():logout_user()return redirect(url_for('auth.home'))
注意,现在在login()中,我们在做其他任何事情之前都会检查current_user是否已认证。在这里,current_user是一个代理,代表当前登录的User记录的对象。在完成所有验证和检查后,用户将使用login_user()方法登录。此方法接受user对象,并处理登录用户所需的全部会话相关活动。
现在,如果我们继续到logout()方法,我们可以看到为login_required()添加了一个装饰器。这个装饰器确保在执行此方法之前用户已经登录。它可以用于我们应用程序中的任何视图方法。要注销用户,我们只需调用logout_user(),这将清理当前登录用户的会话,从而注销用户。
由于我们不自行处理会话,因此需要根据以下代码片段对模板进行一些小的修改。这发生在我们想要检查用户是否已登录以及是否需要向他们显示特定内容时:
{% if current_user.is_authenticated %}
...do something...
{% endif %}
它是如何工作的…
本食谱中的演示与上一个食谱中创建的简单基于会话的认证完全相同,创建一个简单的基于会话的认证。只有实现方式不同,但最终结果保持不变。
还有更多...
Flask-Login扩展使得将remember=True参数应用到login_user()方法上成为可能。这将在一个用户的电脑上保存一个 cookie,如果会话是活跃的,Flask-Login将使用这个 cookie 自动登录用户。你应该尝试自己实现这一功能。
另请参阅
参考之前的食谱,创建简单的基于会话的认证,以了解本食谱的完整工作原理。
Flask 提供了一个名为g的特殊对象。你可以在flask.palletsprojects.com/en/2.2.x/api/#flask.g了解更多相关信息。
另一种有趣的认证方式是使用 JWT 令牌,其工作方式与Flask-Login非常相似。更多详情请参阅flask-jwt-extended.readthedocs.io/en/stable/。
使用 Facebook 进行认证
你可能已经注意到许多网站提供了使用第三方身份验证(如 Facebook、Google、Twitter、LinkedIn 等)登录自己网站的选择。这是通过OAuth 2实现的,它是一个授权的开放标准。它允许客户端站点使用访问令牌来访问资源服务器(如 Google、Facebook 等)提供的受保护信息和资源。在本食谱中,我们将向你展示如何通过 Facebook 实现基于 OAuth 的授权。在后续的食谱中,我们将使用其他提供者做同样的事情。
信息
OAuth 是一种机制,允许用户在不共享密码的情况下,授予网站或应用程序访问其他网站(如 Google、Facebook、Twitter 等)上其信息的能力。本质上意味着第三方客户端应用程序(你的 Flask 应用程序)通过资源服务器(Google、Facebook 等)的认证引擎在资源所有者(用户)批准后,通过访问令牌获取存储在资源服务器上的数据。
准备工作
OAuth 2 仅与 SSL 一起工作,因此应用程序应该使用 HTTPS 运行。要在本地机器上这样做,请按照以下步骤操作:
-
使用
$ pip3 install命令安装pyopenssl。 -
向
app.run()添加额外的选项,包括带有adhoc值的ssl_context。完成的app.run应该如下所示:app.run(debug=True, ssl_context='adhoc')。 -
一旦进行了这些更改,请使用 URL
https://localhost:5000/运行应用程序。在应用程序加载之前,你的浏览器将显示有关证书不安全的警告。只需接受警告并继续即可。
小贴士
这不是一个推荐的方法。在生产系统中,SSL 证书应该从适当的认证机构获取。
要安装Flask-Dance并生成 Facebook 凭证,请按照以下步骤操作:
-
首先,使用以下命令安装
Flask-Dance扩展及其依赖项:$ pip3 install Flask-Dance -
接下来,注册一个用于登录的 Facebook 应用程序。尽管在 Facebook 应用程序上注册的过程相当直接且易于理解,但在此情况下,我们只关注以下截图中的 App ID、App secret 和 Site URL 选项(更多信息可以在 Facebook 开发者页面
developers.facebook.com/上找到):

图 6.5 – Facebook 应用程序凭据
在配置 Facebook 时,请确保将网站 URL 配置为 https://localhost:5000/ 以完成本食谱,并配置有效的 OAuth 重定向 URI,如下截图所示:

图 6.6 – Facebook 网站 URL 配置

图 6.7 – Facebook OAuth 重定向 URI 配置
如何操作...
要为您的应用程序启用 Facebook 认证,请按照以下步骤操作:
-
如往常一样,从
my_app/__init__.py中的配置部分开始。添加以下代码行;除非你确信更改,否则不要删除或编辑其他任何内容:app.config["FACEBOOK_OAUTH_CLIENT_ID"] = 'my facebook APP ID'app.config["FACEBOOK_OAUTH_CLIENT_SECRET"] = 'my facebook app secret'from my_app.auth.views import facebook_blueprintapp.register_blueprint(auth)app.register_blueprint(facebook_blueprint)
在前面的代码片段中,我们使用 Flask-Dance 和我们的应用程序进行认证。这个蓝图将在 views 文件中创建,我们将在下一节中介绍。
-
现在修改视图,即
my_app/auth/views.py,如下所示:from flask_dance.contrib.facebook importmake_facebook_blueprint, facebookfacebook_blueprint =make_facebook_blueprint(scope='email',redirect_to='auth.facebook_login')
make_facebook_blueprint 从应用程序配置中读取 FACEBOOK_OAUTH_CLIENT_ID 和 FACEBOOK_OAUTH_CLIENT_SECRET,并在后台处理所有 OAuth 相关操作。在创建 Facebook 蓝图时,我们将 scope 设置为 email,这样电子邮件地址就可以用作唯一的用户名。我们还设置了 redirect_to 为 auth.facebook_login,这样一旦认证成功,Facebook 就会将应用程序重定向回这个 URL。如果没有设置此选项,应用程序将自动重定向到主页,即 /。
-
现在,创建一个新的路由处理程序来处理使用 Facebook 的登录,如下所示:
@auth.route("/facebook-login")def facebook_login():if not facebook.authorized:return redirect(url_for("facebook.login"))resp = facebook.get("/me?fields=name,email")user = User.query.filter_by(username=resp.json()["email"]).first()if not user:user = User(resp.json()["email"], '')db.session.add(user)db.session.commit()login_user(user)flash('Logged in as name=%s using Facebook login' % (resp.json()['name']), 'success' )return redirect(request.args.get('next',url_for('auth.home')))
此方法首先检查用户是否已经通过 Facebook 授权。如果没有,它将应用程序重定向到 Facebook 的登录处理程序,在那里用户需要遵循 Facebook 提出的步骤,并授予我们的应用程序必要的权限以访问请求的用户详细信息,如 make_facebook_blueprint 中的设置。一旦用户通过 Facebook 授权,该方法随后将从 Facebook 请求用户的详细信息,例如他们的姓名和电子邮件地址。使用这些用户详细信息,确定是否已存在使用输入的电子邮件地址的用户。如果没有,则创建并登录新用户;否则,直接登录现有用户。
-
最后,修改
login.html模板以允许更广泛的社会登录功能。这将为 Facebook 登录以及多个替代社交登录充当占位符,我们将在后面介绍。更新后的login.html模板的代码如下:{% extends 'home.html' %}{% block container %}<div class="top-pad"><ul class="nav nav-tabs"><li class="active"><a href="#simple-form" data-toggle="tab">Old Style Login</a></li><li><a href="#social-logins" data-toggle="tab">Social Logins</a></li></ul><div class="tab-content"><div class="tab-pane active" id="simple-form"><formmethod="POST"action="{{ url_for('auth.login') }}"role="form">{{ form.csrf_token }}<div class="form-group">{{ form.username.label }}: {{ form.username() }}</div><div class="form-group">{{ form.password.label }}: {{ form.password() }}</div><button type="submit" class="btn btn-default">Submit</button></form></div><div class="tab-pane" id="social-logins"><a href="{{ url_for('auth.facebook_login',next=url_for('auth.home')) }}">Login via Facebook</a></div></div></div>{% endblock %}
在前面的代码中,我们在其中创建了一个标签结构,其中第一个标签是我们的传统登录,第二个标签对应于社交登录。
目前,仅提供一种 Facebook 登录选项。在未来的菜谱中还将添加更多选项。请注意,链接目前很简单;我们可以在以后根据需要添加样式和按钮。
它是如何工作的...
登录页面有一个新的标签页,提供用户使用 社交登录 登录的选项,如下面的截图所示:

图 6.8 – 社交登录页面
当我们点击 通过 Facebook 登录 链接时,应用程序将跳转到 Facebook,用户将被要求提供他们的登录详情和权限。一旦权限被授予,用户将登录到应用程序。
使用 Google 进行身份验证
就像我们对 Facebook 所做的那样,我们可以将我们的应用程序集成以启用使用 Google 的登录。
准备工作
从上一个菜谱开始构建。通过简单地省略 Facebook 特定的元素,很容易实现 Google 身份验证。
现在,从 Google 开发者控制台(console.developers.google.com)创建一个新项目。在 APIs and Services 部分中,点击 Credentials。然后,为网络应用程序创建一个新的客户端 ID;此 ID 将提供 OAuth 2.0 运作所需的凭据。在创建客户端 ID 之前,您还需要配置 OAuth 授权屏幕,如下面的截图所示:

图 6.9 – Google 应用配置
如何操作...
要在您的应用程序中启用 Google 身份验证,请按照以下步骤操作:
-
和往常一样,从
my_app/__init__.py中的配置部分开始,如下所示:app.config["GOOGLE_OAUTH_CLIENT_ID"] = "my GoogleOAuth client ID"app.config["GOOGLE_OAUTH_CLIENT_SECRET"] = "my GoogleOAuth client secret"app.config["OAUTHLIB_RELAX_TOKEN_SCOPE"] = Truefrom my_app.auth.views import auth,facebook_blueprint, google_blueprintapp.register_blueprint(google_blueprint)
在前面的代码片段中,我们将 Flask-Dance 提供的 Google 蓝图注册到我们的应用程序以进行身份验证。这个蓝图将在 views 文件中创建,我们将在下一部分查看。注意额外的配置选项 OAUTHLIB_RELAX_TOKEN_SCOPE。这建议在实现 Google 身份验证时使用,因为 Google 有时会提供与提到的范围不一致的数据。
-
接下来,修改视图,即
my_app/auth/views.py,如下所示:from flask_dance.contrib.google importmake_google_blueprint, googlegoogle_blueprint = make_google_blueprint(scope=["openid","https://www.googleapis.com/auth/userinfo.email","https://www.googleapis.com/auth/userinfo.profile"],redirect_to='auth.google_login')
在前面的代码片段中,make_google_blueprint从应用程序配置中读取GOOGLE_OAUTH_CLIENT_ID和GOOGLE_OAUTH_CLIENT_SECRET,并在后台处理所有 OAuth 相关操作。在创建 Google 蓝图时,我们将scope设置为openid、https://www.googleapis.com/auth/userinfo.email和https://www.googleapis.com/auth/userinfo.profile,因为我们想使用用户的电子邮件地址作为他们的唯一用户名和登录后的显示名。openid在scope中是必需的,因为 Google 更喜欢它。
我们还将redirect_to设置为auth.google_login,这样 Google 在身份验证成功后能够将应用程序路由回此 URL。如果没有设置此选项,应用程序将自动重定向到主页,即/。
-
接下来,创建一个新的路由处理程序,用于处理使用以下代码的 Google 登录:
@auth.route("/google-login")def google_login():if not google.authorized:return redirect(url_for("google.login"))resp = google.get("/oauth2/v1/userinfo")user = User.query.filter_by(username=resp.json()["email"]).first()if not user:user = User(resp.json()["email"], '')db.session.add(user)db.session.commit()login_user(user)flash('Logged in as name=%s using Google login' % (resp.json()['name']), 'success' )return redirect(request.args.get('next',url_for('auth.home')))
在这里,该方法首先检查用户是否已经通过 Google 授权。如果没有,它将应用重定向到 Google 登录处理程序,在那里用户需要遵循 Google 概述的步骤,并允许我们的应用程序访问请求的用户详细信息。一旦用户通过 Google 授权,该方法将从 Google 请求用户的详细信息,包括他们的姓名和电子邮件地址。使用这些用户详细信息,可以确定是否已经存在具有此电子邮件地址的用户。如果没有,将创建一个新用户并登录;否则,将直接登录现有用户。
-
最后,修改登录模板
login.html,以允许 Google 登录。在social-logins标签内添加以下行:<a href="{{ url_for('auth.google_login',next=url_for('auth.home'))}}">Login via Google</a>
它是如何工作的…
Google 登录的方式与前面食谱中的 Facebook 登录类似。
使用 Twitter 进行身份验证
OAuth 实际上是在编写 Twitter OpenID API 时诞生的。在这个食谱中,我们将集成 Twitter 登录到我们的应用程序中。
准备工作
我们将继续构建在使用 Google 进行身份验证食谱之上。实现 Twitter 身份验证很容易 – 简单地从前一个身份验证食谱中省略 Facebook 或 Google 特定的部分。
首先,我们必须从 Twitter应用管理页面(developer.twitter.com/en/portal/dashboard)创建一个应用程序。它将自动为我们创建消费者 API 密钥(API Key和API Key Secret),如下面的截图所示:

图 6.10 – Twitter 应用配置
如何实现...
要为您的应用程序启用 Twitter 身份验证,请按照以下步骤操作:
-
首先,从
my_app/__init__.py中的配置部分开始,如下所示:app.config["TWITTER_OAUTH_CLIENT_KEY"] = "my Twitterapp ID"app.config["TWITTER_OAUTH_CLIENT_SECRET"] = "myTwitter app secret"from my_app.auth.views import twitter_blueprintapp.register_blueprint(twitter_blueprint)
在前面的代码片段中,我们使用 Flask-Dance 提供的 Twitter 蓝图在我们的应用程序中进行身份验证注册。这个蓝图将在views文件中创建,我们将在下一节查看。
-
接下来,修改视图,即
my_app/auth/views.py,如下所示:from flask_dance.contrib.twitter importmake_twitter_blueprint, twittertwitter_blueprint = make_twitter_blueprint(redirect_to='auth.twitter_login')
在前面的代码中,make_twitter_blueprint从应用程序配置中读取TWITTER_OAUTH_CLIENT_KEY和TWITTER_OAUTH_CLIENT_SECRET,并在后台处理所有 OAuth 相关操作。不需要设置scope,因为我们之前在 Facebook 和 Google 身份验证中做的那样,因为这个配方将使用 Twitter 昵称作为用户名,这是默认提供的。
我们还将redirect_to设置为auth.twitter_login,以便 Twitter 在身份验证成功后可以将应用程序路由回此 URL。如果不设置此选项,应用程序将自动重定向到主页,即/。
-
接下来,创建一个新的路由处理程序,用于处理使用 Twitter 的登录,如下所示:
@auth.route("/twitter-login")def twitter_login():if not twitter.authorized:return redirect(url_for("twitter.login"))resp = twitter.get("account/verify_credentials.json")user = User.query.filter_by(username=resp.json()["screen_name"]).first()if not user:user = User(resp.json()["screen_name"], '')db.session.add(user)db.session.commit()login_user(user)flash('Logged in as name=%s using Twitter login' % (resp.json()['name']), 'success' )return redirect(request.args.get('next',url_for('auth.home')))
上述方法首先检查用户是否已经通过 Twitter 授权。如果没有,它将应用重定向到 Twitter 登录处理程序,在那里用户需要遵循 Twitter 概述的步骤并允许我们的应用程序访问请求的用户详细信息。一旦用户通过 Twitter 授权,该方法将请求用户的详细信息,包括他们的 Twitter 屏幕名或昵称。使用这些用户详细信息,可以确定是否已经存在具有此 Twitter 昵称的用户。如果没有,将创建一个新用户并登录;否则,现有用户将直接登录。
-
最后,修改登录模板
login.html,以允许 Twitter 登录。在social-logins选项卡内添加以下行:<a href="{{ url_for('auth.twitter_login',next=url_for('auth.home')) }}">Login via Twitter</a>
它是如何工作的...
此配方的工作方式与之前配方中的 Facebook 和 Google 登录类似。
信息
类似地,我们可以集成 LinkedIn、GitHub 和其他数百个提供 OAuth 登录和身份验证支持的第三方提供者。是否实现更多集成取决于您。以下链接已添加供您参考:
领英:learn.microsoft.com/en-us/linkedin/shared/authentication/authentication
GitHub:docs.github.com/en/developers/apps/building-oauth-apps
使用 LDAP 进行身份验证
LDAP 本质上是一种互联网协议,用于从服务器查找有关用户、证书、网络指针等信息,其中数据存储在目录式结构中。在 LDAP 的多个用例中,最流行的是单点登录功能,用户只需登录一次即可访问多个服务,因为凭据在整个系统中是共享的。
准备工作
在这个配方中,我们将创建一个登录页面,类似于我们在本章第一篇配方创建简单的基于会话的认证中创建的页面。用户可以使用他们的 LDAP 凭证登录。如果凭证在提供的 LDAP 服务器上成功认证,用户将被登录。
如果你已经有可以访问的 LDAP 服务器,可以自由跳过本节中解释的 LDAP 设置说明。
第一步是获取访问 LDAP 服务器的权限。这可以是一个已经托管在某处的服务器,或者你可以创建自己的本地 LDAP 服务器。启动演示 LDAP 服务器的最简单方法是通过使用 Docker。
重要
这里,我们假设你之前有 Docker 的经验,并且已经在你的机器上安装了 Docker。如果没有,请参阅docs.docker.com/get-started/。
要使用 Docker 创建 LDAP 服务器,请在终端运行以下命令:
$ docker run -p 389:389 -p 636:636 --name my-openldap-container --detach osixia/openldap:1.5.0
一旦前面的命令成功执行,通过以下方式测试服务器,搜索用户名为admin和密码为admin的示例用户:
$ docker exec my-openldap-container ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
前面命令的成功执行表明 LDAP 服务器正在运行,并准备好使用。
小贴士
更多关于 OpenLDAP Docker 镜像的信息,请参阅https:/``/``github.``com/``osixia/``docker-``openldap。
现在,使用以下代码安装将帮助我们的应用程序与 LDAP 服务器通信的 Python 库:
$ pip install python-ldap
如何操作...
要启用应用程序的 LDAP 认证,请按照以下步骤操作:
-
和往常一样,从
my_app/__init__.py中的配置部分开始,如下所示:import ldapapp.config['LDAP_PROVIDER_URL'] = 'ldap://localhost'def get_ldap_connection():conn = ldap.initialize(app.config['LDAP_PROVIDER_URL'])return conn
在前面的代码片段中,我们导入了ldap,然后创建了一个指向 LDAP 服务器地址的应用配置选项。这之后是创建一个简单的函数get_ldap_connection,该函数在服务器上创建 LDAP 连接对象,然后返回该连接对象。
-
接下来,修改视图,即
my_app/auth/views.py,在这里创建了一个新的路由ldap_login,以便通过 LDAP 进行登录,如下所示:import ldapfrom my_app import db, login_manager,get_ldap_connection@auth.route("/ldap-login", methods=['GET', 'POST'])def ldap_login():if current_user.is_authenticated:flash('Your are already logged in.', 'info')return redirect(url_for('auth.home'))form = LoginForm()if form.validate_on_submit():username = request.form.get('username')password = request.form.get('password')try:conn = get_ldap_connection()conn.simple_bind_s('cn=%s,dc=example,dc=org' % username,password)except ldap.INVALID_CREDENTIALS:flash('Invalid username or password.Please try again.', 'danger')return render_template('login.html',form=form)user = User.query.filter_by(username=username).first()if not user:user = User(username, password)db.session.add(user)db.session.commit()login_user(user)flash('You have successfully logged in.','success')return redirect(url_for('auth.home'))if form.errors:flash(form.errors, 'danger')return render_template('login.html', form=form)
在这里,我们首先检查用户是否已经认证。如果已经认证,我们将他们重定向到主页;否则,我们继续前进。然后我们使用了LoginForm,这是我们之前在创建简单的基于会话的认证配方中创建的,因为我们也需要用户名和密码。接下来,我们验证了表单,然后使用get_ldap_connection获取连接对象。之后,应用程序尝试使用simple_bind_s从 LDAP 服务器认证用户。注意这个方法内部的字符串'cn=%s,dc=example,dc=org',这个字符串可能因服务器内部配置的不同而有所不同。如果你不知道这些细节,请务必联系你的 LDAP 服务器管理员。
如果用户成功认证,那么在我们的本地数据库中创建一个新的用户记录,并且用户将被登录。否则,LDAP 连接失败并抛出错误INVALID_CREDENTIALS,然后被捕获并相应地通知用户。
小贴士
我们刚刚见证了可重用组件的力量!正如你所见,LoginForm现在已被用于两个不同的目的。这是一个好的编程实践。
-
最后,修改登录模板
login.html以允许 LDAP 登录,如下所示:{% extends 'home.html' %}{% block container %}<div class="top-pad"><ul class="nav nav-tabs"><li class="active"><a href="#simple-form" data-toggle="tab">Old Style Login</a></li><li><a href="#social-logins" data-toggle="tab">Social Logins</a></li><li><a href="#ldap-form" data-toggle="tab">LDAPLogin</a></li></ul><div class="tab-content"><div class="tab-pane active" id="simple-form"><br/><formmethod="POST"action="{{ url_for('auth.login') }}"role="form">{{ form.csrf_token }}<div class="form-group">{{ form.username.label }}: {{ form.username() }}</div><div class="form-group">{{ form.password.label }}: {{ form.password() }}</div><button type="submit" class="btn btn-default">Submit</button></form></div><div class="tab-pane" id="social-logins"><a href="{{ url_for('auth.facebook_login',next=url_for('auth.home')) }}">Login via Facebook</a><br/><a href="{{ url_for('auth.google_login',next=url_for('auth.home')) }}">Login via Google</a><br/><a href="{{ url_for('auth.twitter_login',next=url_for('auth.home')) }}">Login via Twitter</a></div><div class="tab-pane" id="ldap-form"><br/><formmethod="POST"action="{{ url_for('auth.ldap_login') }}"role="form">{{ form.csrf_token }}<div class="form-group">{{ form.username.label }}: {{ form.username() }}</div><div class="form-group">{{ form.password.label }}: {{ form.password() }}</div><button type="submit" class="btn btn-default">Submit</button></form></div></div></div>{% endblock %}
它是如何工作的…
带有 LDAP 标签的新登录屏幕应如下截图所示:

图 6.11 – LDAP 登录屏幕
在这里,用户只需输入他们的用户名和密码。如果凭证正确,用户将被登录并带到主屏幕;否则,将发生错误。
参见
你可以在en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol和www.python-ldap.org了解更多关于 LDAP 的信息。
第七章:RESTful API 构建
应用程序编程接口(API)可以概括为开发者与应用程序的接口。正如最终用户有一个可见的前端用户界面,他们可以通过它与应用程序交互和交流一样,开发者也需要一个与之交互的接口。表示状态转换(REST)不是一个协议或标准。它只是一个软件架构风格或一系列为编写应用程序而定义的建议,其目的是简化应用程序内部和外部的接口。当以符合 REST 定义的方式编写网络服务 API 时,它们被称为 RESTful API。保持 RESTful 可以使 API 与内部应用程序细节解耦。这导致易于扩展并保持简单。统一的接口确保每个请求都有文档记录。
信息
关于 REST 或简单的对象访问协议(SOAP)哪个更好,这是一个有争议的话题。这实际上是一个主观问题,因为它取决于需要做什么。每种方法都有自己的优点,应根据应用程序的需求进行选择。
REST 调用用于将 API 分割成逻辑资源,这些资源可以通过 HTTP 请求访问和操作,其中每个请求都包含以下方法之一——GET、POST、PUT、PATCH和DELETE(可能有更多,但这些都是最常用的)。这些方法中的每一个都有其特定的含义。REST 的一个关键隐含原则是资源的逻辑分组应该是易于理解的,因此可以提供简单性和可移植性。
我们有一个名为product的资源,正如我们在本书中迄今为止所使用的。现在,让我们看看我们如何逻辑地将我们的 API 调用映射到资源分割:
-
GET /products/1: 这将获取 ID 为1的产品 -
GET /products: 这将获取产品列表 -
POST /products: 这将创建一个新的产品 -
PUT /products/1: 这将替换或重新创建 ID 为1的产品 -
PATCH /products/1: 这将部分更新 ID 为1的产品 -
DELETE /products/1: 这将删除 ID 为1的产品
在本章中,我们将介绍以下菜谱:
-
创建基于类的 REST 接口
-
创建基于扩展的 REST 接口
-
创建完整的 RESTful API
创建基于类的 REST 接口
我们在第四章的编写基于类的视图菜谱中看到了如何在 Flask 中使用可插拔视图的概念,与视图一起工作。在这个菜谱中,我们现在将看到如何使用相同的方法来创建视图,这些视图将为我们的应用程序提供 REST 接口。
准备就绪
让我们看看一个简单的视图,它将处理对Product模型的 REST 风格调用。
如何做到这一点...
我们只需修改views.py中的产品处理视图,以扩展MethodView类:
import json
from flask.views import MethodView
class ProductView(MethodView):
def get(self, id=None, page=1):
if not id:
products = Product.query.paginate(page,
10).items
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name
}
else:
product =
Product.query.filter_by(id=id).first()
if not product:
abort(404)
res = json.dumps({
'name': product.name,
'price': product.price,
'category': product.category.name
})
return res
紧接着的 get() 方法会查找产品并发送回 JSON 结果。同样,我们也可以编写 post()、put() 和 delete() 方法:
def post(self):
# Create a new product.
# Return the ID/object of the newly created product.
return
def put(self, id):
# Update the product corresponding provided id.
# Return the JSON corresponding updated product.
return
def delete(self, id):
# Delete the product corresponding provided id.
# Return success or error message.
return
许多人会质疑为什么这里没有路由。要包含路由,我们必须做以下事情:
product_view = ProductView.as_view('product_view')
app.add_url_rule('/products/', view_func=product_view,
methods=['GET', 'POST'])
app.add_url_rule('/products/<int:id>',
view_func=product_view,
methods=['GET', 'PUT', 'DELETE'])
这里第一条语句将类内部转换为实际的可用于路由系统的视图函数。接下来的两个语句是与可以进行的调用相对应的 URL 规则。
它是如何工作的...
MethodView 类识别了发送请求中使用的 HTTP 方法类型,并将名称转换为小写。然后,它将此与类中定义的方法进行匹配,并调用匹配的方法。因此,如果我们向 ProductView 发起 GET 调用,它将自动映射到 get() 方法并相应处理。
创建基于扩展的 REST 接口
在之前的菜谱 创建基于类的 REST 接口 中,我们看到了如何使用可插拔视图创建 REST 接口。在这个菜谱中,我们将使用一个名为 Flask-RESTful 的扩展,它是基于我们在上一个菜谱中使用的相同可插拔视图编写的,但它通过自己处理许多细微差别,使我们开发者能够专注于实际的 API 开发。它也独立于 对象关系映射(ORM),因此我们想要使用的 ORM 上没有附加条件。
准备工作
首先,我们将从安装扩展开始:
$ pip install flask-restful
我们将修改上一个菜谱中的目录应用程序,使用此扩展添加 REST 接口。
如何做...
如往常一样,从 my_app/__init__.py 中应用程序配置的更改开始,它看起来像以下几行代码:
from flask_restful import Api
api = Api(app)
在这里,app 是我们的 Flask 应用程序对象/实例。
接下来,在 views.py 文件中创建 API。在这里,我们只是尝试了解如何安排 API 的框架。实际的方法和处理器将在 创建完整的 RESTful API 菜谱中介绍:
from flask_restful import Resource
from my_app import api
class ProductApi(Resource):
def get(self, id=None):
# Return product data
return 'This is a GET response'
def post(self):
# Create a new product
return 'This is a POST response'
def put(self, id):
# Update the product with given id
return 'This is a PUT response'
def delete(self, id):
# Delete the product with given id
return 'This is a DELETE response'
前面的 API 结构是自我解释的。考虑以下代码:
api.add_resource(
ProductApi,
'/api/product',
'/api/product/<int:id>'
)
在这里,我们为 ProductApi 创建了路由,并且可以根据需要指定多个路由。
它是如何工作的...
我们将使用 requests 库在 Python shell 中查看这个 REST 接口是如何工作的。
信息
requests 是一个非常流行的 Python 库,它使得 HTTP 请求的渲染变得非常简单。只需运行 $ pip install requests 命令即可安装。
命令将显示以下信息:
>>> import requests
>>> res = requests.get('http://127.0.0.1:5000/api/product')
>>> res.json()
'This is a GET response'
>>> res = requests.post('http://127.0.0.1:5000/api/product')
>>> res.json()
'This is a POST response'
>>> res = requests.put('http://127.0.0.1:5000/api/product/1')
>>> res.json()
'This is a PUT response'
>>> res = requests.delete('http://127.0.0.1:5000/api/product/1')
>>> res.json()
'This is a DELETE response'
在前面的片段中,我们看到所有我们的请求都正确地路由到了相应的方法;这从收到的响应中可以明显看出。
参见
参考以下菜谱,创建完整的 RESTful API,以查看本菜谱中的 API 框架如何变得生动。
创建完整的 RESTful API
在这个菜谱中,我们将把在最后一个菜谱 创建基于扩展的 REST 接口 中创建的 API 结构转换为完整的 RESTful API。
准备工作
我们将基于最后一个菜谱中的 API 骨架来创建一个完全独立的 SQLAlchemy RESTful API。虽然我们将使用 SQLAlchemy 作为演示目的的 ORM,但这个菜谱可以用类似的方式为任何 ORM 或底层数据库编写。
如何做到这一点...
以下代码行是Product模型的完整 RESTful API。这些代码片段将放入views.py文件中。
从导入开始并添加parser:
import json
from flask_restful import Resource, reqparse
parser = reqparse.RequestParser()
parser.add_argument('name', type=str)
parser.add_argument('price', type=float)
parser.add_argument('category', type=dict)
在前面的代码片段中,我们为POST和PUT请求中预期的参数创建了parser。请求期望每个参数都有一个值。如果任何参数缺少值,则使用None作为值。
按照以下代码块所示编写方法来获取产品:
class ProductApi(Resource):
def get(self, id=None, page=1):
if not id:
products = Product.query.paginate(page=page,
per_page=10).items
else:
products = [Product.query.get(id)]
if not products:
abort(404)
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name
}
return json.dumps(res)
前面的get()方法对应于GET请求,如果没有传递id,则返回分页的产品列表;否则,返回相应的产品。
创建以下方法来添加一个新的产品:
def post(self):
args = parser.parse_args()
name = args['name']
price = args['price']
categ_name = args['category']['name']
category =
Category.query.filter_by(name=categ_name).first()
if not category:
category = Category(categ_name)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
res = {}
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name,
}
return json.dumps(res)
前面的post()方法将通过发送POST请求来创建一个新的产品。
编写以下方法来更新或本质上替换现有的产品记录:
def put(self, id):
args = parser.parse_args()
name = args['name']
price = args['price']
categ_name = args['category']['name']
category =
Category.query.filter_by(name=categ_name).first()
Product.query.filter_by(id=id).update({
'name': name,
'price': price,
'category_id': category.id,
})
db.session.commit()
product = Product.query.get_or_404(id)
res = {}
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name,
}
return json.dumps(res)
在前面的代码中,我们使用PUT请求更新了一个现有的产品。在这里,即使我们打算更改其中的一些参数,我们也应该提供所有参数。这是因为PUT被定义成传统的工作方式。如果我们想要一个只传递我们打算更新的参数的请求,那么我们应该使用PATCH请求。我敦促你自己尝试一下。
使用以下方法删除产品:
def delete(self, id):
product = Product.query.filter_by(id=id)
product.delete()
db.session.commit()
return json.dumps({'response': 'Success'})
最后,但同样重要的是,我们有DELETE请求,它将简单地删除与传递的id匹配的产品。
以下是我们 API 可以容纳的所有可能路由的定义:
api.add_resource(
ProductApi,
'/api/product',
'/api/product/<int:id>',
'/api/product/<int:id>/<int:page>'
)
它是如何工作的...
为了测试和查看它是如何工作的,我们可以通过 Python shell 使用requests库发送多个请求:
>>> import requests
>>> import json
>>> res = requests.get('http://127.0.0.1:5000/api/product')
>>> res.json()
{'message': 'The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.'}
我们发送了一个GET请求来获取产品列表,但没有任何记录。现在让我们创建一个新的产品:
>>> d = {'name': u'iPhone', 'price': 549.00, 'category':
... {'name':'Phones'}}
>>> res = requests.post('http://127.0.0.1:5000/api/product', data=json.
... dumps(d), headers={'Content-Type': 'application/json'})
>>> res.json()
'{"1": {"name": "iPhone", "price": 549.0, "category": "Phones"}}'
我们发送了一个POST请求来创建一个带有一些数据的产品。注意请求中的headers参数。在 Flask-RESTful 中发送的每个POST请求都应该有这个头。现在,我们应该再次查找产品列表:
>>> res = requests.get('http://127.0.0.1:5000/api/product')
>>> res.json()
'{"1": {"name": "iPhone", "price": 549.0, "category": "Phones"}}'
如果我们再次通过GET请求查找产品,我们可以看到现在数据库中有一个新创建的产品。
我将把它留给你去尝试独立地整合其他 API 请求。
重要
RESTful API 的一个重要方面是使用基于令牌的认证,以允许只有有限的认证用户能够使用和调用 API。我敦促你自己去探索这一点。我们在第六章中介绍了用户认证的基础,这将为这个概念提供基础。
第八章:Flask 应用程序的管理界面
许多应用程序需要一种界面,它可以为某些用户提供特殊权限,并可用于维护和升级应用程序的资源。例如,我们可以在电子商务应用程序中有一个界面,允许一些特殊用户创建类别、产品等。一些用户可能拥有特殊权限来处理在网站上购物的其他用户,处理他们的账户信息等。同样,可能存在许多需要将应用程序界面的某些部分从普通用户中隔离出来的情况。
与非常流行的基于 Python 的 Web 框架 Django 相比,Flask 默认不提供任何管理界面。虽然这可能会被许多人视为一个缺点,但这给了开发者根据他们的需求创建管理界面的灵活性,并完全控制应用程序。
我们可以选择从头开始为我们自己的应用程序编写管理界面,或者使用 Flask 的扩展,它为我们做了大部分工作,并允许我们根据需要自定义逻辑。在 Flask 中创建管理界面的一个非常流行的扩展是 Flask-Admin (flask-admin.readthedocs.io/en/latest/)。它受到 Django 管理界面的启发,但以一种让开发者完全控制应用程序的外观、感觉和功能的方式实现。在本章中,我们将从创建自己的管理界面开始,然后转向使用 Flask-Admin 扩展,并根据需要对其进行微调。
在本章中,我们将涵盖以下配方:
-
创建简单的 CRUD 界面
-
使用 Flask-Admin 扩展
-
将模型注册到 Flask-Admin
-
创建自定义表单和操作
-
使用 WYSIWYG 编辑器进行
textarea集成 -
创建用户角色
创建简单的 CRUD 界面
CRUD 代表 创建、读取、更新和删除。管理界面的基本需求是能够根据需要创建、修改或删除应用程序中的记录/资源。我们将创建一个简单的管理界面,允许管理员用户对其他普通用户通常无法操作的记录执行这些操作。
准备工作
我们将从第六章“在 Flask 中认证”中的“使用 Flask-Login 扩展进行认证”配方开始,并添加带有管理员界面的管理员认证,这将仅允许管理员用户创建、更新和删除用户记录。在这里,在这个配方中,我将涵盖理解这些概念所必需的一些特定部分。对于完整的应用程序,您可以参考本书提供的代码示例。
如何做到这一点...
要创建一个简单的管理界面,请执行以下步骤:
-
首先,通过在
auth/models.py中的User模型中添加一个名为admin的新BooleanField字段来修改模型。这个字段将帮助识别用户是否是管理员:from wtforms import BooleanFieldclass User(db.Model):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(100))pwdhash = db.Column(db.String())admin = db.Column(db.Boolean())def __init__(self, username, password,admin=False):self.username = usernameself.pwdhash =generate_password_hash(password)self.admin = admindef is_admin(self):return self.admin
前面的方法只是返回管理员字段的值。这可以根据我们的需求有自定义的实现。
小贴士
由于这是添加到User模型的新字段,因此需要进行数据库迁移。你可以参考第三章中关于使用 Alembic 和 Flask-Migrate 迁移数据库的配方,Flask 中的数据建模,以获取更多详细信息。
-
在
auth/models.py中创建两个表单,这些表单将由管理员视图使用:class AdminUserCreateForm(FlaskForm):username = StringField('Username',[InputRequired()])password = PasswordField('Password',[InputRequired()])admin = BooleanField('Is Admin ?')class AdminUserUpdateForm(FlaskForm):username = StringField('Username',[InputRequired()])admin = BooleanField('Is Admin ?') -
现在,修改
auth/views.py中的视图以实现管理员界面:from functools import wrapsfrom flask import abortfrom my_app.auth.models import AdminUserCreateForm,AdminUserUpdateFormdef admin_login_required(func):@wraps(func)def decorated_view(*args, **kwargs):if not current_user.is_admin():return abort(403)return func(*args, **kwargs)return decorated_view
前面的代码是admin_login_required装饰器,它的工作方式与login_required装饰器类似。在这里,区别在于它需要与login_required一起实现,并检查当前登录的用户是否是管理员。
-
创建以下处理程序,这些处理程序将用于创建简单的管理员界面。注意
@admin_login_required装饰器的使用。其他所有内容基本上都是标准的,正如我们在本书前面章节中学到的,这些章节专注于视图和认证处理。所有处理程序都将放在auth/views.py中:@auth.route('/admin')@login_required@admin_login_requireddef home_admin():return render_template('admin-home.html')@auth.route('/admin/users-list')@login_required@admin_login_requireddef users_list_admin():users = User.query.all()return render_template('users-list-admin.html',users=users)@auth.route('/admin/create-user', methods=['GET','POST'])@login_required@admin_login_requireddef user_create_admin():form = AdminUserCreateForm()if form.validate_on_submit():username = form.username.datapassword = form.password.dataadmin = form.admin.dataexisting_username = User.query.filter_by(username=username).first()if existing_username:flash('This username has been already taken.Try another one.','warning')return render_template('register.html',form=form)user = User(username, password, admin)db.session.add(user)db.session.commit()flash('New User Created.', 'info')returnredirect(url_for('auth.users_list_admin'))if form.errors:flash(form.errors, 'danger')return render_template('user-create-admin.html',form=form)
前面的方法允许管理员用户在系统中创建新用户。这与register()方法的工作方式非常相似,但允许管理员设置用户的admin标志。
以下方法允许管理员用户更新其他用户的记录:
@auth.route('/admin/update-user/<id>', methods=['GET', 'POST'])
@login_required
@admin_login_required
def user_update_admin(id):
user = User.query.get(id)
form = AdminUserUpdateForm(
username=user.username,
admin=user.admin
)
if form.validate_on_submit():
username = form.username.data
admin = form.admin.data
User.query.filter_by(id=id).update({
'username': username,
'admin': admin,
})
db.session.commit()
flash('User Updated.', 'info')
return
redirect(url_for('auth.users_list_admin'))
if form.errors:
flash(form.errors, 'danger')
return render_template('user-update-admin.html', form=form,
user=user)
然而,根据编写 Web 应用程序的最佳实践,我们不允许管理员简单地查看和更改任何用户的密码。在大多数情况下,更改密码的权限应该属于账户的所有者。尽管在某些情况下管理员可以更新密码,但绝对不应该允许他们看到用户之前设置的密码。这个话题在创建自定义表单和操作配方中进行了讨论。
以下方法处理管理员删除用户的情况:
@auth.route('/admin/delete-user/<id>')
@login_required
@admin_login_required
def user_delete_admin(id):
user = User.query.get(id)
db.session.delete(user)
db.session.commit()
flash('User Deleted.', 'info')
return redirect(url_for('auth.users_list_admin'))
user_delete_admin()方法实际上应该在POST请求上实现。这留给你自己实现。
- 在模型和视图之后,创建一些模板来补充它们。对于你们中的许多人来说,从视图本身的代码中可能已经很明显,我们需要添加四个新的模板,即
admin-home.html、user-create-admin.html、user-update-admin.html和users-list-admin.html。这些模板的工作原理将在下一节中展示。现在,你应该能够自己实现这些模板;然而,为了参考,代码总是与书中提供的示例一起提供。
它是如何工作的...
首先,我们在应用程序中添加了一个菜单项;这提供了一个直接链接到管理员主页,其外观如下所示截图:

图 8.1 – 管理员访问菜单项
用户必须以管理员身份登录才能访问此页面和其他相关管理员页面。如果用户没有以管理员身份登录,则应用程序将显示错误,如下面的截图所示:

图 8.2 – 非管理员用户的禁止访问错误
信息
在您能够以管理员身份登录之前,需要创建一个管理员用户。要创建管理员用户,您可以从命令行使用 SQL 查询在 SQLAlchemy 中对数据库进行更改。另一种更简单但有些不规范的实现方法是,在auth/models.py中将admin标志更改为True,然后注册一个新用户。这个新用户将成为管理员用户。请确保在此操作完成后,将admin标志恢复为默认的False。
对于已登录的管理员用户,管理员主页将如下所示:

图 8.3 – 管理员主页
从这里,管理员可以查看系统上的用户列表或创建新用户。编辑或删除用户的选项将直接在用户列表页面上提供。
使用 Flask-Admin 扩展
Flask-Admin是一个可用的扩展,它以更简单、更快的速度帮助我们为应用程序创建管理员界面。本章的所有后续食谱都将专注于使用和扩展此扩展。
准备工作
首先,我们需要安装Flask-Admin扩展:
$ pip install Flask-Admin
我们将从之前的食谱扩展我们的应用程序,并在此基础上继续构建。
如何操作…
使用Flask-Admin扩展将简单的管理员界面添加到任何 Flask 应用程序中只是一个语句的问题。
简单地将以下行添加到应用程序配置的my_app/__init__.py中:
from flask_admin import Admin
app = Flask(__name__)
# Add any other application configurations
admin = Admin(app)
您还可以添加自己的视图;这就像添加一个继承自BaseView类的新类作为新视图一样简单,如下面的代码块所示。此代码块位于auth/views.py中:
from flask_admin import BaseView, expose
class HelloView(BaseView):
@expose('/')
def index(self):
return self.render('some-template.html')
然后,将此视图添加到my_app/__init__.py中 Flask 配置的admin对象中:
import my_app.auth.views as views
admin.add_view(views.HelloView(name='Hello'))
这里需要注意的一点是,此页面默认没有实现任何身份验证或授权逻辑,它将对所有人开放。原因在于Flask-Admin对现有的身份验证系统没有任何假设。由于我们正在使用Flask-Login为我们的应用程序,您可以在HelloView类中添加一个名为is_accessible()的方法:
def is_accessible(self):
return current_user.is_authenticated and \
current_user.is_admin()
工作原理…
只需像本食谱的第一步所演示的那样,使用Flask-Admin扩展中的Admin类初始化应用程序,就会显示一个基本的管理员页面,如下面的截图所示:

图 8.4 – 使用 Flask-Admin 的管理员主页
注意截图中的 URL,它是http://127.0.0.1:5000/admin/。请特别注意 URL 末尾的正斜杠(/)。如果你错过了这个正斜杠,那么它将打开上一个食谱中的网页。
在第二步中添加自定义的HelloView会使管理页面看起来如下截图所示:

图 8.5 – 添加一个虚拟的 Hello 视图
更多内容…
在实现前面的代码之后,仍然有一个行政视图不会完全受到用户保护,并且将是公开可用的。这将是一个行政主页。为了使其仅对管理员可用,我们必须从AdminIndexView继承并实现is_accessible():
from flask_admin import BaseView, expose, AdminIndexView
class MyAdminIndexView(AdminIndexView):
def is_accessible(self):
return current_user.is_authenticated and
current_user.is_admin()
然后,只需将此视图传递到应用程序配置中的admin对象,作为index_view,我们就完成了:
admin = Admin(app, index_view=views.MyAdminIndexView())
这种方法使得所有我们的行政视图仅对管理员用户可访问。我们还可以根据需要实现任何权限或条件访问规则在is_accessible()中。
使用 Flask-Admin 注册模型
在之前的食谱中,我们学习了如何使用Flask-Admin扩展开始创建我们应用程序的行政界面/视图。在本食谱中,我们将检查如何使用执行 CRUD 操作的功能实现现有模型的行政视图。
准备工作
我们将从之前的食谱扩展我们的应用程序,包括User模型的行政界面。
如何做到这一点…
再次强调,使用Flask-Admin,将模型注册到行政界面非常简单;执行以下步骤:
-
只需将以下单行代码添加到
auth/views.py:from flask_admin.contrib.sqla import ModelView# Other admin configuration as shown in last recipeadmin.add_view(ModelView(views.User, db.session))
在这里,在第一行中,我们从flask_admin.contrib.sqla中导入了ModelView,这是由Flask-Admin提供的,用于集成 SQLAlchemy 模型。
观察此食谱中“如何工作…”部分的第一个步骤对应的截图(图 8.6),我们大多数人都会同意,向任何用户显示密码散列,无论是管理员还是普通用户,都没有意义。此外,Flask-Admin提供的默认模型创建机制将无法为我们创建的User模型工作,因为我们User模型中有一个__init__()方法。该方法期望三个字段(username、password和is_admin)的值,而Flask-Admin中实现的模型创建逻辑非常通用,在模型创建过程中不提供任何值。
-
现在,自定义
Flask-Admin的默认行为,使其成为你自己的,在auth/views.py中修复User创建机制并隐藏密码散列:from wtforms import PasswordFieldfrom flask_admin.contrib.sqla import ModelViewclass UserAdminView(ModelView):column_searchable_list = ('username',)column_sortable_list = ('username', 'admin')column_exclude_list = ('pwdhash',)form_excluded_columns = ('pwdhash',)form_edit_rules = ('username', 'admin')def is_accessible(self):return current_user.is_authenticated andcurrent_user.is_admin()
上述代码展示了我们的User模型 admin 视图将遵循的一些规则和设置。这些规则通过其名称即可解释。其中一些,如column_exclude_list和form_excluded_columns可能看起来有些令人困惑。前者将从 admin 视图中排除所提到的列,并避免在搜索、创建和其他 CRUD 操作中使用它们。后者将防止字段在 CRUD 操作的表单中显示。
-
在
auth/views.py中创建一个方法,覆盖从模型创建表单,并添加一个password字段,该字段将用于替代密码散列:def scaffold_form(self):form_class = super(UserAdminView,self).scaffold_form()form_class.password =PasswordField('Password')return form_class -
然后,覆盖
auth/views.py中的模型创建逻辑以适应应用程序:def create_model(self, form):model = self.model(form.username.data, form.password.data,form.admin.data)form.populate_obj(model)self.session.add(model)self._on_model_change(form, model, True)self.session.commit() -
最后,通过编写以下代码将此模型添加到应用程序配置中的
admin对象my_app/__init__.py:admin.add_view(views.UserAdminView(views.User,db.session))
信息
注意self._on_model_change(form, model, True)语句。在这里,最后一个参数True表示调用是为创建新记录。
工作原理…
第一步将创建一个新的User模型 admin 视图,其外观如下截图:

图 8.6 – 没有自定义逻辑的用户列表
在完成所有步骤后,User模型的 admin 界面将类似于以下截图:

图 8.7 – 隐藏密码散列的用户列表
这里有一个搜索框,没有显示密码散列。用户创建和编辑视图也有所更改。我建议您运行应用程序亲自查看。
创建自定义表单和操作
在这个菜谱中,我们将使用Flask-Admin提供的表单创建一个自定义表单。此外,我们还将使用自定义表单创建一个自定义操作。
准备工作
在前面的菜谱中,我们看到User记录更新编辑表单视图没有更新用户密码的选项。表单看起来如下截图:

图 8.8 – 内置用户编辑表单
在这个菜谱中,我们将自定义此表单,允许管理员更新任何用户的密码。
如何操作…
实现此功能只需修改views.py文件:
-
首先,从
Flask-Admin表单导入rules:from flask_admin.form import rules
在前面的菜谱中,我们有一个form_edit_rules,它只有两个字段 – 即username和admin – 作为列表。这表示在User模型的更新视图中,管理员用户可用的字段。
-
更新密码不仅仅是将一个字段添加到
form_edit_rules列表中;这是因为我们不存储明文密码。相反,我们存储用户无法直接编辑的密码散列。我们需要从用户那里输入密码,然后在存储时将其转换为散列。请看以下代码中如何做到这一点:form_edit_rules = ('username', 'admin',rules.Header('Reset Password'),'new_password', 'confirm')form_create_rules = ('username', 'admin', 'notes', 'password')
前面的代码片段表示我们现在在表单中有一个标题;这个标题将密码重置部分与其他部分分开。然后,添加两个新字段new_password和confirm,这将帮助我们安全地更改密码。
-
这也要求对
scaffold_form()方法进行更改,以便在表单渲染时两个新字段是有效的:def scaffold_form(self):form_class = super(UserAdminView,self).scaffold_form()form_class.password =PasswordField('Password')form_class.new_password = PasswordField('NewPassword')form_class.confirm = PasswordField('ConfirmNew Password')return form_class -
最后,实现
update_model()方法,该方法在尝试更新记录时被调用:def update_model(self, form, model):form.populate_obj(model)if form.new_password.data:if form.new_password.data !=form.confirm.data:flash('Passwords must match')returnmodel.pwdhash = generate_password_hash(form.new_password.data)self.session.add(model)self._on_model_change(form, model, False)self.session.commit()
在前面的代码中,我们首先确保两个字段中输入的密码相同。如果相同,我们将继续重置密码以及任何其他更改。
它是如何工作的…
用户更新表单现在将看起来如下截图所示:

图 8.9 – 带自定义操作的定制表单
在这里,如果我们同时在两个密码字段中输入相同的密码,用户密码将被更新。
使用 WYSIWYG 编辑器进行 textarea 集成
作为网站用户,我们都知道使用普通的textarea字段编写格式化的文本是一个噩梦。有一些插件让我们的工作变得更简单,并将简单的textarea字段转换为textarea字段。
准备工作
我们首先在我们的User模型中添加一个新的textarea字段用于笔记,然后将其与 CKEditor 集成以编写格式化文本。这包括向一个普通的textarea字段添加一个 JavaScript 库和一个 CSS 类,将其转换为 CKEditor 兼容的textarea字段。
如何做…
要将 CKEditor 集成到你的应用程序中,请执行以下步骤:
-
首先,按照以下方式在
auth/models.py中的User模型中添加notes字段:class User(db.Model):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(100))pwdhash = db.Column(db.String())admin = db.Column(db.Boolean())notes = db.Column(db.UnicodeText)def __init__(self, username, password,admin=False, notes=''):self.username = usernameself.pwdhash =generate_password_hash(password)self.admin = adminself.notes = notes
重要
为了添加一个新字段,你可能需要运行迁移脚本。你可以参考第三章中关于“使用 Alembic 和 Flask-Migrate 迁移数据库”的配方,Flask 中的数据建模,以获取更多详细信息。
-
然后,在
auth/models.py中创建一个自定义的wtform小部件和一个用于 CKEditortextarea字段的字段:from wtforms import widgets, TextAreaFieldclass CKTextAreaWidget(widgets.TextArea):def __call__(self, field, **kwargs):kwargs.setdefault('class_', 'ckeditor')return super(CKTextAreaWidget,self).__call__(field, **kwargs)
在前面的自定义小部件中,我们向TextArea小部件添加了一个ckeditor类。有关 WTForms 小部件的更多信息,你可以参考第五章中关于“创建自定义小部件”的配方,使用 WTForms 的 Web 表单。
-
接下来,创建一个继承自
TextAreaField的自定义字段,并将其更新为使用之前步骤中创建的小部件:class CKTextAreaField(TextAreaField):widget = CKTextAreaWidget()
在前面的代码中的自定义字段中,我们将小部件设置为 CKTextAreaWidget,当这个字段被渲染时,将会添加 ckeditor CSS 类。
-
接下来,修改
auth/views.py中的UserAdminView类中的form_edit_rules,在那里我们指定用于create和edit表单的模板。此外,用CKTextAreaField替换notes的正常TextAreaField对象。确保从auth/models.py导入CKTextAreaField:form_overrides = dict(notes=CKTextAreaField)create_template = 'edit.html'edit_template = 'edit.html'
在前面的代码块中,form_overrides 允许用 CKEditor 的 textarea 字段覆盖正常的 textarea 字段。
-
此菜谱的最后一部分是前面提到的
templates/edit.html模板:{% extends 'admin/model/edit.html' %}{% block tail %}{{ super() }}<script src="img/pre>/4.20.1/standard/ckeditor.js">
{% endblock %}
在这里,我们扩展了 Flask-Admin 提供的默认 edit.html 文件,并添加了 CKEditor JS 文件,以便我们的 CKTextAreaField 中的 ckeditor 类能够工作。
它是如何工作的…
在我们完成所有更改后,创建用户表单将看起来像下面的截图;特别是,注意笔记字段:

图 8.10 – 使用 WYSIWYG 编辑器创建的笔记字段
在这里,输入到笔记字段中的任何内容在保存时都会自动格式化为 HTML,并且以后可以在任何地方用于显示目的。
参见
这个菜谱受到了 Flask-Admin 作者的 gist 的启发。该 gist 可在gist.github.com/mrjoes/5189850找到。
您也可以选择直接使用 Flask-CKEditor 扩展,该扩展可在flask-ckeditor.readthedocs.io/en/latest/找到。我没有使用这个扩展,因为我想要从较低级别演示这个概念。
创建用户角色
到目前为止,我们已经发现如何使用 is_accessible() 方法轻松创建一组特定管理员用户可访问的视图。这可以扩展到不同的场景,其中特定的用户将能够查看特定的视图。在模型中,还有另一种在更细粒度级别实现用户角色的方法,其中角色决定了用户能否执行所有、一些或任何 CRUD 操作。
准备工作
在这个菜谱中,我们将探索创建用户角色的基本方法,其中管理员用户只能执行他们有权执行的操作。
信息
记住,这只是实现用户角色的一种方式。还有许多更好的方法来做这件事,但这种方法似乎是最好的,可以展示创建用户角色的概念。一种方法可以是创建用户组并将角色分配给组,而不是单个用户。另一种方法可以是更复杂的基于策略的用户角色,这将包括根据复杂的业务逻辑定义角色。这种方法通常由 ERP、CRM 等商业系统采用。
如何实现...
要向应用程序添加基本用户角色,请执行以下步骤:
-
首先,在
auth/models.py中的User模型中添加一个名为roles的字段,如下所示:class User(db.Model):id = db.Column(db.Integer, primary_key=True)username = db.Column(db.String(100))pwdhash = db.Column(db.String())admin = db.Column(db.Boolean())notes = db.Column(db.UnicodeText)roles = db.Column(db.String(4))def __init__(self, username, password,admin=False, notes='', roles='R'):self.username = usernameself.pwdhash =generate_password_hash(password)self.admin = adminself.notes = notesself.roles = self.admin and roles or ''
在这里,我们添加了一个新的字段,roles,它是一个长度为4的字符串字段。我们假设在这个字段中可能出现的条目是C、R、U和D的任何组合。具有roles值为CRUD的用户将有权执行所有操作,而任何缺失的权限将阻止用户执行该操作。请注意,读取权限始终隐含于任何管理员用户,无论是否指定。
重要
为了添加新字段,你可能需要运行迁移脚本。你可以参考第三章中关于使用 Alembic 和 Flask-Migrate 迁移数据库的配方,即在 Flask 中进行数据建模,以获取更多详细信息。
-
接下来,对
auth/views.py中的UserAdminView类进行一些修改:from flask_admin.actions import ActionsMixinclass UserAdminView(ModelView, ActionsMixin):form_edit_rules = ('username', 'admin', 'roles', 'notes',rules.Header('Reset Password'),'new_password', 'confirm')form_create_rules = ('username', 'admin', 'roles', 'notes','password')
在先前的代码中,我们只是将roles字段添加到我们的create和edit表单中。我们还继承了一个名为ActionsMixin的类。这是处理如批量删除等批量更新操作所必需的。
-
接下来,我们有需要实现条件并处理各种角色逻辑的方法:
- 首先是处理模型创建的方法:
def create_model(self, form):if 'C' not in current_user.roles:flash('You are not allowed to createusers.', 'warning')returnmodel = self.model(form.username.data, form.password.data,form.admin.data,form.notes.data)form.populate_obj(model)self.session.add(model)self._on_model_change(form, model, True)self.session.commit()
在先前的方法中,我们首先检查current_user中的roles字段是否有权限创建记录(这表示为C)。如果没有,我们显示错误消息并从方法中返回。
- 接下来是处理更新的方法:
def update_model(self, form, model):
if 'U' not in current_user.roles:
flash('You are not allowed to edit
users.', 'warning')
return
form.populate_obj(model)
if form.new_password.data:
if form.new_password.data !=
form.confirm.data:
flash('Passwords must match')
return
model.pwdhash = generate_password_hash(
form.new_password.data)
self.session.add(model)
self._on_model_change(form, model, False)
self.session.commit()
在先前的方法中,我们首先检查current_user中的roles字段是否有权限更新记录(这表示为U)。如果没有,我们显示错误消息并从方法中返回。
- 下一个方法处理删除操作:
def delete_model(self, model):
if 'D' not in current_user.roles:
flash('You are not allowed to delete
users.', 'warning')
return
super(UserAdminView, self).delete_model(model)
类似地,在先前的方法中,我们检查了current_user是否有权限删除记录。
- 最后,解决了检查相关角色和权限的需求:
def is_action_allowed(self, name):
if name == 'delete' and 'D' not in
current_user.roles:
flash('You are not allowed to delete
users.', 'warning')
return False
return True
在先前的方法中,我们检查操作是否为delete以及current_user是否有权限删除。如果没有,则显示错误消息并返回一个False值。此方法可以扩展以处理任何自定义编写的操作。
它是如何工作的...
这个菜谱的工作方式与我们的应用程序到目前为止的工作方式非常相似,除了现在,具有指定角色的用户将能够执行特定操作。否则,将显示错误信息。
用户列表现在将看起来如下截图所示:

图 8.11 – 分配给用户的管理员角色
为了测试其余的功能,例如创建新用户(包括普通用户和管理员),删除用户,更新用户记录等,我强烈建议您亲自尝试一下。
第九章:国际化和本地化
Web 应用程序通常不仅限于一个地理区域,也不只是为来自一个语言领域的人提供服务。例如,旨在为欧洲用户设计的 Web 应用程序预计将支持多种欧洲语言,例如德语、法语、意大利语和西班牙语,以及英语。本章将介绍如何在 Flask 应用程序中启用对多种语言的支持。
在任何 Web 应用程序中添加对第二种语言的支持是一件棘手的事情。每次对应用程序进行更改时,都会增加一些开销,并且随着语言数量的增加而增加。除了更改文本之外,还需要注意许多其他事情,具体取决于语言。需要更改的一些主要事项包括货币、数字、时间和日期格式。
Flask-Babel,一个为任何 Flask 应用程序添加国际化(i18n)和本地化(l10n)支持的扩展,提供了一些工具和技术,使此过程易于实现。
在本章中,我们将介绍以下配方:
-
添加新语言
-
实现延迟评估和
gettext/ngettext函数 -
实现全局语言切换操作
添加新语言
默认情况下,Flask(以及几乎所有 Web 框架)构建的应用程序的语言为英语。在本配方中,我们将向我们的应用程序添加第二种语言,并为应用程序中使用的显示字符串添加一些翻译。显示给用户的语言将取决于浏览器当前设置的语言。
准备工作
我们将从安装Flask-Babel扩展开始:
$ pip install Flask-Babel
此扩展使用Babel和pytz为应用程序添加 i18n 和 l10n 支持。
我们将使用来自第五章,“Web 表单 与 WTForms”的目录应用程序。
如何操作...
我们将使用法语作为第二种语言。按照以下步骤实现此功能:
-
从创建
Babel类的实例开始配置部分,使用my_app/__init__.py中的app对象。我们还将指定这里将可用的所有语言:from flask import requestfrom flask_babel import BabelALLOWED_LANGUAGES = {'en': 'English','fr': 'French',}babel = Babel(app)
小贴士
在这里,我们使用了en和fr作为语言代码。这些分别代表英语(标准)和法语(标准)。如果我们打算支持来自同一标准语言起源的多种语言,但基于地区不同,例如英语(美国)和英语(英国),那么我们应该使用如en-us和en-gb之类的代码。
-
应用程序的区域设置取决于初始化
babel对象时提供的方法的输出:def get_locale():return request.accept_languages.best_match(ALLOWED_LANGUAGES.keys())babel.init_app(app, locale_selector=get_locale)
之前的方法从请求中获取accept_languages头,并找到与我们允许的语言最匹配的语言。
小贴士
您可以更改浏览器的语言首选项来测试应用程序在另一种语言中的行为。
以前,更改浏览器中的语言首选项相对容易,但随着地区设置在操作系统中的更加根深蒂固,这样做变得困难,除非更改操作系统的全局地区设置。因此,如果您不想弄乱浏览器的语言首选项或操作系统的语言首选项,只需从 get_locale() 方法返回预期的语言代码即可。
-
接下来,在应用程序文件夹中创建一个名为
babel.cfg的文件。此文件的路径为my_app/babel.cfg,其内容如下:[python: catalog/**.py][jinja2: templates/**.html]
在这里,前两行告诉 Babel 要搜索标记为可翻译文本的文件名模式。
信息
在本书的早期版本中,我建议加载 Jinja2 的几个扩展,即 jinja2.ext.autoescape 和 jinja2.ext.with_。但自 Jinja 3.1.0 版本起,这些模块已经内置了支持,因此现在没有必要单独加载它们。
-
接下来,标记一些需要根据语言进行翻译的文本。让我们从启动应用程序时看到的第一个文本开始,该文本位于
home.html中:{% block container %}<h1>{{ _('Welcome to the Catalog Home') }}</h1><a href="{{ url_for('catalog.products') }}"id="catalog_link">{{ _('Click here to see the catalog ') }}</a>{% endblock %}
在这里,_ 是 Babel 提供的 gettext 函数的快捷方式,用于翻译字符串。
-
然后,运行以下命令,以便在浏览器中渲染模板时,标记的文本实际上作为翻译文本可用:
$ pybabel extract -F my_app/babel.cfg -omy_app/messages.pot my_app/
上述命令遍历文件的内容。此命令匹配 babel.cfg 中的模式,并挑选出标记为可翻译的文本。所有这些文本都放置在 my_app/messages.pot 文件中。以下为上述命令的输出:
extracting messages from my_app/catalog/__init__.py
extracting messages from my_app/catalog/models.py
extracting messages from my_app/catalog/views.py
extracting messages from my_app/templates/404.html
extracting messages from my_app/templates/base.html
extracting messages from
my_app/templates/categories.html
extracting messages from my_app/templates/category-
create.html
extracting messages from
my_app/templates/category.html
extracting messages from my_app/templates/home.html
extracting messages from my_app/templates/product-
create.html
extracting messages from my_app/templates/product.html
extracting messages from
my_app/templates/products.html
writing PO template file to my_app/messages.pot
-
运行以下命令以创建一个
.po文件,该文件将保存要翻译的文本的翻译:$ pybabel init -i my_app/messages.pot -dmy_app/translations -l fr
此文件在指定的文件夹 my_app/translations 中创建,名为 fr/LC_MESSAGES/messages.po。随着我们添加更多语言,将添加更多文件夹。
- 现在,向
messages.po文件中添加翻译。这可以手动完成,或者我们可以使用诸如 Poedit (poedit.net/) 这样的图形界面工具。使用此工具,翻译将看起来如下截图所示:

图 9.1 – 编辑翻译时的 Poedit 屏幕
手动编辑 messages.po 的样子如下所示。这里仅为了演示目的,只展示了一条消息的翻译:
#: my_app/catalog/models.py:75
msgid "Not a valid choice"
msgstr "Pas un choix valable"
-
在将翻译合并到
messages.po文件后保存,并运行以下命令:$ pybabel compile -d my_app/translations
这将在 message.po 文件旁边创建一个 messages.mo 文件,该文件将由应用程序用于渲染翻译文本。
信息
有时,在运行前面的命令后,消息没有得到编译。这是因为消息可能被标记为模糊(以 # 符号开头)。这些需要由人工检查,如果消息可以由编译器更新,则必须移除 # 符号。为了绕过此检查,请在前面的 compile 命令中添加一个 -f 标志,因为它将强制编译所有内容。
它是如何工作的...
如果我们在浏览器中将应用程序的主要语言设置为法语(或从 get_locale() 方法返回的语言选择),主页将看起来如下截图所示:

图 9.2 – 法语主页
如果主要语言设置为除法语以外的其他语言,则内容将以默认语言英语显示。
还有更多...
下次,如果需要更新我们的 messages.po 文件中的翻译,我们不需要再次调用 init 命令。相反,我们可以运行一个 update 命令,如下所示:
$ pybabel update -i my_app/messages.pot -d
my_app/translations
然后,像往常一样运行 compile 命令。
信息
根据用户的 IP 地址和位置(从 IP 地址确定)更改网站的语通常更可取,但总的来说,这不如使用我们在应用程序中使用过的 accept-language 标头推荐。
参见
参考本章后面的 实现全局语言切换操作 菜谱,它允许用户直接从应用程序而不是在浏览器级别更改语言。
多语言的一个重要方面是能够相应地格式化日期、时间和货币。Babel 也处理得相当整洁。我敦促你尝试一下。有关此信息,请参阅 Babel 文档,可在 babel.pocoo.org/en/latest/ 找到。
实现懒加载和 gettext/ngettext 函数
懒加载是一种评估策略,它将表达式的评估延迟到其值需要时;也就是说,它是一个按需调用机制。在我们的应用程序中,可能会有几个文本实例在渲染模板时被延迟评估。这通常发生在我们标记为可翻译的文本位于请求上下文之外时,因此我们推迟这些文本的评估,直到它们实际需要时。
准备工作
让我们从上一个菜谱中的应用程序开始。现在,我们希望产品创建表单和分类创建表单中的标签显示翻译后的值。
如何做到这一点…
按以下步骤实现翻译的懒加载:
-
要将产品表单和分类表单中的所有字段标签标记为可翻译,请对
my_app/catalog/models.py进行以下更改:from flask_babel import _class NameForm(FlaskForm):name = StringField(_('Name'), validators=[InputRequired()])class ProductForm(NameForm):price = DecimalField(_('Price'), validators=[InputRequired(),NumberRange(min=Decimal('0.0'))])category = CategoryField(_('Category'), validators=[InputRequired()],coerce=int)image = FileField(_('Product Image'),validators=[FileRequired()])class CategoryForm(NameForm):name = StringField(_('Name'), validators=[InputRequired(), check_duplicate_category()])
注意,所有字段标签都被包含在 _() 中以标记为需要翻译。
-
现在,运行
extract和update pybabel命令以更新messages.po文件,然后填写相关翻译并运行compile命令。有关详细信息,请参阅之前的菜谱,添加新语言。 -
现在,使用以下链接打开产品创建页面:
http://127.0.0.1:5000/product-create。它是否按预期工作?不!正如我们现在大多数人都会猜测的那样,这种行为的原因是这段文本在请求上下文之外被标记为需要翻译。
要使此操作生效,修改import语句如下:
from flask_babel import lazy_gettext as _
-
现在,我们有更多文本需要翻译。假设我们想翻译产品创建的闪存消息内容,其外观如下:
flash('The product %s has been created' % name)
要将其标记为可翻译,我们不能简单地将整个内容包裹在_()或gettext()中。gettext()函数支持占位符,可以使用%(name)s作为。使用它,前面的代码将变成类似这样:
flash(_('The product %(name)s has been created',
name=name), 'success')
对于这个结果,翻译后的文本将类似于La produit %(name)s a été créée。
-
可能会有一些情况,我们需要根据项目数量来管理翻译,即单数或复数名称。这由
ngettext()方法处理。让我们举一个例子,我们想在products.html模板中显示页数。为此,添加以下代码:{{ ngettext('%(num)d page', '%(num)d pages',products.pages) }}
在这里,模板将渲染page如果只有一个页面,如果有多个页面则渲染pages。
有趣的是要注意这种翻译在messages.po文件中的显示方式:
#: my_app/templates/products.html:20
#, python-format
msgid "%(num)d page"
msgid_plural "%(num)d pages"
msgstr[0] "%(num)d page"
msgstr[1] "%(num)d pages"
它是如何工作的…
打开产品创建表单,网址为http://127.0.0.1:5000/product-create。以下截图显示了将其翻译成法语后的样子:

图 9.3 – 使用惰性评估翻译的表单字段
实现全局语言切换操作
在之前的菜谱中,我们看到了语言变化是基于浏览器中的当前语言首选项。然而,现在我们想要一个机制,可以切换正在使用的语言,而不管浏览器的语言如何。在这个菜谱中,我们将了解如何在应用级别处理语言切换。
准备工作
我们首先从上一个菜谱中修改应用程序,即实现惰性评估和 gettext/ngettext 函数,以适应语言切换的更改。我们将为所有路由添加一个额外的 URL 部分,以便我们能够添加当前语言。我们只需更改 URL 中的语言部分即可在语言之间切换。
如何操作…
观察以下步骤以了解如何全局实现语言切换:
-
首先,修改所有 URL 规则以适应额外的 URL 部分。
@catalog.route('/')将变为@catalog.route('/<lang>/'),而@catalog.route('/home')将变为@catalog.route('/<lang>/home')。同样,@catalog.route('/product-search/<int:page>')将变为@catalog.route('/<lang>/product-search/<int:page>')。所有 URL 规则都需要这样做。 -
现在,添加一个函数,该函数将 URL 中传递的语言添加到全局代理对象
g中:@app.before_requestdef before():if request.view_args and 'lang' inrequest.view_args:g.current_lang = request.view_args['lang']request.view_args.pop('lang')
此方法将在每个请求之前运行,并将当前语言添加到g中。
-
然而,这意味着应用程序中所有的
url_for()调用都需要修改,以便传递一个名为lang的额外参数。幸运的是,有一个简单的解决方案,如下所示:from flask import url_for as flask_url_for@app.context_processordef inject_url_for():return {'url_for': lambda endpoint, **kwargs:flask_url_for(endpoint, lang=g.get('current_lang','en'), **kwargs)}url_for = inject_url_for()['url_for']
在前面的代码中,我们首先从flask中导入url_for作为flask_url_for。然后,我们更新了应用程序上下文处理器,使其具有url_for()函数,这是 Flask 提供的修改版url_for(),以便将lang作为额外参数。我们还使用了在视图中使用的相同url_for()方法。
它是如何工作的…
现在,以当前状态运行应用程序,你会注意到所有 URL 都有一个语言部分。以下两个截图显示了渲染的模板将看起来如何。
对于英语,以下截图显示了打开http://127.0.0.1:5000/en/home后的首页外观:

图 9.4 – 英语首页
对于法语,只需将 URL 更改为http://127.0.0.1:5000/fr/home,首页将看起来像这样:

图 9.5 – 法语首页
更多内容…
l10n 不仅仅是翻译字母语言。不同的地理区域遵循不同的数字、小数、货币等格式。例如,1.5 百万美元在荷兰会被写成 1,5 Mio USD,而 123.56 在英语中会被写成 123,56 在法语中。
Babel 使得实现这种格式化非常容易。为此目的,有一整套方法可供选择。以下是一些示例:
>>> from babel import numbers
>>> numbers.format_number(12345, 'en_US')
'12,345'
>>> numbers.format_number(12345, 'fr_FR')
'12\u202f345'
>>> numbers.format_number(12345, 'de_DE')
'12.345'
>>> numbers.format_decimal(12.345, locale='de_DE')
'12,345'
>>> numbers.format_decimal(12.345, locale='en_US')
'12.345'
>>> numbers.format_currency(12.345, 'USD', locale='en_US')
'$12.34'
>>> numbers.format_currency(12345789, 'USD', locale='en_US')
'$12,345,789.00'
>>> numbers.format_compact_currency(12345789, 'USD', locale='de_DE')
'12\xa0Mio.\xa0$'
>>> numbers.format_compact_currency(12345789, 'USD', locale='en_US')
'$12M'
你可以在babel.pocoo.org/en/latest/api/numbers.html#module-babel.numbers了解更多相关信息。
第三部分:高级 Flask
一旦在 Flask 中构建了 Web 应用程序,下一个问题就是如何测试应用程序,然后是部署,最后是维护它们。本书的这一部分涵盖了这些重要主题。这是本书从完全面向开发转向关注开发后活动的转折点。
通过编写单元测试来测试应用程序非常重要,这不仅可以对已编写的代码进行内省,还可以预先识别可能出现在功能进一步开发中的任何问题。一旦应用程序构建完成,你将希望从清晰的角度来衡量应用程序的性能。第十章*讨论了这些主题以及其他内容。
接下来的几章重点介绍了各种工具和技术,这些工具和技术可以用来在不同的平台上部署 Flask 网络应用程序,从云原生服务到裸机服务器。你将了解到如何使用最先进的技术,如 Docker 和 Kubernetes,来有效地部署你的网络应用程序。
新增了一章关于 GPT 的内容,讨论了如何将这项前沿技术与 Flask 集成以应对一些常见用例,以及如何通过人工智能使你的应用程序面向未来。
最后一章收集了一些可以在任何特定用例中使用的额外技巧和窍门。还有更多这样的主题,但我主要涵盖了那些我处理得最多的。
这一部分的书包括以下章节:
-
第十章**,调试、错误处理和测试
-
第十一章,部署和部署后
-
第十二章**,微服务和容器
-
第十三章**,使用 Flask 的 GPT
-
第十四章**,额外的技巧和窍门
第十章:调试、错误处理和测试
到目前为止,在这本书中,我们一直专注于开发和为它们逐个添加功能的应用程序。了解我们的应用程序有多稳健,以及跟踪它的运行和性能情况非常重要。这反过来又产生了在应用程序出现问题时得到通知的需求。在开发应用程序时,遗漏某些边缘情况是正常的,通常,测试用例也会遗漏它们。如果能在它们出现时了解这些边缘情况,以便相应地处理它们,那就太好了。
有效的日志记录和快速调试的能力是选择应用程序开发框架时的决定性因素之一。框架提供的日志记录和调试支持越好,应用程序开发和维护的过程就越快。更好的日志记录和调试支持水平有助于开发者快速发现应用程序中的问题,在许多情况下,日志记录甚至会在最终用户识别问题之前指出问题。有效的错误处理在最终用户满意度中起着重要作用,并减轻了开发者调试的痛苦。即使其代码完美无缺,应用程序也难免会在某些时候抛出错误。为什么?答案很简单——代码可能完美无缺,但它在其中运行的世界却不是。可能会有无数的问题发生,作为开发者,我们总是想知道任何异常背后的原因。与应用程序一起编写测试用例是软件编写最重要的支柱之一。
Python 的内置日志系统与 Flask 配合得相当好。在本章中,我们将使用这个日志系统,然后再转向一个名为Sentry的出色服务,它极大地简化了调试和错误日志记录的痛苦。
既然我们已经讨论了测试对于应用程序开发的重要性,我们现在将看看如何为 Flask 应用程序编写测试用例。我们还将了解如何衡量代码覆盖率并分析我们的应用程序以解决任何瓶颈。
测试本身是一个巨大的主题,与之相关的书籍有很多。在这里,我们将尝试理解使用 Flask 进行测试的基础知识。
在本章中,我们将涵盖以下内容:
-
设置基本的文件日志记录
-
在发生错误时发送电子邮件
-
使用 Sentry 监控异常
-
使用
pdb调试 -
创建应用程序工厂
-
创建第一个简单的测试
-
为视图和逻辑编写更多测试
-
集成 nose2 库
-
使用模拟来避免外部 API 访问
-
确定测试覆盖率
-
使用分析来查找瓶颈
设置基本的文件日志记录
默认情况下,Flask 不会为我们记录任何内容,除了带有堆栈跟踪的错误,这些错误被发送到记录器(我们将在本章的其余部分看到更多)。它在开发模式下使用 run.py 运行应用程序时确实会创建大量的堆栈跟踪,但在生产系统中,我们没有这种奢侈。幸运的是,记录库提供了一系列的记录处理程序,可以根据需要使用。在本食谱中,我们将了解如何利用 logging 库确保从 Flask 应用程序中捕获有效的日志。
准备中
我们将从上一章的目录应用程序开始,并使用 FileHandler 向文件系统上的指定文件添加一些基本的记录,以对它进行记录。我们将从基本的日志格式开始,然后看看如何格式化日志消息以使其更具信息性。
如何操作...
按照以下步骤配置和设置 logging 库,以便与我们的应用程序一起使用:
-
第一个更改是对
my_app/__init__.py文件进行的,该文件作为应用程序的配置文件:app.config['LOG_FILE'] = 'application.log'if not app.debug:import loggingfrom logging import FileHandler, Formatterfile_handler = FileHandler(app.config['LOG_FILE'])app.logger.setLevel(logging.INFO)app.logger.addHandler(file_handler)
在这里,我们添加了一个配置参数来指定日志文件的位置。这采用从应用程序文件夹的相对路径,除非显式指定了绝对路径。接下来,我们将检查应用程序是否已经在调试模式中,然后我们将添加一个记录级别为 INFO 的文件记录处理程序。现在,DEBUG 是最低的记录级别,将记录任何级别的所有内容。有关更多详细信息,请参阅 logging 库文档(在 参见 部分)。
-
在此之后,将记录器添加到应用程序所需的任何位置,应用程序将开始向指定的文件记录。现在,让我们向
my_app/catalog/views.py添加几个记录器以供演示:@catalog.route('/')@catalog.route('/<lang>/')@catalog.route('/<lang>/home')@template_or_json('home.html')def home():products = Product.query.all()app.logger.info('Home page with total of %d products'% len(products))return {'count': len(products)}@catalog.route('/<lang>/product/<id>')def product(id):product = Product.query.filter_by(id=id).first()if not product:app.logger.warning('Requested product notfound.')abort(404)return render_template('product.html',product=product)
在前面的代码中,我们为我们的几个视图处理程序添加了记录器。请注意,home() 中的第一个记录器处于 info 级别,而 product() 中的另一个是 warning。如果我们将在 __init__.py 中设置的日志级别为 INFO,那么两者都将被记录,如果我们设置级别为 WARNING,那么只有警告记录器将被记录。
信息
确保如果尚未完成,从 Flask 中导入 abort – from flask import abort。
它是如何工作的...
前面的步骤将在根应用程序文件夹中创建一个名为 application.log 的文件。根据指定的记录器,记录语句将被记录到 application.log,其内容可能类似于以下片段;第一个来自主页,第二个来自请求一个不存在的商品:
Home page with total of 0 products
Requested product not found.
重要
要启用记录,要么使用 WSGI 服务器运行您的应用程序(请参阅第十一章)或使用终端提示中的 flask run 运行(请参阅第一章)。
使用run.py运行应用程序将始终使其以debug标志为True的方式运行,这将不会允许日志按预期工作。
记录的信息帮助不大。知道问题何时被记录、以何种级别、哪个文件在什么行号引发了问题等会更好。这可以通过高级日志格式实现。为此,我们需要在配置文件中添加一些语句——即my_app/__init__.py:
if not app.debug:
import logging
from logging import FileHandler, Formatter
file_handler = FileHandler(app.config['LOG_FILE'])
app.logger.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
file_handler.setFormatter(Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
在前面的代码中,我们向file_handler添加了一个格式化器,这将记录时间、日志级别、消息、文件路径和行号。之后,记录的消息将看起来像这样:
2023-01-02 13:01:25,125 INFO: Home page with total of 0 products [in /Users/apple/workspace/flask-cookbook-3/Chapter-10/Chapter-10/my_app/catalog/views.py:72]
2023-01-02 13:01:27,657 WARNING: Requested product not found. [in /Users/apple/workspace/flask-cookbook-3/Chapter-10/Chapter-10/my_app/catalog/views.py:82]
还有更多…
我们还可能想要记录当页面未找到时(404错误)的所有错误。为此,我们只需稍微调整一下errorhandler方法:
@app.errorhandler(404)
def page_not_found(e):
app.logger.error(e)
return render_template('404.html'), 404
参见
前往 Python 的logging库处理程序文档docs.python.org/dev/library/logging.handlers.html,了解更多关于日志处理程序的信息。
发生错误时发送电子邮件
当应用程序出现意外情况时,收到通知是个好主意。设置这一点相当简单,并为错误处理过程增加了许多便利。
准备工作
我们将从上一个菜谱中的应用程序开始,向其中添加mail_handler,以便在发生错误时我们的应用程序能够发送电子邮件。此外,我们将演示使用 Gmail 作为 SMTP 服务器进行电子邮件设置。
如何操作...
首先,将处理程序添加到my_app/__init__.py中的配置中。这与我们在上一个菜谱中添加file_handler的方式类似:
RECEPIENTS = ['some_receiver@gmail.com']
if not app.debug:
import logging
from logging import FileHandler, Formatter
from logging.handlers import SMTPHandler
file_handler = FileHandler(app.config['LOG_FILE'])
app.logger.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
mail_handler = SMTPHandler(
("smtp.gmail.com", 587), 'sender@gmail.com',
RECEPIENTS,
'Error occurred in your application',
('some_email@gmail.com', 'some_gmail_password'),
secure=())
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
for handler in [file_handler, mail_handler]:
handler.setFormatter(Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
在这里,我们有一个电子邮件地址列表,错误通知电子邮件将发送到这些地址。另外,请注意,我们在mail_handler的情况下将日志级别设置为ERROR。这是因为只有在关键事项的情况下才需要电子邮件。
有关SMTPHandler配置的更多详细信息,请参阅文档。
重要
总是要确保以debug标志设置为off的方式运行您的应用程序,以启用应用程序记录和发送内部应用程序错误(500错误)的电子邮件。
它是如何工作的…
要引起内部应用程序错误,只需在任何处理程序中拼写一些关键字出错。您将在邮箱中收到一封电子邮件,格式与配置中设置的一样,并附有完整的堆栈跟踪供您参考。
使用 Sentry 监控异常
Sentry 是一个工具,它简化了监控异常的过程,同时也为应用程序用户在使用过程中遇到的错误提供了洞察。很可能日志文件中存在被人类眼睛忽视的错误。Sentry 将错误分类到不同的类别,并记录错误的重复次数。这有助于我们根据多个标准了解错误的严重性以及如何相应地处理它们。它有一个很好的 GUI,便于实现所有这些功能。在本食谱中,我们将设置 Sentry 并将其用作有效的错误监控工具。
准备工作
Sentry 可作为云服务提供,对开发者和基本用户免费。在本食谱的目的上,这个免费提供的云服务将足够使用。请访问 sentry.io/signup/ 并开始注册过程。话虽如此,我们需要安装 Sentry 的 Python SDK:
$ pip install 'sentry-sdk[flask]'
如何操作…
Sentry 注册完成后,将显示一个屏幕,询问需要与 Sentry 集成的项目类型。以下是一个截图:

图 10.1 – Sentry 项目创建屏幕
这将随后显示另一个屏幕,展示如何配置您的 Flask 应用程序以将事件发送到新创建和配置的 Sentry 实例。以下是一个截图:

图 10.2 – Sentry 项目配置步骤
信息
Sentry 也可以免费下载并作为本地应用程序安装。根据您的需求,有多种安装和配置 Sentry 的方法。您可以自由尝试这种方法,因为它超出了本食谱的范围。
在完成之前的设置后,将以下代码添加到您的 Flask 应用程序中的 my_app/__init__.py 文件,将 https://1234:5678@fake-sentry-server/1 替换为 Sentry 项目 URI:
import sentry_sdk
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(
dsn="https://1234:5678@fake-sentry-server/1",
integrations=[FlaskIntegration()]
)
它是如何工作的…
在 Sentry 中记录的错误将类似于以下截图:

图 10.3 – Sentry 错误日志屏幕
还可以在 Sentry 中记录消息和用户定义的异常。我将把这个留给你自己探索。
使用 pdb 进行调试
阅读这本书的大部分 Python 开发者可能已经知道 pdb 是一个交互式源代码调试器,用于 Python 程序。我们可以在需要的地方设置断点,在源代码行级别进行单步调试,并检查堆栈帧。
许多新开发者可能认为调试器的任务可以使用记录器来处理,但调试器提供了对控制流程的更深入洞察,保留每个步骤的状态,因此,可能节省大量开发时间。在这个菜谱中,让我们看看pdb能带来什么。
准备工作
我们将在这个菜谱中使用 Python 的内置pdb模块,并在上一个菜谱的应用程序中使用它。
如何操作…
在大多数情况下,使用pdb相当简单。我们只需要在我们想要插入断点以检查特定代码块的地方插入以下语句:
import pdb; pdb.set_trace()
这将触发应用程序在此处中断执行,然后我们可以使用调试器命令逐个遍历堆栈帧。
因此,让我们在我们的某个方法中插入这个语句——比如说,产品的处理程序:
@catalog.route('/<lang>/products')
@catalog.route('/<lang>/products/<int:page>')
def products(page=1):
products = Product.query.paginate(page=page,
per_page=10)
import pdb; pdb.set_trace()
return render_template('products.html',
products=products)
它是如何工作的…
每当控制流到达这一行时,调试器提示将会启动;它将如下所示:
> /Users/apple/workspace/flask-cookbook-3/Chapter-10/Chapter-10/my_app/catalog/views.py(93)products()
-> return render_template('products.html', products=products)
(Pdb) u
> /Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/flask/app.py(1796)dispatch_request()
-> return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
(Pdb) u
> /Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/flask/app.py(1820)full_dispatch_request()
-> rv = self.dispatch_request()
(Pdb) u
> /Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/flask/app.py(2525)wsgi_app()
-> response = self.full_dispatch_request()
(Pdb) u
> /Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/flask/app.py(2548)__call__()
-> return self.wsgi_app(environ, start_response)
注意(Pdb)旁边写有u。这表示我正在将当前帧在堆栈跟踪中向上移动一级。在该语句中使用的所有变量、参数和属性都将在此上下文中可用,以帮助解决问题或只是理解代码的流程。还有其他调试器命令可能在您的调试日志导航中很有帮助。请参阅以下参见部分以获取这些信息。
参见
前往docs.python.org/3/library/pdb.html#debugger-commands模块文档,以获取各种调试器命令。
创建应用程序工厂
利用工厂模式是组织应用程序对象的好方法,允许有多个具有不同设置的应用程序对象。如在第第一章中讨论的,总是可以通过使用不同的配置来创建多个应用程序实例,但应用程序工厂允许您在同一个应用程序进程中拥有多个应用程序对象。它还有助于测试,因为您可以选择为每个测试案例选择一个全新的或不同的应用程序对象,并具有不同的设置。
准备工作
我们将使用上一个菜谱中的应用程序,并将其修改为使用应用程序工厂模式。
如何操作…
需要进行的以下是一些更改:
-
我们将在
my_app/__init__.py中创建一个名为create_app()的函数:def create_app(alt_config={}):app = Flask(__name__, template_folder=alt_config.get('TEMPLATE_FOLDER', 'templates'))app.config['UPLOAD_FOLDER'] =os.path.realpath('.') + '/my_app/static/uploads'app.config['SQLALCHEMY_DATABASE_URI'] ='sqlite:////tmp/test.db'app.config['WTF_CSRF_SECRET_KEY'] = 'random keyfor form'app.config['LOG_FILE'] = 'application.log'app.config.update(alt_config)if not app.debug:import loggingfrom logging import FileHandler, Formatterfrom logging.handlers import SMTPHandlerfile_handler =FileHandler(app.config['LOG_FILE'])app.logger.setLevel(logging.INFO)app.logger.addHandler(file_handler)mail_handler = SMTPHandler(("smtp.gmail.com", 587),'sender@gmail.com', RECEPIENTS,'Error occurred in your application',('some_email@gmail.com','some_gmail_password'), secure=())mail_handler.setLevel(logging.ERROR)# app.logger.addHandler(mail_handler)for handler in [file_handler, mail_handler]:handler.setFormatter(Formatter('%(asctime)s %(levelname)s:%(message)s ''[in %(pathname)s:%(lineno)d]'))app.secret_key = 'some_random_key'return app
在这个函数中,我们只是在名为create_app()的函数内部重新排列了所有的应用程序配置。这将允许我们通过简单地调用这个函数来创建所需数量的应用程序对象。
-
接下来,我们创建一个名为
create_db()的方法,它初始化数据库然后创建表:db = SQLAlchemy()def create_db(app):db.init_app(app)with app.app_context():db.create_all()return db
在这个函数中,我们只是将数据库特定的代码移动到了一个函数中。这个方法被保留为独立的,因为您可能希望使用不同的数据库配置与不同的应用程序实例一起使用。
-
在
my_app/__init__.py中的最后一步将是调用/执行这些方法并注册蓝图:def get_locale():return g.get('current_lang', 'en')app = create_app()babel = Babel(app)babel.init_app(app, locale_selector=get_locale)from my_app.catalog.views import catalogapp.register_blueprint(catalog)db = create_db(app)
我们通过调用相关方法并初始化扩展来创建了app、db和babel的对象。
应用程序工厂模式的缺点是在导入时不能在蓝图中使用应用程序对象。然而,您始终可以利用current_app代理来访问当前的应用程序对象。让我们看看在my_app/catalog/views.py中是如何做到这一点的:
from flask import current_app
@catalog.before_request
def before():
# Existing code
@catalog.context_processor
def inject_url_for():
# Existing code
# Similarly simply replace all your references to `app` by
`current_app`. Refer to code provided with the book for a
complete example.
它是如何工作的...
应用程序将继续以与上一个配方相同的方式工作。只是代码已经被重新排列以实现应用程序工厂模式。
参见
接下来的几个配方将帮助您了解在编写测试用例时如何使用工厂模式。
创建第一个简单的测试
测试是任何软件开发期间以及后续的维护和扩展期间最坚实的支柱之一。特别是在网络应用程序的情况下,应用程序将处理高流量,并且始终受到大量最终用户的审查,测试变得非常重要,因为用户反馈决定了应用程序的命运。在这个配方中,我们将看到如何从编写测试开始,并在接下来的配方中看到更复杂的测试。
准备工作
我们将从在根应用程序级别创建一个名为app_tests.py的新测试文件开始——即在my_app文件夹旁边。
如何做到这一点...
让我们编写我们的第一个测试用例:
-
首先,
app_tests.py测试文件的内容将如下所示:import osfrom my_app import create_app, db, babelimport unittestimport tempfile
前面的代码描述了此测试套件所需的导入。我们将使用unittest来编写我们的测试。需要一个tempfile实例来动态创建 SQLite 数据库。
-
所有测试用例都需要从
unittest.TestCase派生:class CatalogTestCase(unittest.TestCase):def setUp(self):test_config = {}self.test_db_file = tempfile.mkstemp()[1]test_config['SQLALCHEMY_DATABASE_URI'] ='sqlite:///' + self.test_db_filetest_config['TESTING'] = Trueself.app = create_app(test_config)db.init_app(self.app)babel.init_app(self.app)with self.app.app_context():db.create_all()from my_app.catalog.views import catalogself.app.register_blueprint(catalog)self.client = self.app.test_client()
在每次运行测试之前都会运行前面的方法,并创建一个新的测试客户端。一个测试由这个类中以test_前缀开始的方法表示。在这里,我们在应用程序配置中设置了一个数据库名称,它是一个基于时间戳的值,将始终是唯一的。我们还设置了TESTING标志为True,这禁用了错误捕获以启用更好的测试。请特别注意如何在使用db和babel初始化之前使用应用程序工厂创建应用程序对象。
最后,我们在db上运行create_all()方法,在测试数据库中创建我们应用程序的所有表。
-
在测试执行后删除之前步骤中创建的临时数据库:
def tearDown(self):os.remove(self.test_db_file)
在每次运行测试之后都会调用前面的方法。在这里,我们将删除当前的数据库文件,并为每个测试使用一个新的数据库文件。
-
最后,编写测试用例:
def test_home(self):rv = self.client.get('/')self.assertEqual(rv.status_code, 200)
之前的代码是我们的第一个测试,其中我们向我们的应用程序在/ URL 发送了 HTTP GET请求并测试了状态码,它应该是200,表示成功的GET响应。
它是如何工作的…
要运行测试文件,只需在终端中执行以下命令:
$ python app_tests.py
以下截图显示了表示测试结果的输出:

图 10.4 – 第一个测试结果
相关内容
参考下一道菜谱,为视图和逻辑编写更多测试,以了解如何编写复杂测试的更多内容。
为视图和逻辑编写更多测试
在上一道菜谱中,我们开始了为我们的 Flask 应用程序编写测试。在这道菜谱中,我们将基于相同的测试文件添加更多测试到我们的应用程序中;这些测试将涵盖测试视图的行为和逻辑。
准备工作
我们将基于上一道菜谱中创建的名为app_tests.py的测试文件进行构建。
如何做…
在我们编写任何测试之前,我们需要向setUp()添加一小部分配置来禁用 CSRF 令牌,因为在测试环境中默认不会生成:
test_config['WTF_CSRF_ENABLED'] = False
以下是一些作为这道菜谱的一部分创建的测试。每个测试将在我们进一步进行时进行描述:
-
首先,编写一个测试来向产品列表发送
GET请求:def test_products(self):"Test Products list page"rv = self.client.get('/en/products')self.assertEqual(rv.status_code, 200)self.assertTrue('No Previous Page' inrv.data.decode("utf-8"))self.assertTrue('No Next Page' inrv.data.decode("utf-8"))
之前的测试向/products端点发送GET请求并断言响应的状态码为200。它还断言没有上一页和下一页(作为模板逻辑的一部分渲染)。
-
接下来,创建一个类别并验证它是否已正确创建:
def test_create_category(self):"Test creation of new category"rv = self.client.get('/en/category-create')self.assertEqual(rv.status_code, 200)rv = self.client.post('/en/category-create')self.assertEqual(rv.status_code, 200)self.assertTrue('This field is required.' Inrv.data.decode("utf-8"))rv = self.client.get('/en/categories')self.assertEqual(rv.status_code, 200)self.assertFalse('Phones' inrv.data.decode("utf-8"))rv = self.client.post('/en/category-create',data={'name': 'Phones',})self.assertEqual(rv.status_code, 302)rv = self.client.get('/en/categories')self.assertEqual(rv.status_code, 200)self.assertTrue('Phones' inrv.data.decode("utf-8"))rv = self.client.get('/en/category/1')self.assertEqual(rv.status_code, 200)self.assertTrue('Phones' inrv.data.decode("utf-8"))
之前的测试创建了一个类别并断言相应的状态消息。当类别成功创建时,我们将被重定向到新创建的类别页面,因此状态码将是302。
-
现在,类似于类别创建,创建一个产品然后验证其创建:
def test_create_product(self):"Test creation of new product"rv = self.client.get('/en/product-create')self.assertEqual(rv.status_code, 200)# Raise a ValueError for a valid category notfoundself.assertRaises(ValueError,self.client.post, '/en/product-create')# Create a category to be used in productcreationrv = self.client.post('/en/category-create',data={'name': 'Phones',})self.assertEqual(rv.status_code, 302)rv = self.client.post('/en/product-create',data={'name': 'iPhone 5','price': 549.49,'company': 'Apple','category': 1,'image': tempfile.NamedTemporaryFile()})self.assertEqual(rv.status_code, 302)rv = self.client.get('/en/products')self.assertEqual(rv.status_code, 200)self.assertTrue('iPhone 5' inrv.data.decode("utf-8"))
之前的测试创建了一个产品并断言每个调用/请求的相应状态消息。
信息
作为这个测试的一部分,我们在create_product()方法中识别了一个小的改进。我们在检查允许的文件类型之前没有初始化filename变量。早期的代码只有在if条件通过时才能正常工作。现在,我们只是调整了代码,在if条件之前将filename初始化为filename = secure_filename(image.filename),而不是在条件内部做这件事。
-
最后,创建多个产品并搜索刚刚创建的产品:
def test_search_product(self):"Test searching product"# Create a category to be used in productcreationrv = self.client.post('/en/category-create',data={'name': 'Phones',})self.assertEqual(rv.status_code, 302)# Create a productrv = self.client.post('/en/product-create',data={'name': 'iPhone 5','price': 549.49,'company': 'Apple','category': 1,'image': tempfile.NamedTemporaryFile()})self.assertEqual(rv.status_code, 302)# Create another productrv = self.client.post('/en/product-create',data={'name': 'Galaxy S5','price': 549.49,'company': 'Samsung','category': 1,'image': tempfile.NamedTemporaryFile()})self.assertEqual(rv.status_code, 302)self.client.get('/')rv = self.client.get('/en/product-search?name=iPhone')self.assertEqual(rv.status_code, 200)self.assertTrue('iPhone 5' inrv.data.decode("utf-8"))self.assertFalse('Galaxy S5' inrv.data.decode("utf-8"))rv = self.client.get('/en/product-search?name=iPhone 6')self.assertEqual(rv.status_code, 200)self.assertFalse('iPhone 6' inrv.data.decode("utf-8"))
之前的测试首先创建一个类别和两个产品。然后,它搜索一个产品并确保只返回搜索到的产品在结果中。
它是如何工作的…
要运行测试文件,只需在终端中执行以下命令:
$ python app_tests.py -v
test_create_category (__main__.CatalogTestCase)
Test creation of new category ... ok
test_create_product (__main__.CatalogTestCase)
Test creation of new product ... ok
test_home (__main__.CatalogTestCase) ... ok
test_products (__main__.CatalogTestCase)
Test Products list page ... ok
test_search_product (__main__.CatalogTestCase)
Test searching product ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.390s
OK
命令之后是显示测试结果的输出。
相关内容
另一个可以用于单元测试的有趣且流行的库是 pytest。它与 Python 内置的 unittest 库类似,但具有更多开箱即用的功能。请随意探索它:docs.pytest.org/en/stable/。
集成 nose2 库
nose2 可以用于多种用途,其中最重要的用途仍然是作为测试收集器和运行器。nose2 会自动从当前工作目录中的 Python 源文件、目录和包中收集测试。在本食谱中,我们将重点介绍如何使用 nose2 运行单个测试,而不是每次都运行一整套测试。
重要
在本书的早期版本中,我们使用了 nose 库。自那时起,它已经没有在积极维护,并且可以被认为是过时的。已经创建了一个替代品,名为 nose2。这个库的行为与 nose 类似,但并不完全相同。然而,为了我们的演示目的,主要功能仍然相似。
准备工作
首先,我们需要安装 nose2 库:
$ pip install nose2
nose2 有一个测试文件发现机制,要求文件名应以 test 开头。由于在我们的情况下,测试文件名为 app_tests.py,我们现在应该将其重命名为 test_app.py。在终端中,你可以简单地运行以下命令:
$ mv app_tests.py test_app.py
如何做到这一点...
我们可以通过运行以下命令使用 nose2 执行我们应用程序中的所有测试:
$ nose2 -v
test_create_category (test_app.CatalogTestCase)
Test creation of new category ... ok
test_create_product (test_app.CatalogTestCase)
Test creation of new product ... ok
test_home (test_app.CatalogTestCase) ... ok
test_products (test_app.CatalogTestCase)
Test Products list page ... ok
test_search_product (test_app.CatalogTestCase)
Test searching product ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.241s
OK
这将选择我们应用程序中的所有测试并运行它们,即使我们有多个测试文件。
要运行单个测试文件,只需运行以下命令:
$ nose2 test_app
现在,如果你想运行单个测试,只需运行以下命令:
$ nose2 test_app.CatalogTestCase.test_home
当我们有一个内存密集型应用程序和大量测试用例时,这一点变得很重要。在这种情况下,测试本身可能需要很长时间才能运行,并且每次都这样做可能会让开发者感到非常沮丧。相反,我们更愿意只运行那些与所做的更改相关的测试,或者是在某些更改之后破坏的测试。
参见
根据需求,有许多其他方法可以配置 nose2 以实现最优和有效的使用。有关更多详细信息,请参阅 docs.nose2.io/en/latest/index.html 上的 nose2 文档。
使用模拟来避免外部 API 访问
我们了解测试是如何工作的,但现在,让我们想象我们有一个第三方应用程序/服务通过 API 调用与我们的应用程序集成。每次运行测试时都调用这个应用程序/服务并不是一个好主意。有时,这些调用也可能是付费的,并且在测试期间进行调用不仅可能很昂贵,还可能影响该服务的统计信息。我们可以使用 geoip2 库并通过模拟来测试它。
准备工作
在 Python 3 中,mock 已经被包含在 unittest 库的标准包中。
为了本食谱的目的,我们首先需要安装 geoip2 库和相应的数据库:
$ pip install geoip2
您还需要从 MaxMind 的免费网站(dev.maxmind.com/geoip/geolite2-free-geolocation-data)下载免费的geoip数据库到您偏好的位置,然后解压文件。为了简化,我已经将其下载到项目文件夹本身。在您能够下载geoip城市数据库之前,您需要创建一个免费账户。
在下载城市数据库后,您应该有一个以Geolite2- City-为前缀的文件夹。这个文件夹包含我们将在本食谱中使用的.mmdb扩展名的geoip数据库。
现在,假设我们想要存储创建产品的用户的位置(想象一下应用程序在多个全球位置管理的场景)。
我们需要对my_app/catalog/models.py、my_app/catalog/views.py和templates/product.html进行一些小的修改。
对于my_app/catalog/models.py,我们将添加一个名为user_timezone的新字段:
class Product(db.Model):
# ... Other fields ...
user_timezone = db.Column(db.String(255))
def __init__(self, name, price, category, image_path,
user_timezone=''):
# ... Other fields initialization ...
self.user_timezone = user_timezone
对于my_app/catalog/views.py,我们将修改create_product()方法以包含时区:
import geoip2.database, geoip2.errors
@catalog.route('/<lang>/product-create', methods=['GET',
'POST'])
def create_product():
form = ProductForm()
if form.validate_on_submit():
# ... Non changed code ...
reader = geoip2.database.Reader(
'GeoLite2-City_20230113/GeoLite2-City.mmdb'
)
try:
match = reader.city(request.remote_addr)
except geoip2.errors.AddressNotFoundError:
match = None
product = Product(
name, price, category, filename,
match and match.location.time_zone or
'Localhost'
)
# ... Non changed code ...
在这里,我们使用 IP 查找获取地理位置数据,并在创建产品时传递这些数据。如果没有找到匹配项,那么调用来自localhost、127.0.0.1或0.0.0.0。
此外,我们将在我们的产品模板中添加这个新字段,以便在测试中容易验证。为此,只需在product.html模板的某个位置添加{{ product.user_timezone }}。
如何做到这一点...
首先,修改test_app.py以适应对geoip查找的模拟:
-
首先,通过创建修补器来配置对
geoip查找的模拟:from unittest import mockimport geoip2.recordsclass CatalogTestCase(unittest.TestCase):def setUp(self):# ... Non changed code ...self.geoip_city_patcher =mock.patch('geoip2.models.City',location=geoip2.records.Location(time_zone= 'America/Los_Angeles'))PatchedGeoipCity =self.geoip_city_patcher.start()self.geoip_reader_patcher =mock.patch('geoip2.database.Reader')PatchedGeoipReader =self.geoip_reader_patcher.start()PatchedGeoipReader().city.return_value =PatchedGeoipCitywith self.app.app_context():db.create_all()from my_app.catalog.views import catalogself.app.register_blueprint(catalog)self.client = self.app.test_client()
首先,我们从geoip2中导入了records,这是我们用来创建测试所需模拟返回值的。然后,我们使用location属性在City模型上对geoip2.models.City进行了修补,并将其预设为geoip2.records.Location(time_zone = 'America/Los_Angeles'),然后启动了修补器。这意味着每当创建一个geoip2.models.City实例时,它都会被修补,其location属性上的时区设置为'America/Los_Angeles'。
这随后是对geoip2.database.Reader的修补,我们模拟其city()方法的返回值为我们之前创建的PatchedGeoipCity类。
-
停止在
setUp方法中启动的修补器:def tearDown(self):self.geoip_city_patcher.stop()self.geoip_reader_patcher.stop()os.remove(self.test_db_file)
我们在tearDown中停止了模拟修补器,以确保实际的调用不受影响。
-
最后,修改创建的产品测试用例以断言位置:
def test_create_product(self):"Test creation of new product"# ... Non changed code ...rv = self.client.post('/en/product-create',data={'name': 'iPhone 5','price': 549.49,'company': 'Apple','category': 1,'image': tempfile.NamedTemporaryFile()})self.assertEqual(rv.status_code, 302)rv = self.client.get('/en/product/1')self.assertEqual(rv.status_code, 200)self.assertTrue('iPhone 5' inrv.data.decode("utf-8"))self.assertTrue('America/Los_Angeles' inrv.data.decode("utf-8"))
在这里,在创建产品后,我们断言America/Los_Angeles值出现在渲染的产品模板的某个地方。
它是如何工作的...
运行测试并查看是否通过:
$ nose2 test_app.CatalogTestCase.test_create_product -v
test_create_product (test_app.CatalogTestCase)
Test creation of new product ... ok
----------------------------------------------------------------
Ran 1 test in 0.079s
OK
参见
有多种方法可以进行模拟。我演示了其中的一种。你可以从可用的方法中选择任何一种。请参阅docs.python.org/3/library/unittest.mock.html上的文档。
确定测试覆盖率
在之前的菜谱中,我们涵盖了测试用例的编写,但有一个重要的方面可以衡量测试的范围,称为覆盖率。覆盖率指的是我们的代码有多少被测试覆盖。覆盖率百分比越高,测试就越好(尽管高覆盖率不是良好测试的唯一标准)。在这个菜谱中,我们将检查我们应用程序的代码覆盖率。
小贴士
记住,100%的测试覆盖率并不意味着代码完美无缺。然而,在任何情况下,它都比没有测试或覆盖率低要好。记住,“如果没有经过测试, 它就是有缺陷的.”
准备工作
我们将使用一个名为coverage的库来完成这个菜谱。以下是其安装命令:
$ pip install coverage
如何做到这一点…
测量代码覆盖率的最简单方法是使用命令行:
-
简单地运行以下命令:
$ coverage run --source=<Folder name of the application> --omit=test_app.py,run.py test_app.py
这里,--source表示要考虑在覆盖率中的目录,而--omit表示在过程中需要排除的文件。
-
现在,要直接在终端上打印报告,请运行以下命令:
$ coverage report
以下截图显示了输出:

图 10.5 – 测试覆盖率报告
-
要获得覆盖率报告的漂亮 HTML 输出,请运行以下命令:
$ coverage html
这将在你的当前工作目录中创建一个名为htmlcov的新文件夹。在这个文件夹中,只需在浏览器中打开index.html,就可以看到完整的详细视图。

图 10.6 – 测试覆盖率报告的网页视图
或者,我们可以在测试文件中包含一段代码,并在每次运行测试时获取覆盖率报告。我们只需将以下代码片段添加到test_app.py中:
-
在做任何事情之前,添加以下代码以启动覆盖率评估过程:
import coveragecov = coverage.coverage(omit = ['/Users/apple/workspace/flask-cookbook-3/Chapter-10/lib/python3.10/site-packages/*','test_app.py'])cov.start()
这里,我们导入了coverage库并为它创建了一个对象。这告诉库忽略所有site-packages实例(因为我们不想评估我们没有编写的代码)以及测试文件本身。然后,我们启动了确定覆盖率的进程。
-
在代码的末尾,将最后一个块修改为以下内容:
if __name__ == '__main__':try:unittest.main()finally:cov.stop()cov.save()cov.report()cov.html_report(directory = 'coverage')cov.erase()
在前面的代码中,我们首先将unittest.main()放在一个try..finally块中。这是因为unittest.main()在所有测试执行完毕后退出。现在,在执行完这个方法后,必须强制运行与覆盖率相关的代码。我们停止了覆盖率报告,保存了它,然后在控制台上打印了报告,并在删除临时.coverage文件(这是作为过程的一部分自动创建的)之前生成了它的 HTML 版本。
它是如何工作的…
如果我们在包含覆盖率特定代码后运行测试,那么我们可以运行以下命令:
$ python test_app.py
输出将非常类似于图 10.5中的那个。
参见
还可以使用nose2库来确定覆盖率,我们在集成 nose2 库菜谱中讨论了它。我将把它留给你自己探索。有关更多信息,请参阅docs.nose2.io/en/latest/plugins/coverage.html。
使用性能分析来查找瓶颈
性能分析是在我们决定扩展应用程序时测量性能的重要且方便的工具。在扩展之前,我们想知道是否有任何进程是瓶颈并影响整体性能。Python 有一个内置的性能分析器cProfile,可以为我们完成这项工作,但为了使生活更简单,werkzeug有ProfilerMiddleware,它是基于cProfile编写的。在这个菜谱中,我们将使用ProfilerMiddleware来确定是否有任何影响性能的因素。
准备工作
我们将使用前一个菜谱中的应用程序,并将ProfilerMiddleware添加到一个名为generate_profile.py的新文件中。
如何做…
创建一个新文件generate_profile.py,与run.py并列,它的工作方式与run.py本身一样,但带有ProfilerMiddleware:
from werkzeug.middleware.profiler import ProfilerMiddleware
from my_app import app
app.wsgi_app = ProfilerMiddleware(app.wsgi_app,
restrictions = [10])
app.run(debug=True)
在这里,我们从werkzeug中导入了ProfilerMiddleware,然后修改了我们的 Flask 应用中的wsgi_app以使用它,限制输出中打印的前 10 个调用。
它是如何工作的…
现在,我们可以使用generate_profile.py来运行我们的应用程序:
$ python generate_profile.py
然后,我们可以创建一个新的产品。然后,针对该特定调用的输出将类似于以下截图:

图 10.7 – 性能分析器输出
从前面的截图可以看出,在这个过程中最密集的调用是对geoip数据库的调用。尽管它是一个单独的调用,但它花费了最多的时间。所以,如果我们决定在将来某个时候提高性能,这是需要首先查看的地方。
第十一章:部署和部署后
到目前为止,我们已经学习了如何以不同的方式编写 Flask 应用程序。部署应用程序和管理部署后的应用程序与开发它一样重要。部署应用程序的方法有很多,选择最佳方法取决于需求。从安全和性能的角度来看,正确部署应用程序非常重要。部署后监控应用程序有多种方式,其中一些是付费的,而另一些则是免费使用的。使用它们取决于它们的需求和提供的功能。
在本章中,我们将讨论各种应用程序部署技术,然后是部署后使用的某些监控工具。
我们将要介绍的每个工具和技术都有一套功能。例如,给应用程序添加过多的监控可能会给应用程序和开发者带来额外的开销。同样,忽略监控可能会导致未检测到的用户错误和整体用户不满。
注意
在本章中,我将专注于部署到Ubuntu 22.04服务器。这应该涵盖大多数情况。我会尝试涵盖 macOS 和 Windows 所需的任何特殊步骤,但不要将它们视为详尽的。
因此,我们应该明智地选择我们使用的工具,这将反过来尽可能简化我们的生活。
在部署后的监控工具方面,我们将讨论 New Relic。Sentry 是另一个从开发者的角度来看将证明是最有益的工具。我们已经在第十章的使用 Sentry 监控异常食谱中介绍了这一点,调试、错误处理和测试。
小贴士
本章将涵盖几个主题,并且它们都可以独立于彼此进行遵循/实施。你可以将其中一些组合起来以利用多个功能,我在认为最合适的地方提到了这一点。作为一名软件开发者,请自由地根据自己的判断选择用于何种目的的库。
在本章中,我们将介绍以下食谱:
-
使用 Apache 进行部署
-
使用 uWSGI 和 Nginx 进行部署
-
使用 Gunicorn 和 Supervisor 进行部署
-
使用 Tornado 进行部署
-
使用 S3 存储进行文件上传
-
使用 New Relic 管理和监控应用程序性能
-
使用 Datadog 进行基础设施和应用程序监控
使用 Apache 进行部署
在这个食谱中,我们将学习如何使用Apache部署 Flask 应用程序,这可以说是最受欢迎的 HTTP 服务器。对于 Python 网络应用程序,我们将使用mod_wsgi,它实现了一个简单的 Apache 模块,可以托管任何支持 WSGI 接口的 Python 应用程序。
注意
记住,mod_wsgi与 Apache 不同,需要单独安装。
准备工作
我们将从上一章的目录应用程序开始。不需要对现有代码进行任何修改。
对于使用 Apache 部署,确保在您打算部署的机器上安装了 Apache httpd 服务器的最新版本非常重要。通常,操作系统(尤其是 macOS)附带的 Apache 版本可能较旧,并且不会被 mod_wsgi 库支持。
注意
httpd 代表 Apache 软件基金会创建的守护进程实现的网络服务器。apache 和 httpd 通常可以互换使用。
对于 macOS,可以使用 Homebrew 安装 httpd:
$ brew install httpd
对于 Ubuntu,httpd 是默认提供的。只需简单升级即可:
$ sudo apt update
$ sudo apt install python3-dev
$ sudo apt install apache2 apache2-dev
对于 Windows 操作系统,请遵循官方文档:httpd.apache.org/docs/trunk/platform/。
一旦成功安装/更新了 Apache,下一步是安装 mod_wsgi 库。它可以直接在您的虚拟环境中安装:
$ pip install mod_wsgi
如何操作…
与本书的前几版截然不同,mod_wsgi 现在附带了一个现代的 mod_wsgi-express 命令,该命令消除了编写 Apache httpd 配置的所有复杂性。
mod_wsgi-express 命令的唯一参数是包含 Flask 应用程序对象的文件。因此,创建一个名为 wsgi.py 的文件,并包含以下内容:
from my_app import app as application
重要
由于 mod_wsgi 预期 application 关键字,因此需要将 app 对象导入为 application。
它是如何工作的…
使用以下命令调用 mod_wsgi 运行网络服务器:
$ mod_wsgi-express start-server wsgi.py --processes 4
现在,访问 http://localhost:8000/ 以查看应用程序的实际运行情况。
您也可以通过提供 --port 参数在特权端口(如 80 或 443)上运行您的应用程序。只需确保运行命令的 shell 用户/组有权限访问端口(s)。如果没有,您可以通过传递 --user 和 --group 参数将用户/组更改为相关的用户/组。
参考以下内容
参考以下链接了解 Apache 的更多信息:httpd.apache.org/
mod_wgsi 的最新文档可以在以下链接找到:modwsgi.readthedocs.io/en/develop。
您可以在以下链接中了解 WSGI 的相关信息:wsgi.readthedocs.org/en/latest/。
使用 uWSGI 和 Nginx 部署
对于已经了解 uWSGI 和 Nginx 作用的人来说,无需过多解释。uWSGI 既是协议也是应用程序服务器,提供完整的堆栈,以便您可以构建托管服务。Nginx 是一个非常轻量级的反向代理和 HTTP 服务器,能够处理几乎无限量的请求。Nginx 与 uWSGI 无缝协作,并提供许多底层的优化以获得更好的性能。在本配方中,我们将使用 uWSGI 和 Nginx 一起部署我们的应用程序。
准备工作
我们将使用之前配方中的应用程序,即 使用 Apache 部署,并使用相同的 wsgi.py 文件。
现在,安装 Nginx 和 uWSGI。在基于 Debian 的发行版,如 Ubuntu 上,它们可以很容易地使用以下命令安装:
$ sudo apt-get install nginx
$ pip install pyuwsgi
这些安装说明是针对特定操作系统的,因此请根据您使用的操作系统参考相应的文档。
确保您有一个sites-enabled文件夹用于 Nginx,因为这是我们存放特定站点配置文件的地方。通常,它已经存在于大多数安装的/etc/文件夹中。如果没有,请参考您操作系统的特定文档以了解如何解决这个问题。
如何操作...
按照以下步骤使用 uWSGI 首先部署应用程序,然后将其与 Nginx 作为反向代理结合:
-
第一步是创建一个名为
wsgi.py的文件:from my_app import app as application
这是因为uwsgi期望找到一个名为application的可执行文件,所以我们只需将我们的app导入为application。
-
接下来,在我们的应用程序根目录中创建一个名为
uwsgi.ini的文件:[uwsgi]http-socket = :9090wsgi-file = /home/ubuntu/cookbook3/Chapter-11/wsgi.pyprocesses = 3
在这里,我们已经配置了uwsgi以运行在processes中指定的工人数所提供的 HTTP 地址上的wsgi-file。
要测试 uWSGI 是否按预期工作,请运行以下命令:
$ uwsgi --ini uwsgi.ini
现在,将您的浏览器指向http://127.0.0.1:9090/;这应该会打开应用程序的主页。
- 在继续之前,编辑前面的文件,将
http-socket替换为socket。这将把协议从 HTTP 更改为 uWSGI(您可以在uwsgi-docs.readthedocs.io/en/latest/Protocol.html上了解更多信息)。
重要
您必须保持uwsgi进程运行。目前,我们已将其作为前台进程运行。
您可能希望将 uWSGI 进程作为后台无头服务自动运行,而不是在前景手动运行。有多种工具可以实现这一点,例如supervisord、circus等等。我们将在下一个菜谱中涉及supervisord的不同用途,但也可以在这里复制。我将把它留给你自己尝试。
-
现在,创建一个名为
nginx-wsgi.conf的新文件。这将包含为我们提供应用程序和静态内容的 Nginx 配置:server {location / {include uwsgi_params;uwsgi_pass 0.0.0.0:9090;}location /static/uploads/ {alias /home/ubuntu/cookbook3/Chapter-11/flask_test_uploads/;}}
在前面的代码块中,uwsgi_pass指定了需要映射到指定位置的 uWSGI 服务器。
小贴士
nginx-wsgi.conf文件可以创建在任何位置。它可以与您的代码包一起创建,以便进行版本控制,或者如果您有多个.conf文件,可以放置在/etc/nginx/sites-available以方便维护。
-
使用以下命令从该文件创建到我们之前提到的
sites-enabled文件夹的软链接:$ sudo ln -s ~/cookbook3/Chapter-11/nginx-wsgi.conf /etc/nginx/sites-enabled/ -
默认情况下,Nginx 在
sites-available文件夹中附带一个名为default的站点配置,并有一个指向sites-enabled文件夹的符号链接。取消链接以为我们提供应用程序;否则,默认设置将阻止我们的应用程序被加载:$ sudo unlink /etc/nginx/sites-enabled/default -
在完成所有这些之后,使用以下命令重新加载 Nginx 服务器:
$ sudo systemctl reload nginx.service
将您的浏览器指向 http://127.0.0.1/ 或 http://<您的服务器 IP 或域名地址> 以查看通过 Nginx 和 uWSGI 提供的应用程序。
参考以下内容
有关 uWSGI 的更多信息,请参阅 uwsgi-docs.readthedocs.org/en/latest/。
有关 Nginx 的更多信息,请参阅 www.nginx.com/。
DigitalOcean 上有一篇关于 Nginx 和 uWSGI 的好文章。我建议您阅读它,以便更好地理解这个主题。它可在 www.digitalocean.com/community/tutorials/how-to-serve-flask-applications-with-uwsgi-and-nginx-on-ubuntu-22-04 找到。
要了解 Apache 和 Nginx 之间的差异,我认为 Anturis 的一篇文章相当不错,该文章可在 anturis.com/blog/nginx-vs-apache/ 找到。
使用 Gunicorn 和 Supervisor 进行部署
Gunicorn 是一个适用于 Unix 的 WSGI HTTP 服务器。它非常简单易实现,超轻量级,并且相当快速。它的简单性在于它与各种 Web 框架的广泛兼容性。
Supervisor 是一个监控工具,它可以控制各种子进程,并在这些子进程突然退出或由于其他原因时处理启动/重启这些子进程。它可以扩展到通过 XML-RPC API 在远程位置控制进程,而无需您登录到服务器(我们在这里不会讨论这一点,因为它超出了本书的范围)。
有一个需要注意的事情是,这些工具可以与其他在先前的菜谱中提到的应用程序一起使用,例如使用 Nginx 作为代理服务器。这留给你去尝试。
准备工作
我们将首先安装这两个包——即 gunicorn 和 supervisor。这两个包都可以直接使用 pip 进行安装:
$ pip install gunicorn
$ pip install supervisor
如何做这件事...
按照以下步骤操作:
-
要检查
gunicorn包是否按预期工作,只需从我们的应用程序文件夹中运行以下命令:$ gunicorn -w 4 -b 0.0.0.0:8000 my_app:app
然后,将您的浏览器指向 http://0.0.0.0:8000/ 或 http://<IP 地址或域名>:8000/ 以查看应用程序的主页。
-
现在,我们需要使用 Supervisor 做与之前相同的事情,以便这个进程作为一个由 Supervisor 本身控制的守护进程运行,而不是通过人工干预。首先,我们需要一个 Supervisor 配置文件。这可以通过在您的虚拟环境中运行以下命令来实现。默认情况下,Supervisor 会寻找一个包含名为
supervisord.conf文件的etc文件夹。在系统范围内安装中,此文件夹是/etc/,而在虚拟环境中,它将在虚拟环境根文件夹中寻找一个etc文件夹,然后回退到/etc/。因此,建议您在虚拟环境中创建一个名为etc的文件夹,以保持关注点的分离:$ mkdir etc$ echo_supervisord_conf > etc/supervisord.conf
最后一个命令将在 etc 文件夹中创建一个名为 supervisord.conf 的配置文件。此文件包含使用 Supervisor 守护进程运行的进程的所有配置。
信息
echo_supervisord_conf 程序由 Supervisor 提供;它将一个示例配置文件打印到指定的位置。如果在运行命令时遇到权限问题,请尝试使用 sudo。
-
现在,在您之前创建的文件中添加以下配置块:
[program:flask_catalog]command=<path to virtual environment>/bin/gunicorn -w4 -b 0.0.0.0:8000 my_app:appdirectory=<path to application directory>user=someuser # some user with relevant permissionsautostart=trueautorestart=truestdout_logfile=/tmp/app.logstderr_logfile=/tmp/error.log
在这里,我们指定了 gunicorn 进程作为要运行的命令,以及用于进程的目录和用户。其他设置指定了 Supervisor 守护进程启动或重启时的行为以及保存相应日志文件的位置。
小贴士
注意,您永远不应该以 root 用户身份运行应用程序。这本身就是一个大安全漏洞,因为应用程序可能会崩溃,或者漏洞可能会损害操作系统本身。
-
设置完成后,使用以下命令运行
supervisord:$ supervisord
它是如何工作的...
要检查应用程序的状态,请运行以下命令:
$ supervisorctl status
flask_catalog RUNNING pid 112039, uptime 0:00:06
此命令提供了所有子进程的状态。
小贴士
在本食谱中讨论的工具可以与 Nginx 配合使用,作为反向代理服务器。我建议您亲自尝试一下。
每次您对应用程序进行更改并希望重启 Gunicorn 以反映所做的更改时,请运行以下命令:
$ supervisorctl restart all
您也可以指定特定的进程而不是重启所有内容:
$ supervisorctl restart flask_catalog
参见
您可以在 gunicorn-docs.readthedocs.org/en/latest/index.html 上了解更多关于 Gunicorn 的信息。
有关 Supervisor 的更多信息,请参阅 supervisord.org/index.html。
使用 Tornado 部署
Tornado 是一个完整的 Web 框架,本身也是一个独立的 Web 服务器。在这里,我们将使用 Flask 创建我们的应用程序,Flask 是 URL 路由和模板的组合,而将服务器部分留给 Tornado。Tornado 被构建来处理数千个同时的连接,使得应用程序非常可扩展。
信息
Tornado 在处理 WSGI 应用程序时存在限制,因此请明智选择!您可以在www.tornadoweb.org/en/stable/wsgi.html#running-wsgi-apps-on-tornado-servers了解更多信息。
准备工作
可以使用pip安装 Tornado:
$ pip install tornado
如何操作…
让我们创建一个名为tornado_server.py的文件,并将以下代码放入其中:
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
from my_app import app
http_server = HTTPServer(WSGIContainer(app))
http_server.listen(8000)
IOLoop.instance().start()
在这里,我们为我们的应用程序创建了一个 WSGI 容器;然后使用该容器创建一个 HTTP 服务器,并将应用程序托管在端口8000上。
它是如何工作的…
使用以下命令运行我们在上一节中创建的 Python 文件:
$ python tornado_server.py
将您的浏览器指向http://0.0.0.0:8000/或http://<IP 地址或域名>:8000/以查看正在提供的主页。
小贴士
我们可以将 Tornado 与 Nginx(作为反向代理以提供静态内容)和 Supervisor(作为进程管理器)结合使用以获得最佳效果。这留给你作为练习。
使用 S3 存储进行文件上传
亚马逊网络服务(AWS)将 S3 解释为互联网的存储,旨在使开发者的 Web 规模计算更容易。S3 通过 Web 服务提供了一个非常简单的接口;这使得在任何时间从互联网的任何地方存储和检索任何数量的数据都非常简单。到目前为止,在我们的目录应用程序中,我们看到了在管理作为创建过程一部分上传的产品图像时存在一些问题。如果图像存储在某个全球位置并且可以从任何地方轻松访问,那么整个头疼问题都将消失。我们将使用 S3 来完成同样的目的。
准备工作
亚马逊提供了boto3。可以使用pip进行安装:
$ pip install boto3
如何操作…
现在,对我们的现有目录应用程序进行一些修改,以适应对文件上传和从 S3 检索的支持:
-
首先,我们需要存储 AWS 特定的配置,以便
boto3能够调用 S3。将以下语句添加到应用程序的配置文件中——即my_app/__init__.py。确保将以下配置值与现有的其他配置值分组:app.config['AWS_ACCESS_KEY'] = 'AWS Access Key'app.config['AWS_SECRET_KEY'] = 'AWS Secret Key'app.config['AWS_BUCKET'] = 'Name of AWS Bucket'
您可以从 AWS IAM 获取这些值。确保与这些凭证关联的用户有权在 S3 中创建和获取对象。
-
接下来,我们需要更改我们的
views.py文件:import boto3
这是我们需要从boto3导入的。接下来,我们需要替换create_product()中的以下行:
image.save(os.path.join(current_app.config['UPLOAD_FOL
DER'], filename))
我们必须用以下代码块替换它:
session = boto3.Session(
aws_access_key_id=current_app
.config['AWS_ACCESS_KEY'],
aws_secret_access_key=current_app
.config['AWS_SECRET_KEY']
)
s3 = session.resource('s3')
bucket = s3.Bucket(current_app
.config['AWS_BUCKET'])
if bucket not in list(s3.buckets.all()):
bucket = s3.create_bucket(
Bucket=current_app
.config['AWS_BUCKET'],
CreateBucketConfiguration={
'LocationConstraint':
'ap-south-1'
},
)
bucket.upload_fileobj(
image, filename,
ExtraArgs={'ACL': 'public-read'})
通过这个代码更改,我们实际上是在更改我们保存文件的方式。之前,图片是通过使用image.save在本地保存的。现在,这是通过创建一个 S3 连接并将图片上传到该桶来完成的。首先,我们使用boto3.Session与 AWS 创建一个session连接。我们使用这个会话来访问 S3 资源,然后创建一个bucket(如果不存在,则使用相同的;否则,使用相同的)并使用位置约束'ap-south-1'。这个位置约束不是必需的,可以根据需要使用。最后,我们将我们的图片上传到桶中。
-
最后的更改将应用于我们的
product.html模板,我们需要更改图片的src路径。将原始的img src语句替换为以下语句:<img src="img/pre>1.amazonaws.com/' + config['AWS_BUCKET']- '/' + product.image_path }}"/>
它是如何工作的…
现在,像往常一样运行应用程序并创建一个产品。当创建的产品被渲染时,产品图片会花费一点时间显示出来,因为它现在是从 S3(而不是从本地机器)提供的。如果发生这种情况,那么与 S3 的集成已经成功。
使用 New Relic 管理和监控应用程序性能
New Relic 是一款提供与应用程序相关的近实时运营和业务分析软件。它从多个方面对应用程序的行为进行深入分析。它执行了分析器的任务,同时也消除了在应用程序中维护额外移动部件的需要。它遵循数据推送原则,即我们的应用程序将数据发送到 New Relic,而不是 New Relic 从我们的应用程序请求统计数据。
准备工作
我们将使用我们在整本书中构建的目录应用程序。本质上,这里要使用的应用程序并不重要;它应该只是一个正在运行的 Flask 应用程序。
第一步将是使用 New Relic 注册一个账户。遵循简单的注册流程,完成并验证电子邮件后,您将被发送到仪表板。在这里,从 New Relic 提供的系列产品中选择应用程序监控作为我们需要使用的产品,并选择Python作为技术栈:

图 11.1 – New Relic 堆栈选择
这个引导安装小部件在提供要运行的命令之前会询问正在使用的操作系统:

图 11.2 – New Relic 安装配置
在此之后,您可以选择手动进行配置和设置,或者只需遵循 New Relic 概述的步骤。我将在下一节讨论这两种方法。
对于手动方法,请确保复制显示在上一个屏幕截图中的许可证密钥。
如何操作…
您可以选择引导或手动配置。
引导配置
以下屏幕截图列出了你需要遵循的所有步骤,以便让你的应用与 New Relic 进行 APM:

图 11.3 – New Relic 指导配置
手动配置
一旦我们有了许可证密钥,我们需要安装 newrelic Python 库:
$ pip install newrelic
现在,我们需要生成一个名为 newrelic.ini 的文件,该文件将包含有关许可证密钥、我们的应用名称等详细信息。可以使用以下命令完成此操作:
$ newrelic-admin generate-config <LICENSE-KEY> newrelic.ini
在前面的命令中,将 LICENSE-KEY 替换为你的实际许可证密钥。现在,我们有一个名为 newrelic.ini 的新文件。打开并编辑该文件,根据需要更改应用名称和其他内容。
要检查 newrelic.ini 文件是否成功运行,请运行以下命令:
$ newrelic-admin validate-config newrelic.ini
这将告诉我们验证是否成功。如果不成功,请检查许可证密钥及其有效性。
现在,在应用的配置文件顶部添加以下行,在我们的例子中是 my_app/__init__.py。确保在导入任何其他内容之前添加这些行:
import newrelic.agent
newrelic.agent.initialize('newrelic.ini')
它是如何工作的...
现在,当你运行你的应用时,它将开始向 New Relic 发送统计数据,仪表板将添加一个新的应用。在下面的屏幕截图中,有两个应用,其中一个是我们在工作的应用,另一个是我们之前运行以验证 New Relic 是否正常工作的测试:

图 11.4 – New Relic 应用列表
打开特定应用页面;会出现大量的统计数据。它还会显示哪些调用花费了最多的时间以及应用的性能如何。你还会看到多个菜单项,每个菜单项都将对应不同类型的监控,以涵盖所有必要的方面。
相关阅读
你可以在 docs.newrelic.com/ 上了解更多关于 New Relic 及其配置的信息。
使用 Datadog 进行基础设施和应用监控
Datadog 是一种可观察性服务,为基础设施、数据库、应用和服务提供详细的统计分析。就像 New Relic 一样,Datadog 是一个全栈平台,允许全方位的监控,为应用和基础设施的健康状况提供深刻的见解。
尽管在几乎所有方面,Datadog 和 New Relic 都很相似,但它们各自都有一些相对于对方的优点。例如,有一种普遍的观点认为,虽然 New Relic 在应用性能监控(APM)方面表现优秀,但 Datadog 在基础设施监控方面更加强大。
简而言之,这两个平台对于大多数用途来说都很棒,你可以选择使用其中的任何一个,或者根据你的需求选择其他工具/平台。
准备工作
就像在之前的菜谱中一样,我们将使用我们在整本书中构建的目录应用程序。本质上,这里要使用的应用程序无关紧要;它应该只是一个正在运行的 Flask 应用程序。
第一步是使用 Datadog 创建一个新账户。他们有一个免费层,对于测试来说足够了,就像在我们的案例中一样。一旦你注册并登录,第一步就是在 Datadog 控制台中启用/安装 Python 集成:

图 11.5 – 安装 Python 集成
如前述截图所示,点击Python磁贴并安装集成。
一旦你在 Datadog 上创建了一个账户,它就会默认创建一个 API 密钥。这个 API 密钥将在所有后续步骤中都需要,以确保从你的机器/服务器和应用程序发送的监控统计数据被发送到正确的 Datadog 账户。导航到你的账户 | 组织设置 | API 密钥来获取你的 API 密钥,或者直接访问app.datadoghq.com/organization-settings/api-keys:

图 11.6 – 获取 API 密钥
如何操作...
按照以下步骤设置 Datadog 的基础设施和应用程序监控:
- 一旦你获取了 API 密钥,下一步就是在你的操作系统上安装 Datadog 代理,以便进行基础设施监控。根据你的操作系统或基础设施,通过导航到集成 | 代理来遵循相应的说明:

图 11.7 – 安装 Datadog 代理
如前述截图所示,选择你的操作系统并按照相应的说明操作。你的操作系统可能会要求一些权限,你需要授予这些权限以确保 Datadog 能够正常工作。
- 安装代理后,导航到基础设施 | 基础设施列表以验证你的基础设施正在被监控:

图 11.8 – 基础设施监控验证
随意探索其他选项或深入了解细节,以查看更多分析。
-
下一步是安装
ddtrace实用程序,这是 Datadog 的 Python APM 客户端,允许你分析代码、请求并将跟踪数据流到 Datadog:$ pip install ddtrace
重要
如果你的应用程序正在运行在端口 5000 上,请确保你将其更改到其他端口——端口 5000 到 5002 被 Datadog 用于其代理和其他工具。
-
在安装
ddtrace之后,使用以下命令运行你的 Flask 应用程序:$ DD_SERVICE="<your service name>" DD_ENV="<your environment name>" ddtrace-run python run.py
注意DD_SERVICE和DD_ENV环境变量。这些对于 Datadog 决定如何隔离和分组你的应用程序日志非常重要。
如果你遇到了以下截图所示的错误,只需设置DD_REMOTE_CONFIGURATION_ENABLED=false:

图 11.9 – 处理配置错误
因此,在我的情况下,命令看起来是这样的:
$ DD_SERVICE="flask-cookbook" DD_ENV="stage" DD_REMOTE_CONFIGURATION_ENABLED=false ddtrace-run python run.py
- 现在,只需等待几分钟——你的应用程序统计信息应该开始在 Datadog 上反映:

图 11.10 – APM 在行动
随意尝试并深入了解更多细节,以了解 Datadog 的工作原理以及它提供哪些统计信息。
参见
你可以在以下链接中了解更多关于 Datadog 和 ddtrace 的信息:
-
Flask 为
ddtrace的配置:ddtrace.readthedocs.io/en/stable/integrations.html#flask -
Datadog 文档:
docs.datadoghq.com/
第十二章:微服务和容器
到目前为止,我们一直将完整的应用程序作为一个代码块(通常称为单体)来开发,它通常被设计、测试和部署为一个单一单元。扩展也以类似的方式进行,要么整个应用程序进行扩展,要么不扩展。然而,随着应用程序规模的扩大,自然希望将单体分解成更小的块,以便可以分别管理和扩展。解决这个问题的方案是微服务。本章全部关于微服务,我们将探讨创建和管理微服务的几种方法。
微服务是一种将软件应用程序作为多个松散耦合服务的集合来开发和架构的方法。这些服务被设计和开发出来,以帮助构建具有清晰和细粒度接口的单功能模块。如果设计和架构得当,这种模块化的好处是,整体应用程序更容易理解、开发、维护和测试。多个小型自主团队可以并行工作在多个微服务上,因此开发并交付应用程序的时间实际上得到了有效减少。现在,每个微服务都可以单独部署和扩展,这允许减少停机时间和实现成本效益的扩展,因为只有高流量服务可以根据预定义的标准进行扩展。其他服务可以按常规运行。
本章将从一些常见的术语开始,这些术语可能在讨论微服务时都会听到——那就是,容器和 Docker。首先,我们将探讨如何使用 Docker 容器部署 Flask 应用程序。然后,我们将探讨如何使用 Kubernetes(这是最好的容器编排工具之一)有效地扩展和管理多个容器。接着,我们将探讨如何使用一些云平台(如AWS Lambda和GCP Cloud Run)创建完全管理的微服务。最后,我们将探讨如何使用GitHub Actions将所有这些整合成一个无缝的部署管道。
在本章中,我们将涵盖以下内容:
-
使用 Docker 容器化
-
使用 Kubernetes 编排容器
-
使用 Google Cloud Run 进行无服务器部署
-
使用 GitHub Actions 进行持续部署
使用 Docker 容器化
容器可以被视为一个标准化的代码包,其中包含了运行应用程序及其所有依赖项所需的代码,这使得应用程序可以在多个环境和平台上以统一的方式运行。Docker是一个工具,它允许通过标准且简单的方法创建、分发、部署和运行使用容器的应用程序。
Docker 实质上是一种虚拟化软件,但它不是可视化整个操作系统,而是允许应用程序使用底层主机操作系统,并要求应用程序根据需要打包额外的依赖项和组件。这使得 Docker 容器镜像非常轻量级且易于分发。
准备工作
第一步是安装 Docker。Docker 的安装步骤因所使用的操作系统而异。每个操作系统的详细步骤可以在 docs.docker.com/install/ 找到。
小贴士
Docker 是一种快速发展的软件,在过去的几年中已经发布了多个主要版本,其中许多旧版本已被弃用。我建议您始终仔细阅读文档,以避免安装任何 Docker 的旧版本。
一旦 Docker 成功安装,请转到终端并运行以下命令:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
之前的命令用于列出所有正在运行的容器。如果它没有错误地运行并显示以 CONTAINER ID 开头的标题行,那么 Docker 已成功安装。
信息
Docker 的不同版本,无论是旧版还是当前版本,都被称为 Docker Toolbox、Docker Machine、Docker Engine、Docker Desktop 等。这些名称将在文档和互联网上的其他资源中多次出现。有很大可能性,这些名称在未来也可能发生变化或发展。为了简单起见,我将只称所有这些为 Docker。
验证 Docker 安装的一个更有趣的方法是尝试运行一个 hello-world 容器。只需运行以下命令:
$ docker run hello-world
之前的命令应该给你以下输出。它列出了 Docker 执行此命令所采取的步骤。我建议仔细阅读:

图 12.1 – 测试 Docker
如何操作...
我们将从 使用 New Relic 管理和监控应用程序性能 的配方中的目录应用程序开始,该配方位于 第十一章:
-
创建容器的第一步是为它创建一个镜像。可以通过创建一个名为
Dockerfile的文件以脚本方式轻松创建 Docker 镜像。我们的应用程序的基本Dockerfile可能如下所示:FROM python:3WORKDIR /usr/src/appCOPY requirements.txt requirements.txtRUN pip install -r requirements.txtCOPY . .ENTRYPOINT [ "python" ]CMD [ "run.py" ]
上述文件中的每一行都是一个以线性自上而下的方式执行的命令。FROM 指定了新应用程序容器镜像将构建在其上的基础容器镜像。我选择了基础镜像 python:3,这是一个安装了 Python 3.11 的 Linux 镜像。
信息
写这本书的时候,python:3 Docker 基础镜像预装了 Python 3.11。这会随着时间的推移而改变。
WORKDIR指示应用程序将被安装的默认目录。我已经将其设置为/usr/src/app。在此之后运行的任何命令都将从这个文件夹内部执行。
COPY命令简单地将从本地机器上指定的文件复制到容器文件系统中。我已经将requirements.txt复制到了/usr/src/app。
这后面跟着RUN,它执行提供的命令。在这里,我们使用pip安装了requirements.txt中的所有依赖项。然后,我简单地从当前本地文件夹(本质上是我的应用程序根文件夹)复制了所有文件到/usr/src/app。
最后,定义了一个ENTRYPOINT,它指示了默认的CMD命令,当容器启动时应运行此命令。在这里,我简单地通过运行python run.py来运行我的应用程序。
信息
Dockerfile 提供了许多其他关键字,所有这些都可以用来创建强大的脚本。有关更多信息,请参阅docs.docker.com/engine/reference/builder/。
小贴士
运行应用程序有多种方式,如第十一章中概述的,部署和部署后。我强烈建议你在将应用程序 docker 化时使用这些方法。
-
要运行 Dockerfile,你必须在应用程序的根文件夹中有一个
requirements.txt文件。如果你没有,你可以简单地使用以下命令生成一个requirements.txt文件:$ pip freeze > requirements.txt
以下是我的requirements.txt文件。我鼓励你生成自己的文件,而不是使用下面的文件,因为 Python 包版本会演变,你将希望使用最新和最相关的版本:
aiohttp==3.8.4
aiosignal==1.3.1
async-timeout==4.0.2
attrs==22.2.0
Babel==2.11.0
blinker==1.5
boto3==1.26.76
botocore==1.29.76
certifi==2022.12.7
charset-normalizer==3.0.1
click==8.1.3
Flask==2.2.3
flask-babel==3.0.1
Flask-SQLAlchemy==3.0.3
Flask-WTF==1.1.1
frozenlist==1.3.3
geoip2==4.6.0
greenlet==2.0.2
gunicorn==20.1.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
jmespath==1.0.1
MarkupSafe==2.1.2
maxminddb==2.2.0
multidict==6.0.4
python-dateutil==2.8.2
pytz==2022.7.1
requests==2.28.2
s3transfer==0.6.0
sentry-sdk==1.15.0
six==1.16.0
SQLAlchemy==2.0.4
tornado==6.2
typing_extensions==4.5.0
urllib3==1.26.14
Werkzeug==2.2.3
WTForms==3.0.1
yarl==1.8.2
newrelic==8.7.0
小贴士
如果你的requirements.txt文件中有mod_wsgi,你可以从文件中删除它,除非你特别想使用Apache在 Docker 容器中运行你的应用程序。mod_wsgi是一个特定于操作系统的包,你开发机器上的操作系统可能与 Docker 镜像上的操作系统不匹配,这会导致安装失败。
-
在
run.py中需要做一些小的修改,修改后它将如下所示:from my_app import appapp.run(debug=True, host='0.0.0.0', port='8000')
我已经将host参数添加到了app.run中。这允许应用程序在 Docker 容器外部被访问。
-
在创建
Dockerfile之后,构建一个 Docker 容器镜像,然后可以像这样运行:$ docker build -t cookbook .
在这里,我们要求 Docker 使用同一位置的 Dockerfile 构建一个镜像。-t参数设置了将要构建的镜像的名称/标签。最后的参数是一个点(.),表示当前文件夹中的所有内容都需要打包到构建中。当第一次运行此命令时,可能需要一段时间来处理,因为它将下载基础镜像以及我们应用程序的所有依赖项。
让我们检查创建的镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cookbook latest bceac988395a 48 seconds ago 1.13GB
-
接下来,运行此镜像以创建一个容器:
$ docker run -d -p 8000:8000 cookbook:latest
92a7ee37e044cf59196f5ec4472d9ffb540c7f48ee3f4f1e5f978f7f93b301ba
在这里,我们要求 Docker 使用在底部的 Dockerfile 中指定的命令来运行容器。-d 参数要求 Docker 在后台以分离模式运行容器;否则,它将阻塞当前 shell 窗口的控制。-p 将主机机的端口映射到 Docker 容器端口。这意味着我们要求 Docker 将本地机器上的端口 8000 映射到容器上的端口 8000。8000 是我们运行 Flask 应用程序(见 run.py)的端口。最后一个参数是容器镜像的名称,它是 REPOSITORY 和 TAG 的组合,如 docker images 命令所示。或者,你也可以只提供一个 IMAGE ID。
它是如何工作的…
打开浏览器并访问 http://localhost:8000/ 来查看正在运行的应用程序。
现在,再次运行 docker ps 来查看正在运行的容器的详细信息:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
92a7ee37e044 cookbook:latest "python run.py" 7 seconds ago Up 6 seconds 0.0.0.0:8000->8000/tcp beautiful_ritchie
相关阅读
-
你可以在
docs.docker.com/engine/reference/builder/上了解更多关于 Dockerfile 的信息。 -
Docker 对容器的定义可以在
www.docker.com/resources/what-container中阅读。 -
你可以在
en.wikipedia.org/wiki/Microservices上了解有关微服务的一般信息。 -
Martin Fowler 关于微服务的第一篇文章可以在
martinfowler.com/articles/microservices.html找到。
使用 Kubernetes 编排容器
如前一个菜谱所示,Docker 容器非常简单且强大,但如果没有强大的容器编排系统,管理容器可能会变得相当复杂。Kubernetes(也写作 K8s)是一个开源的容器编排系统,它自动化了容器化应用程序的管理、部署和扩展。它最初是在 Google 开发的,经过多年的发展,已经成为最受欢迎的容器编排软件。它在所有主要云提供商中都有广泛的应用。
准备工作
在这个菜谱中,我们将看到如何利用 Kubernetes 自动部署和扩展我们在上一个菜谱中创建的应用容器。
Kubernetes 与 Docker Desktop 的新版本一起打包,并以非常直接的方式工作。然而,我们将使用 minikube,这是 Kubernetes 本身提供的一个标准发行版。它相当受欢迎,适合入门。对于这个菜谱,我将使用 Minikube,它将允许在本地机器上的虚拟机中运行单个节点的 Kubernetes 集群。你也可以选择使用其他 Kubernetes 发行版;有关更多详细信息,请参阅 kubernetes.io/docs/setup/。
要安装 Minikube,请按照您操作系统的说明在 minikube.sigs.k8s.io/docs/start/ 中进行操作。
信息
Kubernetes 是一个涵盖多个维度的巨大主题。有多个书籍仅专注于 Kubernetes,还有更多正在编写。在这个配方中,我们将介绍 Kubernetes 的一个非常基本的实现,以便您熟悉它。
如何操作...
按照以下步骤了解如何创建和使用本地 Kubernetes 集群:
-
安装 Minikube 后,在您的本地机器上创建一个 Minikube 集群:
$ minikube start😄 minikube v1.29.0 on Darwin 13.0✨ Automatically selected the docker driver. Other choices: hyperkit, ssh📌 Using Docker Desktop driver with root privileges👍 Starting control plane node minikube in cluster minikube🚜 Pulling base image …. . . .Downloading and installing images. . .🔎 Verifying Kubernetes components...🌟 Enabled addons: storage-provisioner, default-storageclass🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
如您所见,前面的命令下载了一组镜像以设置和运行 Minikube,这将在您的本地机器上创建一个虚拟机。使用这些镜像创建虚拟机后,将启动一个仅有一个节点的简单 Kubernetes 集群。这个过程在第一次运行时可能需要一些时间。
Minikube 还提供了一个浏览器仪表板视图。可以通过运行以下命令来启动:
$ minikube dashboard
🔌 Enabling dashboard ...
▪ Using image docker.io/kubernetesui/metrics-scraper:v1.0.8
▪ Using image docker.io/kubernetesui/dashboard:v2.7.0
🤔 Verifying dashboard health ...
🚀 Launching proxy ...
🤔 Verifying proxy health ...
🎉 Opening http://127.0.0.1:57206/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
访问前面命令输出的 URL 来查看仪表板。
在 Kubernetes 中,容器在 pod 中部署,其中 pod 可以定义为共享资源和网络的一组一个或多个容器。在这个配方中,我们将在一个 pod 中只有一个容器。
-
无论何时使用 Minikube 创建部署,它都会在云基础镜像仓库中查找 Docker 镜像,例如 Docker Hub 或 Google Cloud Registry,或者某些自定义的仓库。对于这个配方,我们打算使用本地 Docker 镜像来创建部署。因此,我们将运行以下命令,该命令将
docker环境设置为minikube docker:$ eval $(minikube -p minikube docker-env) -
现在,使用前面设置的
docker环境重新构建 Docker 镜像:$ docker build -t cookbook .
关于构建 Docker 镜像的更多细节,请参考之前的配方,使用 Docker 容器化。
-
接下来,在您的应用程序根目录中创建一个名为
cookbook-deployment.yaml的文件:apiVersion: apps/v1kind: Deploymentmetadata:creationTimestamp: nulllabels:app: cookbook-recipename: cookbook-recipespec:replicas: 1selector:matchLabels:app: cookbook-recipestrategy: {}template:metadata:creationTimestamp: nulllabels:app: cookbook-recipespec:containers:- image: cookbook:latestname: cookbookresources: {}imagePullPolicy: Neverstatus: {}
在此文件中,我们创建了一个名为 cookbook-recipe 的部署,它使用 cookbook:latest 镜像。请注意 imagePullPolicy,它设置为 Never – 这表示 kubectl 不应尝试从在线 Docker 仓库(如 Docker Hub 或 Google Container Registry (GCR))获取镜像;相反,它应始终在本地搜索此镜像。
-
现在,使用 kubectl
apply之前文件以创建 Kubernetes 部署:$ kubectl apply -f cookbook-deployment.yamldeployment.apps/cookbook-recipe created
您可以通过运行以下命令来验证和获取已创建部署的状态:
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
cookbook-recipe 1/1 1 1 26s
检查 READY、UP-TO-DATE 和 AVAILABLE 列的值。这些值代表集群中我们的应用程序副本的数量。
-
我们的应用程序现在正在运行,但目前无法在集群外部访问。要使应用程序在 Kubernetes 集群外部可用,请创建一个
LoadBalancer类型的服务:$ kubectl expose deployment cookbook-recipe --type=LoadBalancer --port=8000service/cookbook-recipe exposed
在上一步中创建的部署将我们的应用程序暴露在端口8000上,这是集群内部的。前面的命令已经将这个内部的8000端口暴露给集群外部的任何随机端口,因此可以通过浏览器访问。
它是如何工作的...
要在浏览器中打开应用程序,请运行以下命令:
$ minikube service cookbook-recipe

图 12.2 – 使用 Kubernetes 部署的服务
运行前面的命令将在随机端口(如58764)上的浏览器中打开应用程序。
使用 Kubernetes 扩展部署非常简单。这只需要运行一个命令。通过这样做,应用程序将在多个 Pod 中进行复制:
$ kubectl scale --replicas=3 deployment/cookbook-recipe
deployment.apps/cookbook-recipe scaled
现在,再次查看部署的状态:
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
cookbook-recipe 3/3 3 3 90s
检查READY、UP-TO-DATE和AVAILABLE列中的副本值,这些值将从1增加到3。
更多内容...
您可以查看 Kubernetes 自动为部署和服务创建的 YAML 配置:
apiVersion: v1
kind: Service
metadata:
creationTimestamp: "2023-03-06T08:23:24Z"
labels:
app: cookbook-recipe
name: cookbook-recipe
namespace: default
resourceVersion: "5991"
uid: 1253962c-f3d5-4a1f-b997-c11a4abd2b33
spec:
allocateLoadBalancerNodePorts: true
clusterIP: 10.105.38.134
clusterIPs:
- 10.105.38.134
externalTrafficPolicy: Cluster
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports:
- nodePort: 31473
port: 8000
protocol: TCP
targetPort: 8000
selector:
app: cookbook-recipe
sessionAffinity: None
type: LoadBalancer
status:
loadBalancer: {}
上一段代码是服务的配置。您可以选择创建/保存并应用这个配置(而不是像我们在步骤 5中为部署所做的那样,在命令行中指定配置值)。
信息
在这个菜谱中,我展示的是 Kubernetes 的一个非常基本的实现。这个目的在于让您熟悉 Kubernetes。这个菜谱并不打算成为一个生产级别的实现。理想情况下,需要创建配置文件,然后围绕这些文件构建整体的 Kubernetes 部署。我敦促您基于这个菜谱的知识,努力追求 Kubernetes 的生产级技术。
参见
要了解更多信息,请查看以下资源:
-
从
kubernetes.io/docs/concepts/overview/开始了解 Kubernetes。 -
Kubernetes 的基础知识可在
kubernetes.io/docs/tutorials/kubernetes-basics/找到。 -
可以遵循
kubernetes.io/docs/tutorials/hello-minikube/中的 Minikube 教程。 -
您可以在
minikube.sigs.k8s.io/docs/start/了解 Minikube 安装的详细信息。 -
完整的 Kubernetes 文档可在
kubernetes.io/docs/home/找到。
使用 Google Cloud Run 实现无服务器
无服务器计算是一种云计算模型,其中云服务提供商运行服务器,并根据消费动态调整机器资源的分配,通过扩展或缩减资源来管理。计费基于实际使用的资源。它还简化了部署代码的整体流程,并且对于不同环境(如开发、测试、预生产和生产)维护不同的执行变得相对容易。无服务器计算的特性使这种模型成为开发部署大量微服务而不必担心管理开销的理想选择。
知识点
为什么这个模型被称为“无服务器”,尽管其中涉及服务器?
尽管涉及一个服务器来托管你的应用程序并处理进入应用程序的请求,但服务器的生命周期与单个请求一样短。因此,你可以将服务器视为只为单个请求而存在的东西。因此,服务器的生命周期通常是毫秒级的。
如谷歌所述,Cloud Run 是一个托管计算平台,允许你直接在谷歌的可扩展基础设施上运行容器。此外,由于 Google Cloud 提供了大量与 Cloud Run 非常好集成的其他服务,它允许我们构建具有许多不同云供应商部件的全功能应用程序。
注意
在整个菜谱中,我将交替使用 Google Cloud、Google Cloud Platform 和 GCP 这三个术语。
小贴士
使用无服务器工具进行部署允许开发者更多地专注于编写代码,而不是担心基础设施。
准备工作
在你能够使用 Cloud Run 部署之前,你需要设置你的机器上的 gcloud CLI,从那里你将部署你的应用程序到 Cloud Run。按照以下步骤操作:
- 首先,在 Google Cloud Platform 上创建一个账户。如果你还没有账户,请访问
console.cloud.google.com/getting-started并创建一个新的账户。然后,创建一个项目,你将在其中部署你的应用程序。
一旦你创建了账户,请确保你有一个激活的计费账户。即使你不会为此菜谱的使用案例付费,谷歌也要求你激活一个计费账户并将其链接到你的项目。
- 接下来,安装
gcloudCLI。这非常依赖于操作系统,所以请在此处查找适当的说明:cloud.google.com/sdk/docs/install。
gcloud CLI 使用 Python 3.5 到 3.9 版本进行安装。如果你有 Python 3.10 或 3.11 等后续版本,你不必担心,因为 gcloud 将下载它自己的 Python 版本并为自身创建一个虚拟环境。
-
一旦完成
gcloud的安装,运行以下命令以初始化gcloud并进行一些基本设置:$ gcloud init
执行此命令将要求您提供一些详细信息,例如您想要使用哪个 GCP 账户来链接到您的 gcloud CLI。在桌面电脑上,它将打开浏览器并要求您登录您的 GCP 账户。请确保登录的用户有足够的权限部署到 Cloud Run。对于这个食谱,owner 权限应该处理所有事情。
然后,您将需要选择您想要使用的 GCP 项目。
您可以通过再次运行相同的命令来更改此配置。
如何做…
请按照以下步骤操作:
- 在设置和初始化
gcloud之后,下一步是创建并将您的应用程序容器上传到 GCR。您还可以使用其他容器注册表,如 Docker Hub,但这超出了本食谱的范围。
重要
Cloud Run 预期应用程序在端口 8080 上运行。因此,更新您的 run.py 以在 8080 上运行,而不是像我们在本书中迄今为止所做的那样在 8000 或 5000 上运行。
从您的应用程序根目录(您的 Dockerfile 所在的位置)运行以下命令:
$ gcloud builds submit --tag "gcr.io/<Your project
ID>/<Name of your container image>"
之前的命令使用 Dockerfile 创建了一个 Docker 容器,然后将其上传到命令中提供的路径上的 GCR。执行成功后,您将获得类似于以下响应:

图 12.3 – GCR 镜像创建
-
现在,使用您创建的镜像将您的应用程序部署到 Cloud Run。运行以下命令进行相同的操作:
$ gcloud run deploy "your-application-name" –image"gcr.io/<Your project ID>/<Name of your containerimage>" --allow-unauthenticated --platform "managed"
如果尚未设置部署区域,此命令将要求您输入一个区域;然后,它将花费几分钟将应用程序成功部署到 Cloud Run。完成后,它将提供可以访问应用程序的服务 URL。请参阅以下截图:

图 12.4 – Cloud Run 上的部署
它是如何工作的…
要检查应用程序部署是否成功并且按预期运行,请打开之前截图所示的服务 URL。它应该打开应用程序的主页。
参见
要了解更多信息,请查看以下资源:
-
您可以在
cloud.google.com/run/docs/overview/what-is-cloud-run了解更多关于 Cloud Run 的信息。 -
还有其他无服务器工具和平台,如 Serverless、Zappa、AWS Beanstalk、AWS Lambda 等。我敦促您自己探索它们。基本概念保持不变。
使用 GitHub Actions 进行持续部署
持续部署是一种部署策略,它允许在提交代码更改时将软件部署和发布到其相关环境(在大多数情况下是生产环境)。通常,这之前会有自动测试用例;当这些测试通过时,代码会自动部署。
GitHub Actions是由 GitHub 提供的一个持续部署平台,允许您在特定的动作(如代码提交/合并/拉取请求)上触发工作流程。这些工作流程可以用于部署到您选择的云提供商。GitHub Actions 最好的地方是它能够无缝集成到 GitHub 中。
可以使用其他工具执行持续部署,但我会专注于 GitHub Actions,因为它是最容易理解和采用的之一。
准备工作
对于这个配方,我假设您有一个 GitHub 账户并且了解 GitHub 上管理代码和仓库的基本知识。
在这个配方中,我们将基于上一个配方中的应用程序进行构建。
需要执行两个步骤才能为这个配方做好准备:
- 在上一个配方中,Google Cloud 的认证步骤是在
gcloud init期间执行的,当时你被要求登录到 GCP。但是当从 GitHub Actions 部署时,你不会有这种自由;因此,你需要在 Google Cloud 上创建一个服务账户,它有权限在 GCR 上创建容器镜像并将其部署到 Cloud Run。
要创建服务账户,请转到您的 GCP 控制台并打开IAM & Admin。接下来,创建一个服务账户并授予它相关的权限/角色。为了测试这个配方,所有者权限应该足够。一旦创建并配置了服务账户,它应该看起来像这样:
图 12.5 – GCP 上的服务账户
当有机会时,下载服务账户文件(JSON 格式)。该文件无法再次下载。
- 接下来,配置您的 GitHub 仓库,以便它在部署运行时存储将被读取的秘密。通过转到
RUN_PROJECT和RUN_SA_KEY来访问您的 GitHub 仓库。
RUN_PROJECT是您的 GCP 项目 ID,而RUN_SA_KEY是您在上一步中下载的服务账户 JSON 文件的内容:

图 12.6 – GitHub 仓库密钥
如何操作...
现在,在您的应用程序根目录中创建一个名为.github/workflows/main.yml的文件。确保文件路径已正确创建。您需要在其中创建两个文件夹名为.github和workflows,然后在后者中创建main.yml文件。以下是该文件的内容:
name: Build and Deploy to Cloud Run
on:
push:
branches:
- vol-3
env:
PROJECT_ID: ${{ secrets.RUN_PROJECT }}
RUN_REGION: asia-south1
SERVICE_NAME: flask-cookbook-git
jobs:
setup-build-deploy:
name: Setup, Build, and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
# Setup gcloud CLI
- uses: 'google-github-actions/auth@v1'
with:
credentials_json: ${{ secrets.RUN_SA_KEY }}
project_id: ${{ secrets.RUN_PROJECT }}
# Build and push image to Google Container Registry
- name: Build
run: |-
gcloud builds submit \
--quiet \
--tag "gcr.io/$PROJECT_ID/$SERVICE_NAME"
# Deploy image to Cloud Run
- name: Deploy
run: |-
gcloud run deploy "$SERVICE_NAME" \
--quiet \
--region "$RUN_REGION" \
--image "gcr.io/$PROJECT_ID/$SERVICE_NAME" \
--platform "managed" \
--allow-unauthenticated
在这个文件中,首先,我们指定了 GitHub 动作在代码推送时触发的分支。然后,我们有一些环境变量将在文件中的下一步中使用。接着创建了一个名为“设置、构建和部署”的工作,它指定了操作系统,在我们的例子中是 Ubuntu。该工作由四个步骤组成:
-
检出:检出之前指定的 GitHub 分支的代码。
-
gcloud并使用服务账户凭证文件进行认证。 -
gcloud submit。 -
部署镜像到 Cloud Run:将前一步创建的 Docker 镜像部署到 Cloud Run。
现在,只需将您的代码提交并推送到相关分支。
它是如何工作的...
当代码推送到 GitHub 的正确分支时,GitHub 动作就会被触发。一个成功的 GitHub 动作看起来像这样:

图 12.7 – 成功的 GitHub 动作执行
要查看您的应用程序正在运行,请复制服务 URL,如前一个屏幕截图所示,并在您的浏览器中打开它。
小贴士
如果您从本书开始就一直在跟随,那么这个菜谱就是一个思考所有主要部分如何拼凑在一起以完成拼图的好地方。我们学习了如何创建 Flask 应用程序,然后将其复杂性增加,接着是单元测试用例,最后但同样重要的是,部署。
在这个菜谱中,我们自动化了被称为持续部署的代码提交部署。
您还可以修改您的 GitHub 工作流程,在构建镜像之前运行测试用例,并在失败时退出。这被称为持续集成。
参见
您可以在docs.github.com/en/actions/deployment/about-deployments/deploying-with-github-actions了解更多关于 GitHub Actions 的信息。
第十三章:使用 Flask 的 GPT
GPT(今天的最新热门词汇,代表生成预训练转换器),是由 OpenAI 开发的最先进的语言模型。它基于 Transformer 架构,并使用无监督学习来生成自然语言文本。GPT 首次在 2018 年 GPT-1 发布时推出,随后在 2019 年和 2020 年分别推出了 GPT-2 和 GPT-3。
GPT 最著名的应用之一是文本补全,它可以基于给定的提示生成连贯且语法正确的句子。这导致它在各种写作辅助工具中得到应用,例如文本编辑器和消息应用中的自动完成和自动纠错功能。
GPT 的另一个流行应用是在聊天机器人(如 ChatGPT)的开发中。凭借其生成自然语言响应的能力,GPT 可以创建模拟人类对话的聊天机器人,这使得它们在客户服务和其他应用中非常有用。
GPT 还被用于图像生成,它根据文本描述生成图像。这为艺术和设计等创意应用开辟了新的可能性。
信息
在本章中,我们将简要介绍一些主要针对 GPT 的新术语。其中最重要的新术语之一将是 提示。
简而言之,GPT 中的提示是一个起点或部分句子,它被提供给模型。这就像给模型一个建议或提示,以便它可以根据这个提示生成剩余的句子或段落。
例如,如果你想为一家餐厅生成评论,你可以从一个提示开始,比如“食物是...”,然后让 GPT 生成剩余的句子。生成的文本可能类似于“食物美味,香料和风味的平衡恰到好处。份量充足,摆盘美观。”
通过提供提示,你为 GPT 提供了一些上下文,并引导它生成符合该上下文的文本。这在各种自然语言处理任务中非常有用,例如文本补全、摘要等。
GPT 是一种强大的语言模型,已被应用于各种自然语言处理任务,例如文本补全、聊天机器人和图像生成。它生成类似人类文本的能力使其成为开发者的宝贵工具,尤其是对自然语言处理和相关网络应用(如使用 Flask 开发的应用)感兴趣的 Python 社区成员。
在本章中,我们将探讨如何实现 GPT 在我们提到的用例中的应用。GPT 的应用几乎无限,因为它开放于想象力和创造力,但在这章中,我将将其限制于一些适用于网络应用的基本但强大的示例。
在本章中,我们将介绍以下内容:
-
使用 GPT 自动化文本补全
-
使用 GPT 实现聊天(ChatGPT)
-
使用 GPT 生成图像
技术要求
对于本章中的所有食谱,以下步骤是常见且必需的:
-
我们将使用一个名为
openai的库,这是 OpenAI 提供的官方 Python 库,用于与 GPT 一起工作:$ pip install openai -
我们还需要从 OpenAI 网站获取一个 API 密钥,这是使用 GPT 进行任何 API 调用的必要条件。为此,只需在platform.openai.com上创建一个账户,然后导航到设置以创建您的 API 密钥。以下是一个展示相同操作的截图:

图 13.1 – OpenAI 上用于 GPT 的 API 密钥
小贴士
请注意,OpenAI 的 GPT 是一个付费工具,并且截至撰写本书时,每个账户都有一笔 5 美元的小额赠款,为期 3 个月,用于实验和熟悉 API。一旦限额用尽,您将不得不选择付费计划。有关定价的更多信息,请参阅openai.com/pricing。
使用 GPT 自动化文本补全
使用 GPT 进行文本补全涉及向模型提供一个提示或起始句子,然后模型生成一个连贯且相关的后续内容。GPT 在这个领域的功能令人印象深刻,因为它可以以高精度生成复杂且上下文相关的文本。这使得它成为涉及写作的 Web 应用的理想工具,如内容创作、自动纠错和消息传递。通过将这些应用中的 GPT 文本补全能力整合进去,开发者可以通过自动化繁琐或耗时的工作、提高书面内容的质量以及提供更自然和响应式的通信来增强用户体验。
如果我们谈论电子商务网站的环境,最重要的功能之一是有效的搜索。除了有效性之外,如果搜索变得互动且直观,那么它将非常吸引用户。在这个食谱中,我们将使用 GPT 实现文本补全,以在电子商务网站上构建直观且用户友好的搜索查询。
准备工作
请参阅本章开头的技术要求部分,以获取有关设置 GPT 的详细信息。
为了展示这个食谱的完整上下文,我将使用一个名为Awesomplete的 JavaScript 库来在演示搜索字段上实现自动完成功能。前往projects.verou.me/awesomplete/下载静态文件并了解更多关于这个库的信息。
为了演示这个食谱,我们将从第四章中开发的代码库开始。
如何做到这一点...
按照以下步骤执行自动完成功能的设置,然后使用 GPT 进行文本补全:
-
首先,将
my_app/templates/base.html中的静态文件添加到项目中:<!-- Group with other CSS files -—><link href="https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css"rel="stylesheet"><!-- Group with other JS files -—><script src="img/pre>ajax/libs/awesomplete/1.1.5/awesomplete.min.js">
我直接从 CDN 链接到静态文件。你也可以选择将这些文件下载到你的静态文件文件夹中,并从那里引用。
-
接下来,将 OpenAI 提供的 API 密钥添加到你的应用程序配置中的
my_app/__init__.py:app.config['OPENAI_KEY'] = 'Your own API Key' -
接下来,在
my_app/catalog/views.py中创建一个新的方法来处理用户搜索词并将其转换为 GPT 生成的搜索查询:import openai@catalog.route('/product-search-gpt',methods=['GET','POST'])def product_search_gpt():if request.method == 'POST':query = request.form.get('query')openai.api_key = app.config['OPENAI_KEY']prompt = """Context: Ecommerce electronics website\nOperation: Create search queries for a product\nProduct: """ + queryresponse = openai.Completion.create(model="text-davinci-003",prompt=prompt,temperature=0.2,max_tokens=60,top_p=1.0,frequency_penalty=0.5,presence_penalty=0.0)return response['choices'][0]['text'].strip('\n').split('\n')[1:]return render_template('product-search-gpt-demo.html')
在前面的代码中,首先导入openai。然后在catalog蓝图下创建一个新的端点,相对路径为/product-search-gpt。这个端点服务于GET和POST请求。
在GET请求中,它将简单地渲染我们创建的product-search-gpt-demo.html模板,以演示这个食谱。
在POST请求中,它期望有一个名为query的表单字段,然后使用这个字段向openai.Completion模块发出一个相关的 API 请求。仔细查看我指定的Context之后的Operation需要执行的操作。没有定义你可以提供提示的格式;它只需要是 GPT 可以理解和工作的内容。GPT 返回的响应在发送到 JS 库进行解释之前需要一些格式化。
信息
注意我们在对openai.Completion模块发出的 API 请求中提供的多个参数。你可以在platform.openai.com/docs/api-reference/completions/create了解更多关于它们的信息。
-
最后,我们需要创建在最后一步中引用的模板。在
my_app/templates/product-search-gpt-demo.html中创建一个新的模板文件:{% extends 'home.html' %}{% block container %}<div class="top-pad"><formclass="form-horizontal"role="form"><div class="form-group"><label for="name" class="col-sm-2 control-label">Query</label><div class="col-sm-10"><input type="text" class="form-controlawesomplete" id="query" name="query"></div></div></form></div>{% endblock %}{% block scripts %}<script>$(document).ready(function(){const input = document.querySelector('input[name="query"]' );const awesomplete = new Awesomplete( input, {tabSelect: true, minChars: 5 } );function ajaxResults() {$.ajax({url: '{{ url_for("catalog.product_search_gpt") }}',type: 'POST',dataType: "json",data: {query: input.value}}).done(function(data) {awesomplete.list = data;});};input.addEventListener( 'keyup', ajaxResults );});</script>{% endblock %}
在前面的代码中,我创建了一个简单的 HTML 表单,只有一个字段。它的目的是演示电子商务商店上的搜索字段。在这里,你可以输入任何你选择的产品,输入的值将被发送到 GPT 以创建帮助用户进行更精确搜索的搜索查询。
它是如何工作的…
在你的浏览器中打开http://127.0.0.1:5000/product-search-gpt。在查询字段中输入你选择的产品值,看看 GPT 如何提供更有帮助的搜索查询。这将在下面的屏幕截图中进行演示。

图 13.2 – 使用 GPT 进行文本补全
参见
-
在
platform.openai.com/docs/guides/completion/introduction了解使用 GPT 进行文本补全的用法和功能。 -
查看针对文本补全的详细 API 参考
platform.openai.com/docs/api-reference/completions/create
使用 GPT 实现聊天(ChatGPT)
毫无疑问,使用 GPT 进行的聊天,或者更通俗地说,ChatGPT,是 GPT 最广泛的应用。使用 GPT 进行聊天涉及在对话环境中使用模型生成对用户输入的自然语言回应。GPT 在这一领域的功能令人印象深刻,因为它可以生成连贯且与上下文相关的回应,模拟人类对话。这使得它成为涉及聊天机器人、虚拟助手或其他对话界面的网络应用的理想工具。
利用 GPT 生成类似人类回应的能力,使用这项技术开发的聊天机器人可以为用户提供更加个性化和吸引人的体验。通过理解对话的上下文并提供相关的回应,这些聊天机器人可以应用于广泛的场景,例如客户服务、预约安排等。
如果我们谈论电子商务网站或任何网络应用,最近的一个常见功能就是聊天机器人。所有企业都希望与他们的用户保持联系,但同时又可能不想雇佣很多客户支持人员。在这种情况下,ChatGPT 变得非常有帮助。我将通过一些基本示例在这个食谱中演示这一点。
准备工作
请参阅本章开头的技术要求部分,以获取设置 GPT 的详细信息。
我们将在之前的食谱“使用 GPT 进行文本补全”的基础上构建这个食谱。请参考该食谱以获取openai配置设置。
如何实现它...
按照以下步骤在你的 Flask 驱动的网络应用上实现一个基本的聊天机器人使用 ChatGPT:
-
首先,创建一个处理程序来接收用户聊天消息并使用 ChatGPT 对他们进行回应。这应该在
my_app/catalog/views.py中完成,如下所示:@catalog.route('/chat-gpt', methods=['GET', 'POST'])def chat_gpt():if request.method == 'POST':msg = request.form.get('msg')openai.api_key = app.config['OPENAI_KEY']messages = [{"role": "system","content": "You are a helpful chatassistant for a generic electronicsEcommerce website"},{"role": "user", "content": msg}]response = openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=messages)return jsonify(message=response['choices'][0]['message']['content'])return render_template('chatgpt-demo.html')
在前面的代码中,我们在catalog蓝图下创建了一个新的端点,具有相对路径/chat-gpt。此端点服务于GET和POST请求。
在GET请求中,它将简单地渲染我们创建来演示此食谱的ChatGPT-demo.html模板。
在POST请求中,它期望一个名为msg的表单字段,该字段应引用用户在与聊天机器人交谈时输入的消息。然后,使用相关消息集将消息用于向openai.ChatCompletion模块发出 API 请求。
如果你仔细查看ChatCompletion API 提供的messages,你会注意到第一条消息有一个system角色。它实际上是为 ChatGPT 准备上下文,其中它将处理实际用户在msg变量中的消息。
-
接下来,我们需要创建我们在上一步中引用的模板。在
my_app/templates/ChatGPT-demo.html创建一个新的模板文件:{% extends 'home.html' %}{% block container %}<div class="top-pad"><ul class="list-group" id="chat-list"><li class="list-group-item"><span class="badge">GPT</span>How can I help you?</li></ul><div class="input-group"><input type="text" class="form-control"name="message" placeholder="Enter yourmessage" aria-describedby="chat-input"><span class="input-group-btn"><button class="btn btn-success" type="button"data-loading-text="Loading..." id="send-message">Send</button></span></div></div>{% endblock %}{% block scripts %}<script>function appendToChatList(mode, message) {$( "#chat-list" ).append( '<li class="list-group-item"><span class="badge">' + mode +'</span>' + message + '</li>' );}$(document).ready(function(){$('button#send-message').click(function() {var send_btn = $(this).button('loading');const inputChat = document.querySelector( 'input[name="message"]' );var message = inputChat.value;appendToChatList('Human', message);inputChat.value = '';$.ajax({url: '{{ url_for("catalog.chat_gpt") }}',type: 'POST',dataType: "json",data: {msg: message}}).done(function(data) {appendToChatList('GPT', data.message);send_btn.button('reset');});});});</script>{% endblock %}
在前面的代码文件中,我使用一个 JS 列表创建了一个非常简单的聊天机器人。在这里,一个简单的textfield接收用户输入并将其发送到我们在第一步中为 GPT 创建的 API 端点以获得回应。
它是如何工作的...
在您的浏览器中打开http://127.0.0.1:5000/chat-gpt。在消息字段中,输入您想要发送给聊天机器人的消息,它将在电子商务网站的上下文中给出相关响应。以下截图为演示:

图 13.3 – 使用 GPT 的聊天助手/机器人
参考以下内容
-
在
platform.openai.com/docs/guides/chat阅读有关 ChatGPT 的用法和功能 -
查看针对 ChatGPT 的详细 API 参考,请访问
platform.openai.com/docs/api-reference/chat
使用 GPT 生成图片
使用 GPT 进行图像生成涉及使用模型根据文本描述生成图像。GPT 在这个领域的功能已经显示出有希望的结果,尽管图像生成不是其主要功能。通过提供文本描述,GPT 可以生成尝试匹配给定描述的图像。
虽然生成的图片质量可能不如专门的图像生成模型,但 GPT 生成视觉表示的能力为网络开发中的创意应用开辟了新的可能性。可能的应用包括生成占位符图片、根据用户输入创建视觉表示,甚至通过提供基于文本描述的视觉建议来协助设计过程。然而,需要注意的是,对于高级图像生成任务,通常更倾向于使用如 GANs 或 VAEs 等专门的图像生成模型。
在这个菜谱中,我们将使用 GPT 为电子商务商店的产品列表生成图片。只要有足够清晰的提示,GPT 应该生成符合我们需求的定制图片。
准备工作
参考本章开头的技术要求部分,了解设置 GPT 的详细信息。
为了演示这个菜谱,我们将从第五章中开发的代码库开始。
参考本章的第一个菜谱,使用 GPT 进行文本补全,以获取openai配置设置。
我们还将使用requests库来下载图片。它可以通过pip简单安装:
$ pip install requests
如何操作...
在创建产品时自动生成图片并使用同一图片在产品查看页面上,请按照以下步骤操作:
-
第一个变化是非常微不足道的。在这个菜谱中,我们创建一个产品,而不上传产品图片,因为图片将使用 GPT 生成。因此,在产品创建表单中的
image字段的需求变得过时。相应地,应在my_app/catalog/models.py中创建以下新表单:class ProductGPTForm(NameForm):price = DecimalField('Price', validators=[InputRequired(),NumberRange(min=Decimal('0.0'))])category = CategoryField('Category', validators=[InputRequired()], coerce=int)
在前面的代码中,创建了一个名为 ProductGPTForm 的新表单,仅包含 price 和 category 字段。name 字段将由 NameForm 提供,这是新创建的表单所继承的。
-
接下来,需要在
my_app/catalog/views.py文件中创建一个新的商品创建处理程序和端点,该处理程序将使用 GPT 在my_app/catalog/views.py文件中生成图像:import openaiimport requestsfrom my_app.catalog.models import ProductGPTForm@catalog.route('/product-create-gpt',methods=['GET','POST'])def create_product_gpt():form = ProductGPTForm()if form.validate_on_submit():name = form.name.dataprice = form.price.datacategory = Category.query.get_or_404(form.category.data)openai.api_key = app.config['OPENAI_KEY']prompt = "Generate an image for a " + name + \" on a white background for a classye-commerce store listing"response = openai.Image.create(prompt=prompt,n=1,size="512x512")image_url = response['data'][0]['url']filename = secure_filename(name + '.png')response = requests.get(image_url)open(os.path.join(app.config['UPLOAD_FOLDER'], filename), "wb").write(response.content)product = Product(name, price, category,filename)db.session.add(product)db.session.commit()flash('The product %s has been created' %name, 'success')return redirect(url_for('catalog.product',id=product.id))if form.errors:flash(form.errors, 'danger')return render_template('product-create-gpt.html',form=form)
在前面的代码片段中,对于 GET 请求,渲染了 product-create-gpt.html 模板,这是一个新创建的模板。
在 POST 请求的情况下,一旦表单经过验证,就会捕获 name、price 和 category 字段的相关数据。然后,使用 openai.Image 模块的 create 方法向 GPT 发送请求,使用给定的 prompt 生成图像。注意 create() 方法提供的其他参数 – 即 n 和 size,分别代表要生成的图像数量和像素大小。从 create() 的响应中捕获 image_url,然后使用 requests.get() 下载图像。下载的图像内容随后保存到在应用程序初始化期间配置的 UPLOAD_FOLDER。然后,继续进行商品创建,正如在 第五章 中讨论的那样。
-
最后,我们需要创建在最后一步中引用的模板。在
my_app/templates/product-create-gpt.html创建一个新的模板文件:{% extends 'home.html' %}{% block container %}<div class="top-pad"><form method="POST"action="{{url_for('catalog.create_product_gpt') }}"role="form"enctype="multipart/form-data">{{ form.csrf_token }}<div class="form-group">{{ form.name.label }}:{{ form.name() }}</div><div class="form-group">{{ form.price.label }}:{{ form.price() }}</div><div class="form-group">{{ form.category.label}}: {{ form.category() }}</div><button type="submit" class="btn btn-default">Submit</button></form></div>{% endblock %}
前面的代码片段是一个简单的 HTML 表单,在发起商品创建的 POST 请求之前,它接受商品名称、价格和类别作为输入。
工作原理…
首先,运行你的应用程序,并使用以下 URL 创建一些类别:http://127.0.0.1:5000/category-create。然后,转到 http://127.0.0.1:5000/product-create-gpt 使用前面在本食谱中描述的 GPT 图像生成逻辑创建一个新的商品。屏幕应该看起来像以下截图:

图 13.4 – 不包含图片字段的商品创建表单
填写详细信息后,提交表单,并查看根据提供的商品名称自动使用 GPT 生成的图像。查看以下截图:

图 13.5 – 使用 GPT 生成的图像的新创建商品
重要
前面的例子只是演示了如何使用 GPT 进行图像生成。由于图像版权问题,生成的图像可能不会完全准确或使用确切的商品标志。您可以根据自己的用例在定义 prompt 时更加富有创意。
参考信息
-
了解使用 GPT 进行图像生成的用法和功能,请参阅
platform.openai.com/docs/guides/images/introduction -
查看针对 GPT 图像生成的详细 API 参考,请访问
platform.openai.com/docs/api-reference/images -
参考第第五章以获取更多关于产品创建 API 和表单的详细信息及背景
第十四章:额外的技巧和窍门
本书几乎涵盖了使用 Flask 创建 Web 应用程序所需了解的所有领域。已经涵盖了大量的内容,还有更多需要探索。在本章的最后,我们将介绍一些额外的食谱,这些食谱可以在需要时用于为基于 Flask 的 Web 应用程序增加价值。
我们将学习如何使用 Elasticsearch 实现全文搜索。对于提供大量内容和选项的 Web 应用程序,如电子商务网站,全文搜索变得非常重要。接下来,我们将了解帮助通过发送通知(信号)来解耦应用程序的信号。当在应用程序的某个地方执行操作时,这个信号会被一个订阅者/接收者捕获,并相应地执行操作。这之后,我们将实现为我们的 Flask 应用程序添加缓存。
我们还将了解如何将电子邮件支持添加到我们的应用程序中,以及如何通过执行不同的操作直接从应用程序发送电子邮件。然后我们将看到如何使我们的应用程序异步。默认情况下,WSGI 应用程序是同步和阻塞的——也就是说,默认情况下,它们不会同时服务多个请求。我们将通过一个小示例来了解如何处理这个问题。我们还将集成 Celery 到我们的应用程序中,并了解如何利用任务队列来为我们的应用程序带来好处。
在本章中,我们将涵盖以下食谱:
-
使用 Elasticsearch 实现全文搜索
-
与信号一起工作
-
在您的应用程序中使用缓存
-
实现电子邮件支持
-
理解异步操作
-
与 Celery 一起工作
使用 Elasticsearch 实现全文搜索
全文搜索是几乎所有通过 Web 应用程序提供的用例的一个基本部分。如果你打算构建一个电子商务平台或类似的东西,其中搜索扮演着核心角色,那么它就变得更加关键。全文搜索意味着在大量文本数据中搜索某些文本的能力,搜索结果可以包含根据配置的全匹配或部分匹配。
Elasticsearch是一个基于 Lucene 的搜索服务器,Lucene 是一个开源的信息检索库。Elasticsearch 提供了一个具有 RESTful 网络接口和无模式 JSON 文档的分布式全文搜索引擎。在本食谱中,我们将使用 Elasticsearch 为我们的 Flask 应用程序实现全文搜索。
准备工作
我们将使用一个名为elasticsearch的 Python 库,它使得处理 Elasticsearch 变得更加容易:
$ pip install elasticsearch
我们还需要安装 Elasticsearch 服务器本身。这可以从www.elastic.co/downloads/elasticsearch下载。在您的机器上选择任何位置解压该包,并运行以下命令:
$ bin/elasticsearch
这将默认在http://localhost:9200/上启动 Elasticsearch 服务器。在继续之前,有几个细节需要注意:
- 当您使用前面的命令运行
elasticsearch服务器时,您将看到以下截图所示的一些详细信息:

图 14.1 – Elasticsearch 安全细节
在此处记录弹性用户的密码。您也可以选择使用前一个截图指定的命令重置密码。
- 当您运行
elasticsearch服务器时,它会生成一个 HTTP CA 证书,在通过我们的 Flask 应用程序建立连接时需要使用此证书。您可以在elasticsearch服务器文件夹的config/certs文件夹中找到此证书文件。在大多数情况下,它应该是<path to your>elasticsearch folder>/config/certs/http_ca.crt。
如何操作...
按照以下步骤执行 Elasticsearch 和我们的 Flask 应用程序之间的集成:
-
首先,将
elasticsearch对象添加到应用程序的配置中——即my_app/__init__.py:from elasticsearch import Elasticsearches = Elasticsearch('https://192.168.1.6:9200/',ca_certs='Users/apple/workspace/elasticsearch-8.6.2/config/certs/http_ca.crt',verify_certs=False,basic_auth=("elastic", '8oJ7C3U8ipo0PE+-n1Ff'))es.indices.create(index='catalog', ignore=400)
小贴士
您会注意到前一段代码中使用了几个配置设置。我直接在实例化es对象时使用了它们,以便您更容易理解。在实际应用程序中,这些应该来自配置设置或配置文件。
在这里,我们已从Elasticsearch类创建了一个es对象,该对象接受服务器 URL、HTTP CA 证书以及使用用户名和密码的基本身份验证。HTTP CA 证书和密码在准备就绪部分的步骤中获取。verify_certs=False是必需的,因为我的应用程序正在运行在 HTTP 上,而 Elasticsearch 运行在 HTTPS 上。如果您的应用程序也运行在 HTTPS 上,则此标志将不需要。ignore=400将忽略与resource_already_exists_exception相关的任何错误,因为此索引已经创建。
-
接下来,我们需要向我们的 Elasticsearch 索引添加一个文档。这可以在视图或模型中完成;然而,在我看来,最好的方法是在模型层添加它,因为它与数据更紧密相关,而不是如何显示。我们将在
my_app/catalog/models.py文件中完成此操作:from my_app import esclass Product(db.Model):def add_index_to_es(self):es.index(index='catalog', document={'name': self.name,'category': self.category.name}, id=self.id)es.indices.refresh(index='catalog')class Category(db.Model):def add_index_to_es(self):es.index('catalog', document={'name': self.name,}, id=self.id)es.indices.refresh(index='catalog')
在这里,我们在每个模型中添加了一个名为add_index_to_es()的新方法,该方法将对应于当前Product或Category对象的文档添加到catalog索引。您可能希望将不同类型的数据分别索引以提高搜索的准确性。最后,我们刷新了索引,以便新创建的索引可用于搜索。
当我们创建、更新或删除产品或类别时,可以调用add_index_to_es()方法。
-
接下来,为了演示,只需在创建产品时将索引文档(产品)的语句添加到
my_app/catalog/views.py中的elasticsearch索引:from my_app import es@catalog.route('/product-create', methods=['GET','POST'])def create_product():#... normal product creation logic … #db.session.commit()product.add_index_to_es()#... normal post product creation logic … #@catalog.route('/product-search-es')@catalog.route('/product-search-es/<int:page>')def product_search_es(page=1):q = request.args.get('q')products = es.search(index="catalog", query={"query_string": {"query": '*' + q + '*'}})return products['hits']
在前面的代码中,我们还添加了一个product_search_es()方法,以便在刚刚创建的 Elasticsearch 索引上进行搜索。同样,在create_category()方法中也进行相同的操作。
小贴士
在前面的代码中,我们发送给 Elasticsearch 的搜索查询相当基础且开放。我强烈建议你阅读有关 Elasticsearch 查询构建的内容,并将其应用于你的程序。请参考以下内容:www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html。
它是如何工作的…
现在,假设我们在每个类别中创建了一些类别和产品。如果我们打开http://127.0.0.1:5000/product-search-es?q=phone,我们将得到以下类似的响应:
{
"hits": [
{
"_id": "5",
"_index": "catalog",
"_score": 1.0,
"_source": {
"category": "Phones",
"name": "iPhone 14"
}
},
{
"_id": "6",
"_index": "catalog",
"_score": 1.0,
"_source": {
"category": "Phones",
"name": "Motorola razr"
}
}
],
"max_score": 1.0,
"total": {
"relation": "eq",
"value": 2
}
}
我鼓励你尝试增强输出格式和显示。
参见
-
你可以在
www.elastic.co/guide/en/elasticsearch/client/python-api/current/connecting.html了解更多关于连接到 elasticsearch 服务器的信息。 -
更多关于 Python Elasticsearch 客户端的信息,请参阅
elasticsearch-py.readthedocs.io/en/v8.6.2/index.html
与信号一起工作
信号可以被视为在我们应用程序中发生的事件。这些事件可以被某些接收者订阅,当事件发生时,接收者将调用一个函数。事件的产生由发送者广播,发送者可以指定函数可以使用的参数,该函数将由接收者触发。
重要
你应该避免在信号中修改任何应用程序数据,因为信号不是按照指定的顺序执行的,很容易导致数据损坏。
准备工作
我们将使用一个名为blinker的 Python 库,它提供了信号功能。Flask 内置了对blinker的支持,并且大量使用了信号。Flask 提供了一些核心信号。
在这个菜谱中,我们将使用来自使用 Elasticsearch 实现全文搜索菜谱的应用程序,并添加product和category文档,通过信号使索引工作。
如何做到这一点…
按照以下步骤实现并理解信号的工作原理:
-
首先,为
product和category创建信号。这可以在my_app/catalog/models.py中完成。然而,你可以使用任何你想要的文件,因为信号是在全局范围内创建的:from blinker import Namespacecatalog_signals = Namespace()product_created = catalog_signals.signal('product-created')category_created = catalog_signals.signal('category-created')
我们使用了Namespace来创建信号,这将它们创建在自定义命名空间中,而不是全局命名空间中,从而有助于信号的整洁管理。我们创建了两个信号,product-created和category-created,它们的意图通过它们的名称都很清楚。
-
然后,我们将创建对这些信号的订阅者,并将函数附加到它们。为此,必须移除(如果您是在上一个配方的基础上构建)
add_index_to_es()方法,并在my_app/catalog/models.py的全局范围内创建新函数:def add_product_index_to_es(sender, product):es.index(index='catalog', document={'name': product.name,'category': product.category.name}, id=product.id)es.indices.refresh(index='catalog')product_created.connect(add_product_index_to_es, app)def add_category_index_to_es(sender, category):es.index(index='catalog', document={'name': category.name,}, id=category.id)es.indices.refresh('catalog')category_created.connect(add_category_index_to_es,app)
在前面的代码片段中,我们使用.connect()创建了在步骤 1中创建的信号的订阅者。此方法接受在事件发生时应调用的函数;它还接受发送者作为可选参数。app对象作为发送者提供,因为我们不希望我们的函数在应用程序的任何地方触发事件时被调用。这在扩展的情况下尤其正确,因为它们可以被多个应用程序使用。接收者调用的函数(在这种情况下,add_product_index_to_es和add_category_index_to_es)将发送者作为第一个参数,如果未提供发送者,则默认为None。我们提供了产品/类别的第二个参数,需要将其记录添加到elasticsearch索引中。
-
现在,发出可以被接收者捕获的信号。这需要在
my_app/catalog/views.py中完成。为此,只需移除对add_index_to_es()方法的调用,并用.send()方法替换它们:From my_app.catalog.models import product_created,category_created@catalog.route('/product-create', methods=['GET','POST'])def create_product():#... normal product creation logic … #db.session.commit()product_created.send(app, product=product)#... normal post product creation logic … #
同样在create_category()方法中执行相同的操作。
它是如何工作的...
每当创建产品时,都会发出product_created信号,其中app对象作为发送者,product作为关键字参数。然后,在models.py中捕获此信号,并调用add_product_index_to_es()函数,将文档添加到目录索引中。
此配方的功能与上一个配方使用 Elasticsearch 实现全文搜索完全相同。
参考信息
-
阅读关于使用 Elasticsearch 实现全文搜索的配方,以了解此配方的背景信息
-
您可以在
pypi.python.org/pypi/blinker上了解blinker库 -
您可以在
flask.palletsprojects.com/en/2.2.x/api/#core-signals-list查看 Flask 支持的核心理信号列表 -
您可以在
flask-sqlalchemy.palletsprojects.com/en/3.0.x/api/#module-flask_sqlalchemy.track_modifications查看 Flask-SQLAlchemy 提供的信号,用于跟踪模型修改
在您的应用程序中使用缓存
当扩展或增加应用程序的响应时间成为一个问题时,缓存成为任何 Web 应用程序的一个重要且不可或缺的部分。在这些情况下,缓存是首先实施的事情。Flask 本身默认不提供任何缓存支持,但Werkzeug提供了。Werkzeug 有一些基本支持,可以使用多个后端进行缓存,例如 Memcached 和 Redis。Werkzeug 的这种缓存支持是通过一个名为Flask-Caching的包实现的,我们将在这个菜谱中使用它。
准备工作
我们将安装一个名为flask-caching的 Flask 扩展,它简化了缓存过程:
$ pip install flask-caching
我们将使用我们的目录应用程序来完成这个目的,并为某些方法实现缓存。
如何做到这一点…
实现基本的缓存相当简单。按照以下步骤进行操作:
-
首先,初始化
Cache以与我们的应用程序一起工作。这是在应用程序的配置中完成的——即my_app/__init__.py:from flask_caching import Cachecache = Cache(app, config={'CACHE_TYPE': 'simple'})
在这里,我们使用了simple作为Cache类型,其中缓存存储在内存中。这不建议在生产环境中使用。对于生产环境,我们应该使用 Redis、Memcached、文件系统缓存等类似的东西。Flask-Caching 支持所有这些以及一些额外的后端。
-
接下来,将缓存添加到需要缓存的那些方法中。只需在视图方法中添加一个
@cache.cached(timeout=<time in seconds>)装饰器。一个简单的目标可以是分类列表(我们将在my_app/catalog/views.py中这样做):@catalog.route('/categories')@cache.cached(timeout=120)def categories():categories = Category.query.all()return render_template('categories.html',categories=categories)
这种缓存方式将此方法的输出值以键值对的形式存储在缓存中,键为请求路径。
它是如何工作的…
在添加前面的代码后,为了检查缓存是否按预期工作,请将浏览器指向http://127.0.0.1:5000/categories来获取分类列表。这将在这个 URL 的缓存中保存一个键值对。现在,快速创建一个新的分类并导航到相同的分类列表页面。你会注意到新添加的分类没有被列出。等待几分钟然后重新加载页面。现在新添加的分类将显示出来。这是因为分类列表第一次被缓存后,2 分钟(120 秒)后过期。
这可能看起来像是应用程序的缺陷,但在大型应用程序的情况下,这成为了一种福音,因为减少了数据库的访问次数,整体应用程序体验得到了改善。缓存通常用于那些结果不经常更新的处理程序。
还有更多…
我们中的许多人可能会认为,在单个类别或产品页面的情况下,每个记录都有单独的页面,这种缓存可能会失败。解决这个问题的方法是记忆化。它与缓存类似,区别在于它将方法的结果存储在缓存中,同时存储传递给参数的信息。因此,当多次使用相同参数创建方法时,结果将从缓存中加载,而不是进行数据库调用。实现记忆化相当简单:
@catalog.route('/product/<id>')
@cache.memoize(120)
def product(id):
product = Product.query.get_or_404(id)
return render_template('product.html', product=product)
现在,如果我们第一次在我们的浏览器中打开一个 URL(例如,http://127.0.0.1:5000/product/1),它将在调用数据库后加载。然而,如果我们再次进行相同的调用,页面将从缓存中加载。另一方面,如果我们打开另一个产品(例如,http://127.0.0.1:5000/product/2),那么它将在第一次访问时从数据库中获取产品详情后加载。
参见
-
Flask-Caching 在
flask-caching.readthedocs.io/en/latest/
实现电子邮件支持
发送电子邮件的能力通常是任何网络应用最基本的功能之一。通常,使用任何应用实现它都很简单。对于基于 Python 的应用,借助smtplib实现起来相当简单。在 Flask 的情况下,这通过一个名为Flask-Mail的扩展进一步简化。
准备工作
Flask-Mail可以通过pip轻松安装:
$ pip install Flask-Mail
让我们看看一个简单的例子,当在应用程序中添加新类别时,将向目录管理员发送电子邮件。
如何操作…
首先,在我们的应用程序配置中实例化Mail对象——即my_app/__init__.py文件:
from flask_mail import Mail
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = 'gmail_username'
app.config['MAIL_PASSWORD'] = 'gmail_password'
app.config['MAIL_DEFAULT_SENDER'] = ('Sender name', 'sender
email')
mail = Mail(app)
我们还需要进行一些配置,以设置电子邮件服务器和发送者账户。前面的代码是 Gmail 账户(未启用双因素认证)的示例配置。任何 SMTP 服务器都可以这样设置。还有其他几个选项可供选择;它们可以在Flask-Mail文档中找到,网址为pythonhosted.org/Flask-Mail。
要在创建类别时发送电子邮件,我们需要在my_app/catalog/views.py文件中进行以下更改:
from my_app import mail
from flask_mail import Message
@catalog.route('/category-create', methods=['POST',])
def create_category():
# ... Create a category ... #
message = Message(
"New category added",
recipients=['shalabh7777@gmail.com']
)
message.body = 'New category "%s" has been created'
% category.name
message.html = render_template(
"category-create-email-html.html",
category=category
)
mail.send(message)
# ... Rest of the process ... #
在这里,将向默认发送者配置中的收件人列表发送一封新电子邮件。您会注意到,创建类别需要一秒或两秒的时间来执行。这就是发送电子邮件所需的时间。
它是如何工作的…
通过向http://127.0.0.1:5000/category-create发送一个 POST 请求来创建一个新的类别。您可以使用Python提示中的requests库来完成此操作:
>>> requests.post('http://127.0.0.1:5000/category-create', data={'name': 'Headphone'})
您应该在提供的收件人电子邮件 ID(s)上收到电子邮件。
还有更多…
现在,让我们假设我们需要发送一个包含大量 HTML 内容的电子邮件。将所有这些内容都写在我们的 Python 文件中会使整体代码变得丑陋且难以管理。一个简单的解决方案是在发送电子邮件时创建模板并渲染其内容。在这里,我创建了两个模板:一个用于 HTML 内容,另一个仅用于文本内容。
category-create-email-text.html模板将看起来像这样:
A new category has been added to the catalog.
The name of the category is {{ category.name }}.
Click on the URL below to access the same:
{{ url_for('catalog.category', id=category.id, _external =
True) }}
This is an automated email. Do not reply to it.
category-create-email-html.html模板将看起来像这样:
<p>A new category has been added to the catalog.</p>
<p>The name of the category is <a href="{{
url_for('catalog.category', id=category.id, _external =
True) }}">
<h2>{{ category.name }}</h2>
</a>.
</p>
<p>This is an automated email. Do not reply to it.</p>
在此之后,我们需要修改我们之前创建的电子邮件消息创建过程:
message.body = render_template(
"category-create-email-text.html",
category=category
)
message.html = render_template(
"category-create-email-html.html",
category=category
)
参见
下一个配方,理解异步操作,将向我们展示我们如何可以将耗时的电子邮件发送过程委托给异步线程,并加快我们的应用程序体验。
理解异步操作
网络应用程序中的某些操作可能很耗时,即使实际上并不慢,也会使用户的整体应用程序感觉缓慢。这会显著阻碍用户体验。为了处理这个问题,实现操作异步执行的最简单方法就是使用线程。在这个配方中,我们将使用 Python 的threading库来实现这一点。在 Python 3 中,thread包已被弃用。尽管它仍然作为_thread可用,但强烈建议使用threading。
准备工作
我们将使用实现 Flask 应用程序的电子邮件支持配方中的应用程序。我们中的许多人都会注意到,当电子邮件正在发送时,应用程序会等待整个过程的完成,这是不必要的。电子邮件发送可以轻松地在后台完成,我们的应用程序可以立即对用户可用。
如何做…
使用threading包进行异步执行非常简单。只需将以下代码添加到my_app/catalog/views.py中:
from threading import Thread
def send_mail(message):
with app.app_context():
mail.send(message)
# Replace the line below in create_category()
# mail.send(message)
# by
t = Thread(target=send_mail, args=(message,))
t.start()
如你所见,电子邮件的发送是在一个新的线程中进行的,该线程将消息作为参数传递给新创建的方法。我们需要创建一个新的send_mail()方法,因为我们的电子邮件模板包含url_for,它只能在应用程序上下文中执行;默认情况下,它在新创建的线程中不可用。这提供了灵活性,可以在需要时启动线程,而不是同时创建和启动线程。
它是如何工作的…
观察它是如何工作的非常简单。比较在这个配方中发送电子邮件的性能与上一个配方中应用程序的性能,实现 Flask 应用程序的电子邮件支持。你会注意到应用程序的响应性更好。另一种方法是监控调试日志,其中新创建的分类页面将在发送电子邮件之前加载。
参见
-
由于我提到了多线程和异步操作,你们中的许多人可能正在思考 Python 内置的
asyncio库及其潜在的应用。尽管可以使用async..await编写 Flask 方法,并且它们将以非阻塞的方式工作,但由于 WSGI 仍然需要在单个工作进程上运行以处理请求,因此并没有明显的性能提升。更多详情请见flask.palletsprojects.com/en/2.2.x/async-await/。 -
你可以查看 Flask 的
asyncio实现或与 Flask 语法非常相似的asyncio。
与 Celery 一起工作
Celery 是 Python 的任务队列。曾经有一个扩展用于集成 Flask 和 Celery,但自从 Celery 3.0 以来,它已经过时。现在,只需进行一些配置,就可以直接使用 Celery 与 Flask 一起使用。在理解异步操作菜谱中,我们实现了异步处理来发送电子邮件。在这个菜谱中,我们将使用 Celery 实现相同的功能。
准备中
Celery 可以从 PyPI 简单安装:
$ pip install celery
要使 Celery 与 Flask 一起工作,我们需要修改我们的 Flask 应用程序配置文件。为了完成其工作,Celery 需要一个代理来接收和交付任务。在这里,我们将使用 Redis 作为代理(感谢其简单性)。
信息
确保你运行 Redis 服务器以建立连接。要安装和运行 Redis 服务器,请参阅redis.io/docs/getting-started/。
你还需要在你的虚拟环境中安装 Redis 客户端:
$ pip install Redis
我们将使用之前菜谱中的应用程序,并以相同的方式实现 Celery。
如何做到这一点...
按照以下步骤了解 Celery 与 Flask 应用程序的集成:
-
首先,我们需要在应用程序的配置中进行一些配置——即
my_app/__init__.py:from celery import Celeryapp.config['SERVER_NAME'] = '127.0.0.1:5000'app.config.update(CELERY_BROKER_URL='redis://127.0.0.1:6379',CELERY_RESULT_BACKEND='redis://127.0.0.1:6379')def make_celery(app):celery = Celery(app.import_name,broker=app.config['CELERY_BROKER_URL'])celery.conf.update(app.config)TaskBase = celery.Taskclass ContextTask(TaskBase):abstract = Truedef __call__(self, *args, **kwargs):with app.app_context():return TaskBase.__call__(self, *args,**kwargs)celery.Task = ContextTaskreturn celerycelery = make_celery(app)
上述代码片段直接来自 Flask 网站,在大多数情况下可以直接用于你的应用程序。在这里,我们实际上是在配置 Celery 任务以拥有应用程序上下文。
-
要运行 Celery 进程,执行以下命令:
$ celery --app=my_app.celery worker -l INFO
在这里,-app指向配置文件中创建的celery对象,而-l是我们想要观察的日志级别。
重要
确保 Redis 也在配置中指定的代理 URL 上运行。
-
现在,在
my_app/catalog/views.py文件中使用这个celery对象来异步发送电子邮件:from my_app import db, app, es, cache, mail, celery@celery.task()def send_mail(category_id, category_name):with app.app_context():category = Category(category_name)category.id = category_idmessage = Message("New category added",recipients=['some-receiver@domain.com'])message.body = render_template("category-create-email-text.html",category=category)message.html = render_template("category-create-email-html.html",category=category)mail.send(message)# Add this line wherever the email needs to be sentsend_mail.apply_async(args=[category.id,category.name])
我们将@celery.task装饰器添加到任何我们希望用作 Celery 任务的方法上。Celery 进程将自动检测这些方法。
它是如何工作的...
现在,当我们创建一个类别并发送电子邮件时,我们可以在 Celery 进程日志中看到正在运行的任务,它看起来像这样:
[2023-03-22 15:24:21,838: INFO/MainProcess] Task my_app.catalog.views.send_mail[1e869100-5bee-4d99-a4cc-6a3dca92e120] received
[2023-03-22 15:24:25,927: INFO/ForkPoolWorker-8] Task my_app.catalog.views.send_mail[1e869100-5bee-4d99-a4cc-6a3dca92e120] succeeded in 4.086294061969966s: None
参见
-
阅读理解异步操作*菜谱,了解线程如何用于各种目的——在我们的案例中,用于发送电子邮件
-
您可以在
docs.celeryproject.org/en/latest/index.html了解更多关于 Celery 的信息。


浙公网安备 33010602011771号