写一本Flask入门教程

第一次萌生出这个念头是在2016年,刚开始写知乎专栏《Hello, Flask!》的时候。写了几篇文章后,原来计划的系统性的教程就变成了一堆零散主题的文章。一年后,又有过一次写教程的念头,那是在《用Flask实现豆瓣相册(一)》;只不过,刚刚完成第一篇,就开始写《Flask Web开发实战》了。书写完到现在,又是一年过去了。

为什么要写一个教程

《Flask Web开发实战》作为一本书,必然要尽可能的包含详尽的相关知识。而有的人更希望能有一个简单的入门教程,用来快速对Python Web开发建立一个基本的概念,为后续的学习打下基础。如果你在阅读《Flask Web开发实战》的时候感到吃力,那么这个入门教程就是为你准备的。

教程的名字暂定为《Flask入门教程:使用Python和Flask开发你的第一个Web程序》。

暂定的目录如下:

  • 准备工作
  • Hello, Flask!
  • 模板和静态文件
  • 表单
  • 数据库
  • 用户认证
  • 组织你的代码
  • 测试
  • 部署上线

新的编写形式

这个教程采用了一种新的编写模式,我计划在教程里完整的呈现开发一个Flask程序的基本过程,包括每一个需要执行的命令,每一个文件的编写内容。因此,它不会像一本书一样包含大量解释和提示,除了开发流程外,尽量只保留入门所需的最简信息量,同时优化所有术语的描述。

作为阅读者,则需要自己动手敲出教程里的每一个命令和每一行代码,最终部署一个完全由自己编写的Flask程序。我想这个学习方式大概可以叫做“肌肉复制学习法”,或者是“自己动手跟着做一遍学习法” :p

通过自己动手开发一个程序,初学者可以对开发过程中涉及的概念建立一些自己的理解,后续的深入学习可以进一步加深或是纠正这些理解。

这个想法参考了ZED A. SHAW的《Learn X the Hard Way》系列。如果你对于这个教程的形式设计和内容安排有什么想法和建议,欢迎评论提出来。

写作计划

也许有人已经开始期待了,不过很抱歉,这个教程还没有诞生……好消息是,我已经开始写了,预计会在11月底完成所有内容。教程会连载在专栏,到时也会提供各类电子书文件的下载。

相关链接

 

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

写作《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个程序比较简单,可以排除在外):

PyCon China 2018:自由的Flask

这是我在PyCon China 2018的主题演讲。这个演讲并没有涉及太多复杂的内容,主要的目的还是想推介一下Flask,让更多的人能够了解和使用Flask。

演讲主题简介如下:

作为一个流行的Python Web框架,很多开发者都喜欢Flask的简洁和灵活,并且常常拿它和“笨重”的Django做比较。基于这些特点,我们可以说Flask是自由的,自由的Flask会让你的Web开发更加自由。具体来说,这里的自由表现在很多方面,包括程序功能的扩展、路由的定义、项目结构的组织以及程序模式的设计等。

相关资源:

参加北京 PyCon China 2018

说来惭愧,我在北京场开始前一个月才知道国内也有PyCon。9月16号,在聊天群里有朋友建议我去参加PyCon。从考虑去不去,到决定演讲主题,报名闪电演讲,再到变成主题演讲,只花了两天的时间。此时距离大会开始还有26天。

与此同时,新书《Flask Web开发实战》刚刚开始正式发售,有很多事情要做:整理新书的勘误、处理各个渠道读者的问题、写宣传新书的文章、处理淘宝上的盗版书、在知乎和v2ex送书并寄出,这些杂务花费了很多时间,导致幻灯片和对应的演讲稿在大会前一天晚上才最终定稿,原计划的多次试讲练习最终也只完成了1次。

因为练习太少,再加上很久没有演讲了,导致说错不少话,哈哈。演讲的过程也状态连连:幻灯片忘记打开注释窗口,忘记带水上去喝……不过,从后续的反馈来看,整体上来说还不错。我的演讲主题是《自由的Flask》,内容比较简单,主要还是想推介一下Flask,让更多的人了解它。

至于活动本身,对比往年的评价,今年可以说是最好的一届。无论如何,都要感谢组织者们的无私投入。但这并不意味着没有缺点,从我个人的体验来说,主要有这些不足:

  • 文件协作方式很糟糕,会场拍摄的照片没有发送给讲师,而演讲视频既没有在网络上公开分享,也没有发送给讲师,只是被几个提供录像服务的赞助商握在手里。

  • 各个会场的投影设备和尺寸不一,分会场A、B是16:9的电子屏幕,而分会场C则是4:3的投影。我的幻灯片一开始使用默认的4:3,后来问了组织方的老师,改成了16:9……

  • 场地和设备不好,尤其是 C 会场,因为是投影而不是电子屏幕,为了录制效果就关掉了讲台的灯,导致会场很黑,没有拍一张正常的照片。而录像也只录了黑漆漆的幻灯片,看不到讲师。

  • 形式太单调,可以在中间穿插一些聊天、座谈、编程等类似的活动,增加一些面对面的交流和互动,而不仅仅是严肃的讲和听。

除此之外,最开心的事情是和华章出版社的杨福川老师以及其他读者见了面,面对面的交流要比网络上的对话有趣的多。在我那场演讲,有一些我的读者来听,很感谢他们的支持!

下次有机会的话,希望能够带来一场更好的分享,也祝PyCon China能够越办越好。

如何向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开发实战》番外文章系列。

Pipenv使用时出现TypeError、ResourceWarning、或ImportError错误的解决方法

10月16日更新

Pipenv 10.13 据说已经修复所有相关问题,可以尝试更新:

$ python3 -m pip install --upgrade pip 
$ python3 -m pip install --upgrade pipenv

Pipenv 2018.7.1

Pipenv 2018.7.1版本和最新版本的pip不兼容,根据不同的命令和环境,会出现下面的异常:

  • TypeError: ‘module’ object is not callable (pipenv install)
  • ResourceWarning: Implicitly cleaning up … (pipenv install)
  • ImportError: cannot import name ‘get_installed_distributions’ (pipenv graph)

目前可以临时通过降级pip来解决:

$ python3 -m pip install pip==10.0.1

如果你使用Python2,则使用下面的命令:

$ python -m pip install pip==10.0.1

相关issue见#2924#2925#2944

Pipenv 2018.10.9

如果使用的是最新的2018.10.9版本,那么在Python 2.7下会出现下列异常:

  • ImportError: No module named weakref
  • ImportError: No module named shutil_get_terminal_size

可以临时通过手动安装这两个库来解决:

$ pip install backports.weakref backports.shutil_get_terminal_size

Pipenv 修复版本发布后

等到Pipenv发布新版本修复了这些问题后,可以通过下面的命令升级pipenv和pip:

$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pipenv

如果你在阅读《Flask Web开发实战》时还遇到了其他问题,可以先到FAQ中查看,没有解决可以在helloflask仓库创建issue。

欢迎来 PyCon China 2018 听我的演讲

下个月 14 号的 PyCon China 2018 北京场的分会场 C,我有一场关于 Flask 的演讲,主题是“自由的Flask”。因为能力有限,内容不会太过深入,但我会尽量让它有趣一点,介绍一下 Flask 灵活性在各个方面的表现。另外,我还准备了一些 Flask 贴纸,欢迎参加的同学来找我领取。日程和购票入口见这里

使用ngrok让你的本地Flask程序外网可访问

2022/4/3 Update:如果你在 macOS 上映射的 Flask 程序无法访问,可以参考这篇文章解决。

注:本文隶属于《Flask Web开发实战》番外文章系列,文章列表见《Flask Web开发实战番外文章索引》

在开发Web程序时,有时候会有这样的需求:

  • 让朋友可以访问到你本地运行的程序
  • 在本地测试各类服务(比如Telegram机器人、微信公众号等)
  • 有一台废弃的电脑,你想让它作为一台服务器来运行你的博客程序,或是运行其他简单的程序

外网可见

要实现上面的需求,我们就需要程序能够“外网可见”。在《Flask Web开发实战》第一章介绍启动程序时,我们提及了“外网可见”的内容。简单来说,当你将Flask服务器监听的主机地址设置为0.0.0.0时,就可以让服务器外网可见。不过,这里有一个前提,那就是你的服务器需要运行在拥有公网IP的主机上,因为我们开发用的电脑通常不会有公网IP,所以这里的外网只能是你的电脑所在的局域网,比如在客厅的电脑可以访问你的笔记本上运行的程序(通过你的笔记本的内网IP)。

事实上,借助内网穿透/映射工具,我们也可以让外网上的朋友访问到运行在你的笔记本上的程序。这些工具会为我们分配一个域名A,你只需要在本地运行程序,并建立映射,那么当其他用户(不仅是你客厅的电脑,还可能是北京的毛毛,或是美国的Peter)访问A网址时,内网穿透工具就会把请求转发到你的笔记本中,取回响应后再返回给用户。具体流程如下所示:

blank

左边是美国的Peter,右边是你

在这篇文章,我们会了解如何使用ngrok来实现这个目的。本文将会以一个简单的Flask程序作为示例,不过你也可以替换为任意的Web程序,比如使用Django、PHP或是JAVA等语言/框架编写的Web程序。

运行本地服务器

我们先来编写一个简单的Flask程序:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'It works!'

将上面的代码保存为app.py,然后打开一个命令行窗口,使用下面的命令运行这个程序:

$ flask run

如果还没有安装Flask,可以执行pip install flask命令进行安装(你也可以创建一个虚拟环境,推荐使用Pipenv)。

默认情况下,Flask内置的开发服务器会监听本地机的5000端口,你可以使用127.0.0.1:5000或localhost:5000访问程序。

安装和配置ngrok

ngrok支持三大主流操作系统,安装流程比较简单,如下所示:

  1. 访问https://ngrok.com/download
  2. 根据操作系统下载对应的压缩包
  3. 解压到合适的目录

压缩包里包含一个名为ngrok的二进制文件,我们打开一个命令行窗口,切换到这个文件所在的目录。现在可以先执行help命令来测试一下。在Windows下,你可以使用下面的命令:

> .\ngrok help

或是直接执行:

> ngrok help

在Linux/macOS下,则可以使用下面的命令:

$ ./ngrok help

建立映射/HTTP隧道

因为我们的Flask程序已经运行在本地机的5000端口,我们只需要启动ngrok服务,输入对应的端口即可建立映射,或者说建立一条HTTP隧道:

$ ./ngrok http 5000

附注 在Windows下可以使用ngrok http 5000命令。

输出的信息中包含ngrok为你随机分配的域名:

Session Status online
Account Grey Li (Plan: Free)
Version 2.2.8
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding http://d15a56b1.ngrok.io -> localhost:5000
Forwarding https://d15a56b1.ngrok.io -> localhost:5000

Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00

其中的https://d15a56b1.ngrok.io就是为你分配的可以外部访问的网址,所有发向这个网址的请求都会转发到你的本地机5000端口,即localhost:5000或127.0.0.1:5000。这时访问这个网站会看到我们上面程序中定义的输出“It works!”。

当有请求进入后,你可以在这里看到请求列表。另外,你也可以访问http://127.0.0.1:4040访问ngrok本地程序提供的Web监控页面。

注册账户与付费套餐

在上面的流程里我们并没有介绍注册账户,因为这是可选的。未注册时,你执行ngrok http命令的输出会和上面稍微有些不同,你会看到下面两行:

Session Status online
Session Expires 7 hours, 42 minutes

这是因为未注册账户每个会话只会维持8小时,过期后你需要重新启动。

注册用户没有这个限制,注册相当简单,这里不再赘述,注册完成后需要执行下面的命令连接本地ngrok程序:

$ ./ngrok authtoken <令牌值>

附注 Windows系统可以使用ngrok authtoken <令牌值>命令。

命令中的令牌值可以在注册后跳转的控制面板页面看到,如下图所示:

blank

你也可以访问https://dashboard.ngrok.com/auth查看。

每次建立映射,ngrok都会分配一个随机子域的网址,如果你想拥有一个固定的域名,则要升级套餐。升级套餐的好处很多,但如果只是临时测试用的话,免费账户或是不注册使用就足够了。

注:更多的功能介绍可以访问https://ngrok.com/product查看。文图均来ngrok.com

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

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

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

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

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

blank

这就是封面设计初稿:

blank

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

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

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

blank

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

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

blank

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

blank

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

blank

于是,《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上关注我,以便及时获取本书的最新动态。