标签归档:Flask Web开发实战

开始写作《Flask Web 开发实战》第二版

Flask Web 开发实战》第二版已经开始写作,计划在今年完成这本书。那本《Python Web API 设计与开发》一直拖着没写完,让我感觉很焦虑。因为拖得太久,HelloFlask 群聊里已经开始谣传我转行送快递了 :D。

所以我想先把《Flask Web 开发实战》新版完成,这样这一年我至少完成了三个年度目标中的一个。「优化」是我擅长和喜欢做的事情,除了要用 Word 写之外,我很乐意给这本书写新版。

内容变动

在各处收集了很多读者反馈和建议,综合我自己的规划,第二版主要有这些大的变动:

  • 不再兼容 Python 2.7,基于 Python 3.x、Flask 2.x。
  • 把大部分知识点集中到第一部分,让第二部分的示例程序内容更精简。
  • 第 1 章使用 pip+venv(+pip-tools) 或 PDM 替代 Pipenv,增加更多对 PyCharm 的介绍。
  • 第 6 章使用 Flask-Mailman 替代 Flask-Mail(同时引入介绍一些新的扩展,比如 Flask-Admin)。
  • 删除第 9、10、11 章(删除后 3 个实战项目),仅保留 Sayhello 和 Bluelog,删减项目的部分功能会合并到这两个项目。
  • 第 10 章删除实战项目,只保留 Web API 开发部分,会新引入 marshmallow 和 APIFlask。
  • 第 12 章添加异步任务相关内容。
  • 第 14 章添加 Docker 相关内容。
  • 删除第 15 章(Flask 扩展开发)。
  • 删掉一些在国内访问有问题的服务介绍,比如 SendGrid(第 6 章)、Heroku(第 14 章)、Twitter 第三方登录(第 11 章)。

大部分内容变动是在做删减,第一版 704 页,第二版内容会删减到 450 页左右。

代码片段

第一部分每一章都有一个示例程序,读者需要切换到每一个子文件夹运行,如果同时自己编写练习代码的话,很容易产生混乱。所以新版的考虑是,第一部分的示例程序不再给出可运行的示例文件(至少不在书里介绍),而是把所有的代码片段放到 HelloFlask 文档,以代码片段的形式组织。读者需要的话可以自己复制代码到本地运行。作为示例,第一章的代码片段可以在这里看到:https://docs.helloflask.com/book/4/snippets/c1/。因为前六章的代码片段变化不大,所以第一版的读者也可以使用这些文档。

P.S. HelloFlask 文档(https://docs.helloflask.com)是我最近刚创建的一个文档集合。所有和书相关的内容都会放到这里,比如勘误、代码片段、衍生文章等。后续也会在这里写一些扩展快速入门介绍、Flask 基础知识等 Flask 相关的东西。

书名

第二版因为有独立的书号,所以可以改书名。责编老师认为原书名就很好,换书名会浪费掉第一本书建立的品牌。我也基本同意这个观点。目前的考虑是主书名不变,出于营销的考虑,副书名或许可以加一个「Python」关键字,比如改成「Python Web 开发入门、进阶与原理解析」。

封面

目前这本书主页上的封面不是正式封面(我用画图软件在初版封面上随手涂了两笔 >_<)。目前的想法是把封面标题颜色换成深红色,然后换一个封面图案,不过暂时还没想好换成什么。

措辞

第一版是用 Word 写的,排版很痛苦,毕竟要在 Word 里面排版代码。第二版也要继续用 Word 写,而且更加痛苦,因为第二版的书稿是编辑老师用工具从 PDF 文件转制成 Word 的,大部分格式都变得很混乱。希望这是我最后一次用 Word 来写技术书。

对于第一版,我交的终稿和最终成书有一些内容的变化,编辑老师会修正错别字和一些病句。但是后面也发现有一些句子修改得并不合理,导致句子原意产生了变化。这次我会在出版前过一遍校正后的书稿,确保不会再出现这样的情况。

另外,编辑老师还会替换一些词汇用法,比如把「我」替换为「笔者」,这个笔者勉强能接受,但是笔者最不能接受的就是要把所有的「点击」改成「单击」。单击一个链接?这太奇怪了。今年和编辑老师再次沟通了这件事,事情终于有了变化,书中的「点击」在这一版不用再死板的按照出版社的规定修改为「单击」。新的读者不会被指导在网页上单击一个链接了。

周边开源项目

我的很多开源项目的开发都是写作驱动的,上一本书驱动我开发了那几个 Flask 示例程序和扩展,这一次同样会有一些开发计划:

  • APIFlask:补齐所有文档,发布 1.0 版本。
  • Bootstrap-Flask:添加 Bootstrap 5 支持。
  • Flask-CKEditor:添加 CKEditor 5 支持,同时集成新版本内置的 Markdown 支持。
  • Flask-WTF:添加多文件上传验证支持。
  • Sayhello:添加分页、多语言、Markdown 支持等功能(总之就是尽可能把删减的示例程序里比较有意思的功能合并到这里)。
  • Bluelog:把 Albumy 的除了图片相关的功能合并过来,会换一个名字(暂定 WeBlog),作为一个社交博客。

如果你对这本书的新版有其他建议和想法,欢迎提出。这两本新书相关的动态会发到 Twitter @helloflask 和我的公众号,欢迎关注。

一个困扰我两年的 Flask「Bug」

TL;DR 版本:

如果你的程序存储在单脚本里,比如 app.py,那么 .flaskenv 和 .env 应该放在程序脚本的同级目录:

myproject/
    - app.py
    - templates
    - static
    - .flaskenv
    - .env

如果你的程序存储在程序包里,那么 .flaskenv 和 .env 应该放在程序包的同级目录:

myproject/
    - app/
        - templates
        - static
        - __init__.py
        - views.py
    - .flaskenv
    - .env

如果你把 .flaskenv 和 .env 放到了再往上一层(myproject 再往上),或是把你的程序包或程序脚本放到了一个子目录(比如 /myproject/myapp),那么执行 flask run 就会出错。


严格来说算不上 bug,而是一个很容易导致出错的设计。具体行为是,如果你安装了 python-dotenv,同时在 Flask 程序的上层目录创建了 .env 或 .flaskenv 文件,那么你将没法成功执行 flask run 等命令,因为这会导致 Flask 没法正确找到对应的 Flask 程序实例。

这个问题从 Flask 开始引入 CLI 机制开始就存在了,困扰了我两年。18 年偶然在用户根目录创建了一个 .env 文件,发现 Flask 程序没法运行了,当时遇到的各种 bug 太多,没仔细考虑这两者之间的关联。后来经过几次测试,才确定下来是上层目录的 .env 和 .flaskenv 文件导致,但是一时找不到原因,就暂时放下了。直到 19 年 11 月,花了几个小时排查,还是没找到原因。

中间花了很长时间来追踪 Windows 特定的 Flask 程序无法启动的 bug(TypeError: environment can only contain strings),实在是怕了。因为 Flask 的 CLI 涉及太多东西,有时你要钻进 python-dotenv(#101) 和 Werkzeug(#1320) 才能找到问题的原因。

但是问题不解决的话,你永远睡不好觉。《Flask Web 开发实战》第一部分的示例程序都放在了一个程序仓库,而且都放在了子目录,这意味着如果读者错误的在仓库根目录创建 .env 和 .flaskenv 文件的话,就会导致子目录下的六个示例程序没法运行。前后大概收到 6 个相关的读者反馈,虽然后续在网站上添加了提醒,在重印的书里介绍创建 .env/.flaskenv 文件的地方追加提醒,但这终究没有真正解决问题,而且总会有人可以完美的错过所有提示。

如果每个人都可以在书上标记出错位置并共享,那么这一页应该会有很多红色小叉号(参考《超级马里奥制造》,也许未来某个电子书平台会做出来这个功能 :P)。

前几天在这个 Issue 的提醒下,又花了两个小时排查,这次终于找到原因。加上写 PR(#3560)和 Issue(#3561),前后两年一共花了 8 个小时,这个问题终于有了着落。没意外的话,预计会在下一个版本的 Flask 中更新。下面是具体原因。

本来以为这个问题和 python-dotenv 或 Werkzeug 相关,没想到只是 Flask 本身代码的问题,我太笨了,这个问题本可以早一点解决。

按照预定的行为,当安装了 python-dotenv,Flask 会自动加载 .env 和 .flaskenv 里的环境变量。python-dotenv 在搜索存储环境变量的文件时,会从当前目录开始向上搜索,如果找到就返回对应的文件路径。但是这时 Flask 如果发现 .env 或 .flaskenv 的所在目录不是当前目录,就会把当前工作目录切换到 .env 和 .flaskenv 所在的目录(相关源码)。而如果你的程序模块或程序包不是和 .env/.flaskenv 同级目录的话,就会导致找不到程序实例。

使用下面的步骤可以重现:

$ git clone https://github.com/greyli/flask-env-test
$ cd flask-env-test
$ pip install -r requirements.txt  # or just pip install flask[dotenv]
$ cd hello
$ flask run

示例项目的文件结构如下:

- flask-env-test
    - .env
    - hello
        - app.py

像示例程序这样把程序存储在 app.py 文件中时,运行 flask run 你会看到下面的报错:

$ flask run
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
Usage: flask run [OPTIONS]

Error: Could not locate a Flask application. You did not provide the "FLASK_APP" environment variable, and a "wsgi.py" or "app.py" module was not found in the current directory.

如果你使用 FLASK_APP 指定了程序的导入路径,那么错误大概会是这样:

$ flask run
 * Serving Flask app "myapp"
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
Usage: flask run [OPTIONS]

Error: Could not import "myapp".

相关链接

安装 Python 依赖出现 MarkupSafe ImportError … Feature 报错的解决方法

这个报错在 3 月 8 号 setuptools 发布新版本之后出现,通常会在安装 Python 依赖时触发。

报错信息

使用 pip 安装依赖时的报错如下:

Collecting markupsafe==1.0
  Downloading https://.../MarkupSafe-1.0.tar.gz (14 kB)
    ERROR: Command errored out with exit status 1:
     command: '...\python.exe' -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'...\\pip-install-bsormril\\markupsafe\\setup.py'"'"'; __file__='"'"'...\\pip-install-bsormril\\markupsafe\\setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base '...\pip-install-bsormril\markupsafe\pip-egg-info'
         cwd: ...\pip-install-bsormril\markupsafe\
    Complete output (5 lines):
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
      File "...\pip-install-bsormril\markupsafe\setup.py", line 6, in <module>
        from setuptools import setup, Extension, Feature
    ImportError: cannot import name 'Feature'
    ----------------------------------------
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.

使用 Pipenv 安装依赖时的报错如下:

An error occurred while installing markupsafe==1.0 --hash=sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665! Will try again.
Installing initially failed dependencies…
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 1874, in do_install
[pipenv.exceptions.InstallError]:       keep_outdated=keep_outdated
[pipenv.exceptions.InstallError]:   File "...Python\Python36\site-packages\pipenv\core.py", line 1253, in do_init
[pipenv.exceptions.InstallError]:       pypi_mirror=pypi_mirror,
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 859, in do_install_dependencies
[pipenv.exceptions.InstallError]:       retry_list, procs, failed_deps_queue, requirements_dir, **install_kwargs
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 763, in batch_install
[pipenv.exceptions.InstallError]:       _cleanup_procs(procs, not blocking, failed_deps_queue, retry=retry)
[pipenv.exceptions.InstallError]:   File "...\Python\Python36\site-packages\pipenv\core.py", line 681, in _cleanup_procs
[pipenv.exceptions.InstallError]:       raise exceptions.InstallError(c.dep.name, extra=err_lines)
[pipenv.exceptions.InstallError]: ['Looking in indexes: https://.../pypi/simple', 'Collecting markupsafe==1.0', '  Using cached https://.../MarkupSafe-1.0.tar.gz (14 kB)']
[pipenv.exceptions.InstallError]: ['ERROR: Command errored out with exit status 1:', '
command: \'...\\.virtualenvs\\helloflask-evdb6idn\\scripts\\python.exe\' -c \'import sys, setuptools, tokenize; sys.argv[0] = \'"\'"\'...\pip-install-pkgojp4t\\\\markupsafe\\\\setup.py\'"\'"\'; 
__file__=\'"\'"\'...\\\\pip-install-pkgojp4t\\\\markupsafe\\\\setup.py\'"\'"\';f=getattr(tokenize, \'"\'"\'open\'"\'"\', open)(__file__);code=f.read().replace(\'"\'"\'\\r\\n\'"\'"\', \'"\'"\'\\n\'"\'"\');f.close();exec(compile(code, __file__, \'"\'"\'exec\'"\'"\'))\' egg_info --egg-base \'...\\pip-install-pkgojp4t\\markupsafe\\pip-egg-info\'', '
cwd: ...\\pip-install-pkgojp4t\\markupsafe\\', '    Complete output (5 lines):', '    
Traceback (most recent call last):', '      
File "<string>", line 1, in <module>', ' 
File "...\\pip-install-pkgojp4t\\markupsafe\\setup.py", line 6, in <module>', '
from setuptools import setup, Extension, Feature', "
ImportError: cannot import name 'Feature'", '----------------------------------------',
'ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.']
ERROR: ERROR: Package installation failed...

其他工具输出类似,主要异常信息是 MarkupSafe setup.py: ImportError: cannot import name Feature。通常会在安装 Flask 项目的依赖时发生,因为 MarkupSafe 是 Flask 的依赖之一。

原因和解决方法

出现这个报错的原因是因为 Python 打包工具 setuptools 在 46.0.0 版本删掉了弃用的 Feature,而 MarkupSafe 刚好在 setup.py 文件里导入了这个类,所以会出现报错。

解决方法很多,最直接的是更新 MarkupSafe 到最新版本(1.1.1),新版本去掉了对 Feature 类的导入。如果使用 requirements.txt 存储依赖列表,那就把 MarkupSafe 的版本号改成 1.1.1(找到 MarkupSafe 开头那一行,替换版本号):

MarkupSafe==1.1.1

然后重新执行:

$ pip install -r requirements.txt

对于 Pipenv,可以直接执行:

$ pipenv install markupsafe==1.1.1

如果你是《Flask Web 开发实战》的读者,正在为第一部分的示例程序安装依赖,那你还需要执行下面的命令固定 sendgrid-python 的版本(它在新版本添加了不向后兼容的 API 变动):

$ pipenv install sendgrid==5.3.0

《Flask Web 开发实战》读者备注

如果你在 2020 年 3 月 8 号到 4 月 5 号之间买了《Flask Web 开发实战》,把示例程序克隆到了本地,然后尝试运行 pipenv install 或 pip install -r requirements.txt 命令来安装依赖,那很大概率你会遇到这个问题。

除了使用上面的方法之外,你还可以通过更新本地代码来解决。我最近给所有示例程序的依赖文件做了一次更新,除了书里涉及的 API 产生变动的依赖,其他依赖都已经更新到最新版本。

你可以使用下面的命令来更新你在本地的程序仓库(注意这会重置你对源码进行的修改):

$ git fetch --all
$ git fetch --tags
$ git reset --hard origin/master

然后重新执行一次 pipenv install 或 pip install -r requirements.txt 即可解决这个问题。如果遇到其他虚拟环境和依赖安装的问题可以参考这篇文章解决。

相关链接:

《Flask Web 开发实战》虚拟环境/依赖/Pipenv 等问题解决方法

注:这篇文章的主要受众是《Flask Web 开发实战》的读者。

注2:文中的 $ 符号标识一条命令行命令的开始,$ 前面是当前工作目录,# 号后面是注释。你实际只需要输入 $ 符号和 # 号之间的内容,不包括开头和结尾的空格。

在群聊和论坛里总是看到和虚拟环境和依赖安装相关的各类问题,这篇文章希望能够提供一个统一的解决方案。下次如果遇到有人问虚拟环境/Pipenv/依赖安装相关的问题,请把这篇文章的链接丢过去。

安装 Python 库非常慢?

在进入正题之前,你需要先解决基础设施问题。你在执行 pip install 命令或 pipenv install 等命令时会不会网速非常慢?20k/s 或者干脆看到 Time out,Connection reset 之类的报错,这种情况下,你需要设置 PyPI 镜像。具体操作可以在这篇《从国内的 PyPI 镜像(源)安装 Python 包》看到。

要不要继续使用 Pipenv?

因为书里面在一开始介绍了使用 Pipenv 管理依赖和虚拟环境,同时所有的安装第三方库的命令也都是使用 Pipenv,所以我们要解决的第一个问题是「要不要继续使用 Pipenv?」

我的建议是,如果你在使用的过程中没有遇到过任何报错,那么就继续使用它。直到你觉得它在某些地方不再让你满意。

但是如果你在使用的过程中遇到了问题(首先确保你使用的是最新版本的 Pipenv),比如:

  • 锁定依赖很慢,停留在「locking…」这样的提示不动
  • 执行正确的命令但是总是出现报错

附注 如果你安装依赖时的报错是「MarkupSafe setup.py: ImportError: cannot import name Feature」,请参考这篇文章解决。

那么就继续看下去。

如果不用 Pipenv,我该怎么办?

解决方法和替代工具非常多,这里给出两个。

方法一:不用虚拟环境

最简单的解决方法就是不用虚拟环境。如果你是一个初学者,那么不用虚拟环境完全没问题。现在你把所有的 Python 包全都安装在一个盒子里,你只需要会使用 pip 安装依赖,也就是使用下面的 pip install 命令:

$ pip install flask

附注 顺便说一句,不用虚拟环境时,如果你是使用 Linux 和 macOS 系统的 Python 3 用户,那么执行 Python 和 pip 相关命令的时候需要输入的是 python3 和 pip3,比如:pip3 install flask。后面不再提示。

这个命令会为你安装 Flask。每到需要安装一个包,你就执行这个命令,把上面的 flask 替换成你要安装的包名即可。类似的,书中所有 pipenv install xxx 形式的命令也都替换为 pip install xxx。

这时你可以跳过 Pipenv 和虚拟环境相关的内容。在第一章,当你把当前工作目录切换到 helloflask 文件夹内之后,整个 1.1 小节你只需要执行下面这行命令:

$ pip install -r requirements.txt

而第二部分每章开头的下面这两行命令:

$ pipenv install --dev
$ pipenv shell

都要替换为:

$ pip install -r requirements.txt

这个命令会安装对应项目的所有依赖,所以后续介绍各个 Python 库时的安装命令不需要再执行。

方法二:使用 virtualenv/venv 来管理虚拟环境,搭配 pip 来管理依赖

第二种方法是改用更基础的工具:你仍然使用 pip 安装依赖,同时搭配 virtualenv 或 venv 来管理虚拟环境。如果你选择这个方法,那就跳过书中对 Pipenv 的介绍,改为阅读这篇《要不我们还是用回 virtualenv/venv 和 pip 吧》。阅读完上述文章后,再继续阅读。

现在你可以跳过 Pipenv 相关的内容。第一章切换进 helloflask 目录后,整个 1.1 小节你只需要执行下面的命令:

$ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

附注 :上面命令里 # 号及之后的文字是注释,不需要输入。如果你使用 Python2,第一条命令需要改为 virtualenv env。这三行命令的作用依次为:创建虚拟环境、激活虚拟环境、从 requirements.txt 文件安装依赖列表。

类似的,第二部分每章开头的下面这两行命令:

$ pipenv install --dev
$ pipenv shell

都要替换为:

$ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
$ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
$ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

同时书中所有 pipenv install xxx 形式的命令都替换为 pip install xxx(不过你并不需要一个一个执行,因为每章开头执行 pip install -r requirements.txt 时会安装所有项目相关的依赖)。

对于 PyCharm 设置 Python 解释器部分的内容,大致可以沿用,只不过在图 1-4 的位置你需要从列表里选择当前目录 env 文件夹(虚拟环境文件夹)中的 Python 解释器,根据操作系统的不同,将会是 env/bin/python 或 env\Scripts\python.exe。

退出虚拟环境时,使用下面的命令:

$ deactivate

使用书中同样的代码,但是却出现报错?/ 我该怎么安装依赖?

有一些 Python 库在版本变化时会带来 API 的变化,而书中示例程序代码是基于每章开头注明的 Python 库来开发的,所以在更新依赖版本的时候可能会导致示例程序的代码出错。我建议你按照书中的命令来安装依赖,这会从项目依赖文件里安装固定版本的依赖列表,不要跳过第二部分每章开头的命令。

具体来说,如果使用 Pipenv,对应的安装依赖的命令是:

$ pipenv install --dev

如果使用 pip,对应的命令则是(如果创建了虚拟环境,需要先激活虚拟环境):

$ pip install -r requirements.txt

如果是第一章的示例程序,那么在 helloflask 目录下执行一次即可。如果是第二章的四个程序,那么在每一个程序的根目录执行一次即可。后续所有介绍新的 Python 包时给出的安装命令不用再执行。

关于第一部分示例程序的项目结构和启动问题

在第一部分的源码中,一共有 6 个 Flask 程序,分别保存在 helloflask/demos/ 目录下的六个子文件夹内。用来存储环境变量的 .env 和 .flaskenv 文件需要在这些子文件夹内创建,而不是放到顶层目录(helloflask/)。同时为了方便操作,这 6 个程序共用同一个虚拟环境,所以在 helloflask/ 目录下创建虚拟环境。

注意 不要在 helloflask/ 目录下创建 .env 和 .flaskenv 文件,这会导致子目录下的程序无法正确启动(issue #3561)。

如果你按照书里给出的提示执行命令,一般不会出现问题。但是为了防止各种意外情况,这里列一下本书第一部分的操作流程($ 符号前是当前工作目录)。

具体的操作顺序就是,克隆仓库以后,切换到 helloflask 目录:

$ cd helloflask

然后创建虚拟环境、激活虚拟环境、安装依赖:

/helloflask $ python -m venv env  # Linux、macOS 系统的 Python3 用户,使用 python3 -m venv env
/helloflask $ env\Scripts\activate  # Linux、macOS 系统使用 source env/bin/activate
/helloflask $ pip install -r requirements.txt  # 这个命令会安装对应项目的所有依赖

接着看到介绍 Flask 最小程序部分,启动第一部分的示例程序:

helloflask/ $ cd demos/hello
helloflask/demos/hello $ flask run

现在看到了第二章,启动第二部分的示例程序(先执行 cd .. 回到上层目录,即 demos/ 目录):

helloflask/demos/hello $ cd ..
helloflask/demos $ cd http
helloflask/demos/http $ flask run

看到第三章,启动第三部分的示例程序:

helloflask/demos/http $ cd ..
helloflask/demos $ cd template
helloflask/demos/template $ flask run

以此类推。

遇到了自己没法解决的问题怎么办?

如果遇到问题,你可以先尝试:

  • 看一下本书 GitHub 仓库里的 FAQ 页面 有没有你要找的答案
  • 在搜索引擎搜索你的错误信息关键字,尝试自己解决

自己无法解决的话,可以:

  • 发到 GitHub(issue)和论坛(用文字形式贴出完整的错误信息、相关代码和命令,尽可能的详细描述相关信息)。
  • 发到交流群(建议优先选择发到 GitHub 和论坛,我会定期回复,群聊沟通效率很低,更适合闲聊)

我在一本技术书里放置的十个彩蛋

Flask Web 开发实战》已经出版一年多,书里面的一些彩蛋也不知道有多少被人发现了。再不公开,我可能都要忘了。所以就趁着出版一周年这个契机整理一下吧。

这些彩蛋大都是关于电影的一些双关和文字游戏,有一些说是彩蛋可能会有些勉强。比较正式的一共有 10 个,分别对应十部电影。也就是第三章介绍模板引擎时的实例程序电影清单里列出来的十部电影。

下面按照出现顺序一一剧透。

阿甘正传(Forrest Gump)

P14 第一章:初识 Flask 1.3

这一节介绍运行 Flask 程序,章节标题是「Run, Flask, Run!」。

阿甘正传里,当阿甘被欺负的时候,珍妮总会喊「Run, Forrest, Run!」。

红白蓝三部曲(Three Colours trilogy)

P36 第二章:Flask 和 HTTP 2.2.3.3

介绍 URL 规则的 any 转换器时使用的示例如下:

@app.route('/colors/<any(blue, white, red):color>')
def three_colors(color):
    return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'

这个并没有什么特殊含义,就是列了三个颜色作为 URL 选项,返回的都是圣经里的那句话。

电影清单

P76 第三章:模板

第三章在示例程序里的填充数据是整本书所有彩蛋相关的十部电影,作为一个足够显眼的线索。

movies = [
    {'name': 'My Neighbor Totoro', 'year': '1988'},
    {'name': 'Three Colours trilogy', 'year': '1993'},
    {'name': 'Forrest Gump', 'year': '1994'},
    {'name': 'Perfect Blue', 'year': '1997'},
    {'name': 'The Matrix', 'year': '1999'},
    {'name': 'Memento', 'year': '2000'},
    {'name': 'The Bucket list', 'year': '2007'},
    {'name': 'Black Swan', 'year': '2010'},
    {'name': 'Gone Girl', 'year': '2014'},
    {'name': 'CoCo', 'year': '2017'},
]

另外这个电影清单也有现实版:IMDb镜像豆列

寻梦环游记(CoCo)

P105 第四章:模板

这一章一开始给出了一个 HTML 登录表单的示例,示例代码和渲染后的画面(图 4-1)中,用户名输入框的占位文字是 Héctor Rivera,密码输入框的占位文字是 19001130,勾选了记住登录状态的「记住我(Remember me)」选项。

Héctor Rivera 是电影里小男孩在亡灵世界里遇到的死去的爸爸,他的生日是 1900 年 11 月 30 日,勾选了「Remember me」,他就会被人记住,所以就不会消失了……

消失的爱人(Gone Girl)

P139 第五章:数据库

第一小节介绍关系型数据库的时候给出了一个示例表格(表 5-1),如下:

这两条数据分别对应《消失的爱人》里的男女主角。

记忆碎片(Memento)

同样在第五章,这一章写了一个用来记笔记的示例程序,分别使用了三条笔记内容作为示例演示 CRUD 操作,即:

这三条笔记均是《记忆碎片》里男主角记在各处的笔记。

中国发出的第一封电子邮件

P178 第六章:电子邮件

第六章介绍电子邮件使用的示例邮件正文是「Across the Great Wall we can reach every corner in the world.」。这是 1987 年从中国发出的第一封电子邮件正文。

本来是想把 Wall 换成另一个单词以便反映现状的,但是出于安全考虑,没那么做。不过这句话原版现在来看已经是够讽刺的了。

黑客帝国(The Matrix)

P246 第八章:个人博客

介绍自定义 flash 消息样式的时候,给出了下面的自定义 CSS 类示例:

.alert-matrix {
    color: #66ff66;
    background-color: #000000;
    border-color: #ebccd1; /* 这一行好像没用 */
}

调用示例如下:

flash('Knock, knock, Neo.', 'matrix')

实际的显示效果如下所示:

模拟了电影原图:

 

《未麻的部屋》(Perfect Blue)和《黑天鹅》(Black Swan)

P236 第八章:个人博客

这一章的示例博客名字叫做 Bluelog,模仿了《未麻的部屋》里记录未麻生活的网站,初始化账户的信息如下:

def fake_admin():
    admin = Admin(
        username='admin',
        blog_title='Bluelog',
        blog_sub_title="No, I'm the real thing.",
        name='Mima Kirigoe',
        about='Um, l, Mima Kirigoe, had a fun time as a member of CHAM...'
    )
    ...

另外介绍博客的主题地方提供了蓝色和黑色两个主题,主题名分别是「Perfect Blue」和「Black Swan」,对应《未麻的部屋》和《黑天鹅》两部电影的英文名(后者致敬了前者)。

龙猫(My Neighbor Totoro)

P260 第八章:个人博客

这个算不上彩蛋……在介绍生成英文标题 slug 的 slugify 函数的时候,实际调用的示例使用了龙猫的几种名字作为输入数据:

>>> slugify(u'My Neighbor Totoro')
u'my-neighbor-totoro'
>>> slugify(u'邻家的豆豆龙')
u'lin-jia-de-dou-dou-long'
>>> slugify(u'となりのトトロ')
u'tonarinototoro'

遗愿清单(The Bucket List)

P421 第十章:待办事项程序

这个彩蛋是刻意加的,因为这一章的示例程序是一个 Todo List 程序,所以要找一个和清单相关的电影。示例程序里的几条待办事项就是电影里的一些遗愿。当时还没看过这个电影,特意去看了一遍,感觉一般。

应该就这些了,如果还有其他的,欢迎补充。

那么放置这些彩蛋的目的是?没目的……仅仅就是为了好玩。同时为了能让发现彩蛋的人也会觉得好玩。

 

《Flask Web 开发实战》第二次重印

最近要进行第二次重印,花了整整两天时间整理勘误。接下来的计划是,在第二次重印的书上市前,只在晚上处理论坛问题,尽量不在 IM 上回答和书上错误无关的提问(耗费心力,沟通成本非常高),也不再更新勘误页面。等到第二次重印的书上市,做下面几件事:

  • 写一篇《Flask Web 开发实战 2019 补丁》,汇总所有额外的项目源码变动,第三方工具和库的新版变化,常见的错误和常见问题等。
  • 和豆瓣协商,再为购买旧版本的读者更新一次文件;同时了解微信读书、多看、掌阅、京东、当当几个电子书平台的更新情况。
  • 再集中清理一次盗版文件。
  • 处理 Albumy 现存的两个 bug。
  • 整理一遍所有勘误,更新勘误页面。

2019/4/15 更新:

收到出版社的新版样书,也就是说,1-3 版本的书已经上市销售,目前哪里可以买到还不清楚(估计是京东自营)。

我的书终于重印了

盼了几个月,《Flask Web 开发实战》终于重印了。为什么这么期待重印?当然是因为重印可以修正书中的错误!假如你经常写博客的话,你可以想象一下这样的场景,你在昨天发布的一篇文章里发现大量的错误,但是却没法更新文章,而你的读者还在不停的找出更多的错误……而面对一本已经付印的书,除了感到愧疚,努力整理勘误,告诉读者阅读勘误外,你什么也做不了。这种感觉真是太糟糕了!

这次重印的 1-2 版本主要有下面这些变化:

  • 修正了 1-1 版本 11 月前已知的所有勘误。
  • 更新前言开始介绍 Flask 时的 GitHub 仓库 stars 和 watchers 数量。
  • 前言最后添加了初稿被删掉的后记(稍作修改)。
  • 在前言、附录、封底的作者简介下面添加了勘误页面网址。
  • 完善了多处内容,包括为新版本 Pipenv 和 PyCharm 的变化添加说明,去掉附录中过时的项目等,具体见可改进实现文件
  • 修正了 42 处断行连字符错误。
  • 比 1-1 的封面更好看,封面文字的颜色变淡了,纸张变白了。

尽管修正了这么多的错误,1-2 版本的勘误表仍然还有不少内容。不过,我相信,如果还有下一次重印,那么大部分错误都将得到修正。希望这一天早点来到!

顺便说一句,据出版社编辑的消息,电子书文件已经相应更新,如果没有接收到推送,可以联系各平台客服手动更新文件。

出版社寄了两本 1-2 版本的样书,稍后会在知乎专栏 Hello, Flask! 送出,也算是稍稍填补一下我的愧疚。

写作一本技术书,能给一个社区带来哪些改变?

写作《Flask Web开发实战》花费了一年多的时间,在这期间,除了编写5个项目实例和写作外,我还花了一部分时间来和书中涉及的Python库(主要是Flask扩展)打交道。这篇文章总结了这本书的写作给整个Flask社区带来的一些改变。

国内第一本Flask书

虽然国内已经有几本书介绍过Flask,但都是顺便介绍,而不是真正意义上的Flask书。因此,这本书可以说是国内第一本Flask书,很高兴能为国内的Flask社区带来这样一本书。希望这本书能够推动更多人了解和使用Flask,进而为社区贡献更多的力量。

完善多个开源项目

在实际介绍和使用某个库时,总会遇到各种各样的问题,主要有两类:

  • Bug
  • 不完善的用法或实现

Bug不用多说,肯定要处理。对于不完善的用法和实现,如果沿用原来的代码,一来不容易读者理解,二来还需要在书中加进很多不必要的内容。所以,我更倾向于让这些代码变得更好一点。

如果把不完善的实现写到书里,我会觉得非常不舒服。比如说,WTForms本身是支持设置内置的错误消息的语言的,我们可以设为中文,而不用手动写错误消息。但是在Flask-WTF中,因为内部重写了相关方法,导致没法在不使用Flask-Babel的情况下使用内置的翻译。问题来了:如果在第4章介绍Flask-Babel和i18n等相关概念的话,这显然需要占据大量篇幅,并不合适,而且这部分内容已经计划加入到第10章。

为此,书的安排没法改动,那么只好调整Flask-WTF的内部实现,所以就有了这个PR。类似的情况还有很多,比如Firefox支持headless模式后,Selenium中却没有提供和Chrome相同的导入接口;Flask-OAuthlib中,传入access令牌只能使用元组或字典类型,而不能使用更直观的字符串变量传入……

下面是一些写作期间提交的开源贡献:

当然,如果加上其他没有太大意义的文档更新,这个列表还可以更长。
 
值得特别说明的是Werkzeug的这个PR,这个PR对应的bug困扰了我很长一段时间。一开始我把问题归咎到python-dotenv身上,还创建了一个PR(#101),但是维护者迟迟没有回复。后来再次研究,发现可以直接在Werkzeug内部解决,于是创建了#1320,然后被lepture用更好的实现取代了(#1321)。不过,在python-dotenv创建的那个PR倒是促成了Pipenv的这个PR(#2386)。

推动发布新的版本

除了让PR被合并,还要让这些项目发布新的release,这样才能在项目源码公开后,让读者可以正常使用。在我的推动下,Flask-OAuthlib、Flask-Whooshee和Flask-Moment都发布了新的版本。

其他的项目,除了Werkzeug以外,大部分都在整个写作的时间跨度发布了新的版本。尤其重要的是Flask的1.0版本。从书一开始写作,我就直接采用了主分支的代码,书中我直接将版本号写为0.13。如果Flask迟迟不发布新版本,那我会陷入一个很尴尬的境地:要么大胆采用主分支代码,但是可能会出现变动,导致书中的内容不可靠;要么采用旧版本,那书出版后可能会很快面临过时的危险。还好,在写作接近尾声的4月末,新版本1.0发布了。

4个Flask扩展

用于集成Bootstrap的扩展Flask-Bootstrap目前的维护状态很糟糕:上一次release是17年1月,上一次合并commit是17年3月,而且已经很长时间不再处理PR和Issue。

原项目还存在很多问题:Jinja语法不标准、内置基模板引入不必要的复杂度、分页宏不支持传入URL片段值。除此之外,让我决定写新的扩展替代它的最主要原因是因为它不支持Bootstrap 4,而我不能接受在书中引入一个这样过时的项目。

为了解决这些问题,只好写了Bootstrap-Flask。最终决定写替代扩展的时候,书已经完成了三分之二,大量旧的内容和源码都要改写。把书稿和源码从Flask-Bootstrap和Bootstrap3迁移到Bootstrap-Flask和Bootstrap4是一个非常痛苦的过程,到现在还记忆犹新……

对于某些空白的领域,我写了三个扩展来简化集成操作:

5个Flask开源项目

在此之前,完善的Flask开源项目屈指可数,除了Flask官方提供的两个示例程序外,就只有Miguel Grinberg的两个程序。而这本书带来了5个相对比较完善、所有依赖都基于最新版本的开源项目(严格来说,第1个程序比较简单,可以排除在外):

如何向Jinja宏传递额外参数(*args和**kwargs)?

这段时间有多个读者问关于Jinja宏定义时的参数接受问题。这一点在《Flask Web开发实战》里没有介绍,这篇文章作为一个补充。

一个不符合直觉的设定

在某个晴朗的早晨,你打开电脑,想在你的项目Jinja模板里编写一个宏来简化操作。按照直觉,你可能会像定义Python函数那样来定义宏,传入**kwargs来让它接收任意数量的关键字参数,比如:

{% macro say_hello(**kwargs) %}
     ...
{% endmacro %}

或是传入*args让它接收任意数量的位置参数:

{% macro say_hello(*args) %}
     ...
{% endmacro %}

遗憾的是,上面的调用会分别获得下面的错误信息:

jinja2.exceptions.TemplateSyntaxError: expected token 'name', got '**'
jinja2.exceptions.TemplateSyntaxError: expected token 'name', got '*'

在Jinja宏里接收额外的关键字参数和位置参数

在Jinja中,宏默认会自动接收额外的关键字参数和位置参数,并在宏内部提供kwargsvarargs特殊变量来获取它们。具体来说,在定义宏的时候,不需要进行任何声明。在宏的内部,你可以直接使用kwargs字典来获取额外的关键字参数;同样的,你可以使用varargs元组来获取额外传入的位置参数。

下面是使用kwargs的示例:

{% macro say_hello() %}
    <p>Hello, {{ kwargs['name'] }}!</p>
{% endmacro %}

{# 调用示例 #}
{{ say_hello(name='Grey')}}

你可以把这个字典传递给其他函数,比如url_for():

{% macro nav_link(endpoint, text) %}
    <a href="{{ url_for(endpoint, **kwargs) }}">{{ text }}</a>
{% endmacro %} 

{# 调用示例 #}
{{ nav_link('index', 'Home', foo='value1', bar='value2')}}

下面是使用varargs的示例:

{% macro say_hello() %}
    <p>Hello, {{ varargs[0] }}!</p>
{% endmacro %}

{# 调用示例 #}
{{ say_hello('Grey')}}

提示 在宏内部,如果kwargs字典里没有对应的键,那么会返回空字符串,而不是抛出KeyError异常;如果向varargs元组索引一个超出范围的下标值,也会返回空值,而不会抛出IndexError异常。

隐藏的陷阱

虽然宏自动处理额外传入的关键字参数和位置参数,但是这里有一个隐藏的小陷阱。如果你在调用一个宏的时候传入了额外的关键字参数和位置参数,但是宏的内部并没有使用它们,这时就会出错。比如下面使用kwargs的示例:

{% macro say_hello() %}
    <p>Hello!</p>
{% endmacro %} 

{# 调用示例 #} 
{{ say_hello(name='Grey')}}

调用宏的时候传入了name参数,但是宏内部并没有使用它,这时Jinja会抛出下面的异常:

TypeError: macro 'say_hello' takes no keyword argument 'name'

类似的是位置参数,你会获得下面的异常:

TypeError: macro 'say_hello' takes not more than 1 argument(s)

所以,如果你想让某个宏接收额外的关键字参数或位置参数,你就分别需要在这个宏内部至少调用一次(access)kwargs字典或是varargs元组。一般情况下,你并不需要担心这个问题。

本文隶属于《Flask Web开发实战》番外文章系列。

《Flask Web开发实战》的封面设计是如何诞生的?

一本书当然包含很多故事,这篇文章来介绍一下《Flask Web开发实战》封面设计的过程。

其实,这也算不上设计。因为这本书隶属于“Web开发系列”,这个系列的书都有一个通用的设计模板,唯一不同的就是确定封面中的图案。在选择图片前,我立下了几个规则:

  • 使用Flask风格图片(黑白线条)
  • 突出“实战”的含义

基于这两点,我最先发现的合适素材是一张飞机的图片,这个图片是Flask作者Armin Ronacher在2011年的演讲“Opening the Flask”中使用的幻灯片的第10页的插图:

这就是封面设计初稿:

我非常喜欢这个封面,编辑也表示很不错。遗憾的是,我没有搜索到这个图片的版权方,无法商用,而且Armin Ronacher也表示记不起来是从哪里拿到这张图片。

接下来,我花费了大量时间想找一个替代品,但是却始终找不到满意的。

无奈之下,我只好扩大范围,不再局限于飞机图片,而且延伸到了船、武器、人物,陆续找到下面这些替代品:

也许是因为“曾经沧海”,试来试去这些都不满意。这时我忽然有了一个念头:Miguel Grinberg那本《Flask Web开发》封面上是一只狗,我可以用一只狼呀。既能突出实战的特点,又有一点遥相呼应的味道,形成一个有趣的对比。

虽然心底有一些犹疑:会不会太像O’reilly的“动物书”?读者会不会觉得这是山寨?但是当我看到这张狼的图片时,顿时就喜欢上了,疑虑也消了大半:

把图片放到封面上看起来也非常合适:

这就是最终的封面初稿。当然,经过出版社设计人员的美化,最终的封面要好看得多:

于是,《Flask Web开发实战》的封面就这么诞生了。

从HTTP请求-响应循环探索Flask的基本工作方式

本文基于Flask Web开发实战2章《Flask与HTTP》删减改写而来,完整的章节目录请访问本书主页http://helloflask.com/book查看。

本文中所指的示例程序在helloflask仓库的demos/http目录下。

HTTP(Hypertext Transfer Protocol,超文本传输协议)定义了服务器和客户端之间信息交流的格式和传递方式,它是万维网(World Wide Web)中数据交换的基础。在这篇文章中,我们会以HTTP协议定义的请求响应循环流程作为框架,了解Flask处理请求和响应的各种方式。

附注 HTTP的详细定义在RFC 7231~7235中可以看到。RFC(Request For Comment,请求评议)是一系列关于互联网标准和信息的文件,可以将其理解为互联网(Internet)的设计文档。完整的RFC列表可以在这里看到:https://tools.ietf.org/rfc/

本章的示例程序在helloflask仓库的demos/http目录下,你可以通过下面的操作来运行程序:

$ git clone https://github.com/greyli/helloflask
$ cd helloflask
$ pipenv install
$ pipenv shell
$ cd demos/http
$ flask run

请求响应循环

为了更贴近现实,我们以一个真实的URL为例:

http://helloflask.com/hello

当我们在浏览器中的地址栏中输入这个URL,然后按下Enter,稍等片刻,浏览器会显示一个问候页面。这背后到底发生了什么?你一定可以猜想到,这背后也有一个类似我们第1章编写的程序运行着。它负责接收用户的请求,并把对应的内容返回给客户端,显示在用户的浏览器上。事实上,每一个Web应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”:客户端发出请求,服务器处理请求并返回响应,如下图所示:

请求响应循环示意图

请求响应循环示意图

附注 客户端(Client Side)是指用来提供给用户的与服务器通信的各种软件。在本书中,客户端通常指Web浏览器(后面简称浏览器),比如Chrome、Firefox、IE等;服务器端(Server Side)则指为用户提供服务的服务器,也是我们的程序运行的地方。

这是每一个Web程序的基本工作模式,如果再进一步,这个模式又包含着更多的工作单元,

下图展示了一个Flask程序工作的实际流程:

Flask Web程序工作流程

Flask Web程序工作流程

从上图可以看出,HTTP在整个流程中起到了至关重要的作用,它是客户端和服务器端之间沟通的桥梁。

当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP格式的请求数据转换成我们的Flask程序能够使用的Python数据。在程序中,Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行JavaScript代码,最终把解析后的页面呈现在用户浏览器的窗口中。

提示 关于WSGI的更多细节,我们会在第16章进行详细介绍。

提示 这里的服务器指的是处理请求和响应的Web服务器,比如我们上一章介绍的开发服务器,而不是指物理层面上的服务器主机。

HTTP请求

URL是一个请求的起源。不论服务器是运行在美国洛杉矶,还是运行在我们自己的电脑上,当我们输入指向服务器所在地址的URL,都会向服务器发送一个HTTP请求。一个标准的URL由很多部分组成,以下面这个URL为例:

http://helloflask.com/hello?name=Grey

这个URL的各个组成部分如下表所示:

信息

说明

http://

协议字符串,指定要使用的协议

helloflask.com

服务器的地址(域名)

/hello?name=Grey

要获取的资源路径(path),类似Unix的文件目录结构

附注 这个URL后面的?name=Grey部分是查询字符串(query string)。URL中的查询字符串用来向指定的资源传递参数。查询字符串从问号?开始,以键值对的形式写出,多个键值对之间使用&分隔。

请求报文

当我们在浏览器中访问这个URL时,随之产生的是一个发向http://helloflask.com所在服务器的请求。请求的实质是发送到服务器上的一些数据,这种浏览器与服务器之间交互的数据被称为报文(message),请求时浏览器发送的数据被称为请求报文(request message),而服务器返回的数据被称为响应报文(response message)。

请求报文由请求的方法、URL、协议版本、首部字段(header)以及内容实体组成。前面的请求产生的请求报文示意如下表所示:

组成说明

请求报文内容

报文首部:请求行(方法、URL、协议)

GET /hello HTTP/1.1

报文首部:各种首部字段

Host: helloflask.com

Connection: keep-alive

Cache-Control: max-age=0

User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36

空行

 

报文主体

name=Grey

如果你想看看真实的HTTP报文,可以在浏览器中向任意一个有效的URL发起请求,然后在浏览器的开发者工具(F12)里的Network标签中看到URL对应资源加载的所有请求列表,点击任一个请求条目即可看到报文信息,下图是使用Chrome访问本地的示例程序的示例:

在Chrome浏览器中查看请求和响应报文

在Chrome浏览器中查看请求和响应报文

报文由报文首部和报文主体组成,两者由空行分隔,请求报文的主体一般为空。如果URL中包含查询字符串,或是提交了表单,那么报文主体将会是查询字符串和表单数据。

HTTP通过方法来区分不同的请求类型。比如,当你直接访问一个页面时,请求的方法是GET;当你在某个页面填写了表单并提交时,请求方法则通常为POST。下表是常见的几种HTTP方法类型:

方法

说明

GET

获取资源

POST

传输数据

PUT

传输文件

DELETE

删除资源

HEAD

获得报文首部

OPTIONS

询问支持的方法

报文首部包含了请求的各种信息和设置,比如客户端的类型,是否设置缓存,语言偏好等等。

附注 HTTP中可用的首部字段列表可以在https://www.iana.org/assignments/message-headers/message-headers.xhtml看到。请求方法的详细列表和说明可以在RFC 7231中看到。

如果运行了示例程序,那么当你在浏览器中访问http://127.0.0.1:5000/hello,开发服务器会在命令行中输出一条记录日志,其中包含请求的主要信息:

127.0.0.1 - - [02/Aug/2017 09:51:37] "GET /hello HTTP/1.1" 200 –

Request对象

现在该让Flask的请求对象request出场了,这个请求对象封装了从客户端发来的请求报文,我们能从它获取请求报文中的所有数据。

注意 请求解析和响应封装实际上大部分是由Werkzeug完成的,Flask子类化Werkzeug的请求(Request)和响应(Response)对象并添加了和程序相关的特定功能。在这里为了方便理解,我们先略过不谈。在第16章,我们会详细了解Flask的工作原理。

和上一节一样,我们先从URL说起。假设请求的URL是http://helloflask.com/hello?name=Grey,当Flask接收到请求后,请求对象会提供多个属性来获取URL的各个部分,常用的属性如下表所示:

属性

path

u’/hello’

full_path

u’/hello?name=Grey’

host

u’helloflask.com’

host_url

u’http://helloflask.com/ ‘

base_url

u’http://helloflask.com/hello ‘

url

u’http://helloflask.com/hello?name=Grey ‘

url_root

u’http://helloflask.com/ ‘

除了URL,请求报文中的其他信息都可以通过request对象提供的属性和方法获取,其中常用的部分如下表所示:

属性/方法

说明

args

Werkzeug的ImmutableMultiDict对象。存储解析后的查询字符串,可通过字典方式获取键值。如果你想获取未解析的原生查询字符串,可以使用query_string属性

blueprint

当前蓝本的名称,关于蓝本的概念在本书第二部分会详细介绍

cookies

一个包含所有随请求提交的cookies的字典

data

包含字符串形式的请求数据

endpoint

与当前请求相匹配的端点值

files

Werkzeug的MultiDict对象,包含所有上传文件,可以使用字典的形式获取文件。使用的键为文件input标签中的name属性值,对应的值为Werkzeug的FileStorage对象,可以调用save()方法并传入保存路径来保存文件

form

Werkzeug的ImmutableMultiDict对象。类似files,包含解析后的表单数据。表单字段值通过input标签的name属性值作为键获取

values

Werkzeug的CombinedMultiDict对象,结合了args和form属性的值

get_data(cache=True, as_text=False, parse_from_data=False)

获取请求中的数据,默认读取为字节字符串(bytestring),将as_text设为True返回值将是解码后的unicode字符串

get_json(self, force=False, silent=False, cache=True)

作为JSON解析并返回数据,如果MIME类型不是JSON,返回None(除非force设为True);解析出错则抛出Werkzeug提供的BadRequest异常(如果未开启调试模式,则返回400错误响应,后面会详细介绍),如果silent设为True则返回None;cache设置是否缓存解析后的JSON数据

headers

一个Werkzeug的EnvironHeaders对象,包含首部字段,可以以字典的形式操作

is_json

通过MIME类型判断是否为JSON数据,返回布尔值

json

包含解析后的JSON数据,内部调用get_json(),可通过字典的方式获取键值

method

请求的HTTP方法

referrer

请求发起的源URL,即referer

scheme

请求的URL模式(http或https)

user_agent

用户代理(User Agent,UA),包含了用户的客户端类型,操作系统类型等信息

提示 Werkzeug的MutliDict类是字典的子类,它主要实现了同一个键对应多个值的情况。比如一个文件上传字段可能会接收多个文件。这时就可以通过getlist()方法来获取文件对象列表。而ImmutableMultiDict类继承了MutliDict类,但其值不可更改。具体访问Werkzeug文档相关数据结构章节http://werkzeug.pocoo.org/docs/latest/datastructures/

在我们的示例程序中实现了同样的功能。当你访问http://localhost:5000/hello?name=Grey,页面加载后会显示“Hello, Grey!”。这说明处理这个URL的视图函数从查询字符串中获取了查询参数name的值,如下所示:

from flask import Flask, request

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'Flask')  # 获取查询参数name的值
    return '<h1>Hello, %s!</h1>' % name  # 插入到返回值中

注意 上面的示例代码包含安全漏洞,在现实中我们要避免直接将用户传入的数据直接作为响应返回,在本章的末尾我们将介绍包括这个漏洞在内的Web常见安全漏洞的具体细节和防范措施。

需要注意的是,和普通的字典类型不同,当我们从request对象中类型为MutliDict或ImmutableMultiDict的属性(比如files、form、args)中直接使用键作为索引获取数据时(比如request.args[‘name’]),如果没有对应的键,那么会返回HTTP 400错误响应(Bad Request,表示请求无效),而不是抛出KeyError异常,如下图所示。为了避免这个错误,我们应该使用get()方法获取数据,如果没有对应的值则返回None;get()方法的第二个参数可以设置默认值,比如requset.args.get(‘name’, ‘Human’)。

400错误响应

400错误响应

提示 如果开启了调试模式,那么会抛出BadRequestKeyError异常并显示对应的错误堆栈信息,而不是常规的400响应。

在Flask中处理请求

URL是指向网络上资源的地址。在Flask中,我们需要让请求的URL匹配对应的视图函数,视图函数返回值就是URL对应的资源。

路由匹配

为了便于将请求分发到对应的视图函数,程序实例中存储了一个路由表(app.url_map),其中定义了URL规则和视图函数的映射关系。当请求发来后,Flask会根据请求报文中的URL(path部分)来尝试与这个表中的所有的URL规则进行匹配,调用匹配成功的视图函数。如果没有找到匹配的URL规则,说明程序中没有处理这个URL的视图函数,Flask会自动返回404错误响应(Not Found,表示资源未找到)。你可以尝试在浏览器中访问http://localhost:5000/nothing,因为我们的程序中没有视图函数负责处理这个URL,所以你会得到404响应,如下图所示:

404错误响应

404错误响应

如果你经常上网,那么肯定会对这个错误代码相当熟悉,它表示请求的资源没有找到。和前面提及的400错误响应一样,这类错误代码被称为HTTP状态码,用来表示响应的状态,具体会在下面详细讨论。

当请求的URL与某个视图函数的URL规则匹配成功时,对应的视图函数就会被调用。使用flask routes命令可以查看程序中定义的所有路由,这个列表由app.url_map解析得到:

$ flask routes
Endpoint  Methods  Rule
--------  -------  -----------------------
hello     GET      /hello
go_back   GET      /goback/<int:age>
hi         GET      /hi
...
static    GET      /static/<path:filename>

在输出的文本中,我们可以看到每个路由对应的端点(Endpoint)、HTTP方法(Methods)和URL规则(Rule),其中static端点是Flask添加的特殊路由,用来访问静态文件,具体我们会在第3章学习。

设置监听的HTTP方法

在上一节通过flask routes命令打印出的路由列表可以看到,每一个路由除了包含URL规则外,还设置了监听的HTTP方法。GET是最常用的HTTP方法,所以视图函数的默认监听的方法类型就是GET,HEAD、OPTIONS方法的请求由Flask处理,而像DELETE、PUT等方法一般不会在程序中实现,在后面我们构建Web API时才会用到这些方法。

我们可以在app.route()装饰器中使用methods参数传入一个包含监听的HTTP方法的可迭代对象。比如,下面的视图函数同时监听GET请求和POST请求:

@app.route('/hello', methods=['GET', 'POST'])
def hello():
    return '<h1>Hello, Flask!</h1>'

当某个请求的方法不符合要求时,请求将无法被正常处理。比如,在提交表单时通常使用POST方法,而如果提交的目标URL对应的视图函数只允许GET方法,这时Flask会自动返回一个405错误响应(Method Not Allowed,表示请求方法不允许),如下图所示:

405错误响应

405错误响应

通过定义方法列表,我们可以为同一个URL规则定义多个视图函数,分别处理不同HTTP方法的请求,我们在本书第二部分构建Web API时会用到这个特性。

3. URL处理

从前面的路由列表中可以看到,除了/hello,这个程序还包含许多URL规则,比如和go_back端点对应的/goback/<int:year>。现在请尝试访问http://localhost:5000/goback/34,在URL中加入一个数字作为时光倒流的年数,你会发现加载后的页面中有通过传入的年数计算出的年份:“Welcome to 1984!”。仔细观察一下,你会发现URL规则中的变量部分有一些特别,<int:year>表示为year变量添加了一个int转换器,Flask在解析这个URL变量时会将其转换为整型。URL中的变量部分默认类型为字符串,但Flask提供了一些转换器可以在URL规则里使用,如下表所示:

转换器

说明

string

不包含斜线的字符串(默认值)

int

整型

float

浮点数

path

包含斜线的字符串。static路由的URL规则中的filename变量就使用了这个转换器

any

匹配一系列给定值中的一个元素

uuid

UUID字符串

转换器通过特定的规则指定,即“<转换器:变量名>”。<int:year>把year的值转换为整数,因此我们可以在视图函数中直接对year变量进行数学计算:

@app.route('goback/<int:year>')
def go_back(year):
    return '<p>Welcome to %d!</p>' % (2018 - year)

默认的行为不仅仅是转换变量类型,还包括URL匹配。在这个例子中,如果不使用转换器,默认year变量会被转换成字符串,为了能够在Python中计算天数,我们就需要使用int()函数将year变量转换成整型。但是如果用户输入的是英文字母,就会出现转换错误,抛出ValueError异常,我们还需要手动验证;使用了转换器后,如果URL中传入的变量不是数字,那么会直接返回404错误响应。比如,你可以尝试访问http://localhost:5000/goback/tang。

在用法上唯一特别的是any转换器,你需要在转换器后添加括号来给出可选值,即“<any(value1, value2, …):变量名>”,比如:

@app.route('/colors/<any(blue, white, red):color>')
def three_colors(color):
    return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'

当你在浏览器中访问http://localhost:5000/colors/<color>时,如果将<color>部分替换为any转换器中设置的可选值以外的任意字符,均会获得404错误响应。

如果你想在any转换器中传入一个预先定义的列表,可以通过格式化字符串的方式(使用%或是format()函数)来构建URL规则字符串,比如:

colors = ['blue', 'white', 'red']

@app.route('/colors/<any(%s):color>' % str(colors)[1:-1])
...

HTTP响应

在Flask程序中,客户端发出的请求触发相应的视图函数,获取返回值会作为响应的主体,最后生成完整的响应,即响应报文。

响应报文

响应报文主要由协议版本、状态码(status code)、原因短语(reason phrase)、响应首部和响应主体组成。以发向localhost:5000/hello的请求为例,服务器生成的响应报文示意如下表所示:

组成说明

响应报文内容

报文首部:状态行(协议、状态码、原因短语)

HTTP/1.1 200 OK

报文首部:各种首部字段

Content-Type: text/html; charset=utf-8

Content-Length: 22

Server: Werkzeug/0.12.2 Python/2.7.13

Date: Thu, 03 Aug 2017 05:05:54 GMT

空行

 

报文主体

<h1>Hello, Human!</h1>

响应报文的首部包含了一些关于响应和服务器的信息,这些内容由Flask生成,而我们在视图函数中返回的内容即为响应报文中的主体内容。浏览器接受到响应后,会把返回的响应主体解析并显示在浏览器窗口上。

HTTP状态码用来表示请求处理的结果,下表是常见的几种状态码和相应的原因短语:

类型

状态码

原因短语(用于解释状态码)

说明

成功

200

OK

请求被正常处理

201

Created

请求被处理,并创建了一个新资源

204

No Content

请求处理成功,但无内容返回

重定向

301

Moved Permanently

永久重定向

302

Found

临时性重定向

304

Not Modified

请求的资源未被修改,重定向到缓存的资源

客户端错误

400

Bad Request

表示请求无效,即请求报文中存在错误

401

Unauthorized

类似403,表示请求的资源需要获取授权信息,在浏览器中会弹出认证弹窗

403

Forbidden

表示请求的资源被服务器拒绝访问

404

Not Found

表示服务器上无法找到请求的资源或URL无效

服务器端错误

500

Internal Server Error

服务器内部发生错误

提示 当关闭调试模式时,即FLASK_ENV使用默认值production,如果程序出错,Flask会自动返回500错误响应;而调试模式下则会显示调试信息和错误堆栈。

附注 响应状态码的详细列表和说明可以在RFC 7231中看到。

在Flask中生成响应

响应在Flask中使用Response对象表示,响应报文中的大部分内容由服务器处理,大多数情况下,我们只负责返回主体内容。

根据我们在请求一节介绍的内容,Flask会先判断是否可以找到与请求URL相匹配的路由,如果没有则返回404响应。如果找到,则调用对应的视图函数,视图函数的返回值构成了响应报文的主体内容,正确返回时状态码默认为200。Flask会调用make_response()方法将视图函数返回值转换为响应对象。

完整的说,视图函数可以返回最多由三个元素组成的元组:响应主体、状态码、首部字段。其中首部字段可以为字典,或是两元素元组组成的列表。

比如,普通的响应可以只包含主体内容:

@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>'

默认的状态码为200,下面指定了不同的状态码:

@app.route('/hello')
def hello():
    ...
    return '<h1>Hello, Flask!</h1>', 201

有时你会想附加或修改某个首部字段。比如,要生成状态码为3XX的重定向响应,需要将首部中的Location字段设置为重定向的目标URL:

@app.route('/hello')
def hello():
    ...
    return '', 302, {'Location': 'http://www.example.com'}

现在访问http://localhost:5000/hello,会重定向到http://www.example.com。在多数情况下,除了响应主体,其他部分我们通常只需要使用默认值即可。

重定向

如果你访问http://localhost:5000/hi,你会发现页面加载后地址栏中的URL变为了http://localhost:5000/hello。这种行为被称为重定向(Redirect),你可以理解为网页跳转。在上一节的示例中,状态码为302的重定向响应的主体为空,首部中需要将Location字段设为重定向的目标URL,浏览器接受到重定向响应后会向Location字段中的目标URL发起新的GET请求,整个流程下图所示:

重定向流程示意图

重定向流程示意图

在Web程序中,我们经常需要进行重定向。比如,当某个用户在没有经过认证的情况下访问需要登录后才能访问的资源,程序通常会重定向到登录页面。

对于重定向这一类特殊响应,Flask提供了一些辅助函数。除了像前面那样手动生成302响应,我们可以使用Flask提供的redirect()函数来生成重定向响应,重定向的目标URL作为第一个参数。前面的例子可以简化为:

from flask import Flask, redirect
...
@app.route('/hello')
def hello():
    return redirect('http://www.example.com')

提示 使用redirect()函数时,默认的状态码为302,即临时重定向。如果你想修改状态码,可以在redirect()函数中作为第二个参数或使用code关键字传入。

如果要在程序内重定向到其他视图,那么只需在redirect()函数中使用url_for()函数生成目标URL即可,如下所示:

from flask import Flask, redirect, url_for 

...

@app.route('/hi')
def hi():
    ...
    return redierct(url_for('hello'))  # 重定向到/hello

@app.route('/hello')
def hello():
    ...

错误响应

如果你访问http://localhost:5000/brew/coffee,你会获得一个418错误响应(I’m a teapot),如下图所示:

418错误响应

418错误响应

附注 418错误响应由IETF(Internet Engineering Task Force,互联网工程任务组)在1998年愚人节发布的HTCPCP(Hyper Text Coffee Pot Control Protocol,超文本咖啡壶控制协议)中定义(玩笑),当一个控制茶壶的 HTCPCP 收到 BREW 或 POST 指令要求其煮咖啡时应当回传此错误。

大多数情况下,Flask会自动处理常见的错误响应。HTTP错误对应的异常类在Werkzeug的werkzeug.exceptions模块中定义,抛出这些异常即可返回对应的错误响应。如果你想手动返回错误响应,更方便的方法是使用Flask提供的abort()函数。

在abort()函数中传入状态码即可返回对应的错误响应,下面的视图函数返回404错误响应:

from flask import Flask, abort

...

@app.route('/404')
def not_found():
    abort(404)

提示 abort()函数前不需要使用return语句,但一旦abort()函数被调用,abort()函数之后的代码将不会被执行。

附注 虽然我们有必要返回正确的状态码,但这并不是必须的。比如,当某个用户没有权限访问某个资源时,返回404错误要比403错误更加友好。  

本文基于Flask Web开发实战2章《Flask与HTTP》删减改写而来,完整的章节目录请访问本书主页http://helloflask.com/book查看。

《Flask Web开发实战》勘误、源码等资源索引

这篇文章列出了本书的勘误、源码等相关资源。衷心的希望这本书能够帮助到你,同时也为书中包含的错误为你带来的不便说声抱歉。

勘误 & FAQ & 可改进实现 & 版本更新记录

在本书的Meta仓库helloflask(https://github.com/greyli/helloflask)中,你可以找到下列资源:

实战项目源码

本书第7~11章的5个实战项目的源码地址如下:
这些项目的介绍以及在线Demo链接可以在本书主页找到。

其他相关资源

本书附带的其他相关资源如下:

HelloFlask.com

helloflask.com是本书的衍生站点,以上的资源信息都可以在本书的主页http://helloflask.com/book找到。

问题与错误反馈

如果你发现了书中的错误和不足,或是有和书相关的问题和反馈,根据类别,你可以分别选择下面的途径告诉我:
  • 书中包含的笔误、错误和不足的反馈:在helloflask仓库创建issue或PR
  • 各个项目的bug:在各个项目所在的仓库创建issue或PR
  • 其他问题、建议和批评:直接发邮件给我(withlihui@gmail.com),或是发送知乎私信、新浪微博私信、Twitter私信给我(邮件可以被我最快看到)

Powered by This Book

如果你通过阅读整本书编写了自己的小项目,欢迎通过各种方式告诉我,我会把你的项目添加到本书主页中的“Powered by This Book”栏目。
 
最后,欢迎大家在新浪微博Twitter上关注我,以便及时获取本书的最新动态。