年度归档:2018年

遗失的后记

今天拿到书,才偶然发现编辑把我的后记删掉了……补在这里,请读完最后一页后来看。

《Flask Web开发实战》后记:

经过这一段漫长的Flask之旅,你应该收获了不少东西。虽然本书结束了,但你的学习还没有止步。希望这本书可以让你的某些想法走进现实,带给人们一些特别的记忆。也希望你可以慷慨的分享你的代码、经验和思想,因为你正和其他人一样,用你的方式改变着这个世界。但愿这本书帮到了你,祝你好运!

李辉 

2018年4月1日

《Flask Web开发实战》第二部分项目Demo和源码上线

很抱歉,因为电子书突然提前上架,一些进度被拖延了,现在终于把所有项目的源码都推送到GitHub了(如果你不方便访问GitHub,本书主页上提供了这些项目的源码合集文件下载)。

下面是这些项目的源码和Demo链接。关于这些项目的截图和功能介绍参见《Flask Web开发实战》中的示例程序们或本书主页(helloflask.com/book)。

第1~6章、13章:HelloFlask

第7章:留言板 – SayHello

Say hello to the world.

第8章:个人博客 – Bluelog

A blue blog.

第9章:图片社交网站 – Albumy

Capture and share every wonderful moment.

第10章:待办事项程序 – Todoism

We are todoist, we use todoism.

第11章:在线聊天室 – CatChat

Chatroom for coders, not cats.

提示 在线Demo部署在DigitalOcean的廉价主机上,某些地区或在特定时段可能会无法访问,请尝试使用VPN。另外,在线Demo关闭了部分功能。建议根据书中第二部分每章开始的操作指导在本地运行程序。

特别说明(给使用Windows系统的Python2.7用户)

Werkzeug当前版本(14.2)存在一个Bug,当在Windows系统下使用Python2开启调试模式时,重载器会因为环境变量FLASK_ENV的编码问题而出现TypeError异常。这个Bug已在master分支修复(话说定位这个Bug花了我很长时间),预计在纸书正式发售前会发布Werkzeug 0.15版本。

目前,临时的解决方案有修改Werkzeug源码、修改python-dotenv源码、从GitHub上的master分支更新Werkzeug等,但这些方法都太麻烦。我建议你临时不开启调试模式来避免这个异常出现,也就是在.flaskenv文件中将FLASK_ENV定义那一行注释掉(使用#号),比如:

# FLASK_ENV=development

等到Werkzeug 0.15发布后,我会发一篇文章通知大家更新本地依赖,并给出具体的更新方法。

致购买《Flask Web开发实战》电子版的读者

按照编辑的说法,电子书是要和纸书一起发售的。没想到的是,纸书还在印刷的时候,电子书突然就在24号上架了……这个“突袭”带来了一些问题,这篇文章汇总了这些问题及对应的解决方法。很抱歉这些问题为你带来了不便!

电子书亚马逊链接:https://www.amazon.cn/dp/B07GST8Z8M

豆瓣阅读链接:https://read.douban.com/ebook/56335667/

排版错误

或许是因为排版人员不够专心,又或者是软件问题,电子书存在一些排版问题:

  • 文本中所有的半角括号被转换为全角括号
  • 类似“python -m”命令中的空格被去掉
  • 部分代码缩进错乱
  • 部分字符缺失

我这几天会大致的过一遍电子书,汇总出所有的排版问题,然后尽快推送更新。

你可以访问勘误文件查看完整的勘误列表。

运行程序时出现TypeError异常(针对在Windows系统使用Python2的用户)

更新 python-dotenv 到最新版本即可:

$ pip install -U python-dotenv

Flask 0.12.2版本发现安全漏洞,请考虑升级

因为Flask-CKEditor的示例程序目录下包含一个旧的requirements.txt文件,其中Flask版本被固定为0.12.2,推动代码到GitHub时,触发了内置的依赖安全提示,进而了解了一下这个关于Flask 0.12.2版本的漏洞。

漏洞描述

这个漏洞(CVE-2018-1000656)四天前(8月20号)被发布在NVD(National Vulnerability Database,国家漏洞数据库)上,漏洞描述如下:
The Pallets Project flask version Before 0.12.3 contains a CWE-20: Improper Input Validation vulnerability in flask that can result in Large amount of memory usage possibly leading to denial of service. This attack appear to be exploitable via Attacker provides JSON data in incorrect encoding. This vulnerability appears to have been fixed in 0.12.3.
大致的翻译如下:
Pallets项目组开发的Flask 0.12.3及以下版本包含CWE-20类型的漏洞:不合适的输入验证漏洞。这个漏洞将会导致大量内存占用,可能会导致拒绝服务。攻击者可以通过提供使用了错误编码的JSON数据来进行攻击。这个漏洞已经在0.12.3版本中修复(#2691)。

应对措施

对于这个漏洞,你可以通过升级来进行防范。如果你打算使用最新版本(Flask 1.0.2),可以使用下面的命令更新(参见这篇文章了解Flask 1.0版本包含哪些主要变化):
$ pip install -U flask
如果你使用Pipenv,则可以使用下面的命令:
$ pipenv update flask
如果你还没有准备好使用最新版本,可以升级到0.12.3版本
$ pip install flask==0.12.3
然后更新requirements.txt:
flask ~> 0.12.3
如果使用Pipenv,则使用下面的命令:
$ pipenv install flask==0.12.3

附注

文如其人

偶然看到读库网站上的投稿要求——《我们需要什么样的稿件》,觉得非常有意思。与其说是筛选稿件,倒不如说是在筛选作者,因此标题也可以看做《我们需要什么样的作者》。进一步说,这种筛选也会反向作用到读者身上。在大多数媒体和网站都在努力扩大受众群体时,这种主动筛选难能可贵。

……

如果您的文章中,经常出现“我感到……”“我想……”“我觉得……”这样的句式,则基本不符合我们的选稿标准。说得再严苛一些,一篇文章中如果三分之一以上的句子主语都是“我”,就不会出现在《读库》里。

如果您的文章中定语太多,或是结论评价类的形容词太多,则不符合我们的选稿标准。我们只希望你把你看到的记录下来,至于它在你心中泛起的涟漪,希望能通过你的记录和传递,荡漾到读者心中,而描述涟漪本身,是我们不需要的。

如果你的文章试图以小见大,或直接以大见大,以探讨人生终极问题和家国大计为己任,我们也要对您敬而远之。我们要的不是高度的概括性,而是极度的细部展示。具体地说,如果一篇三万字的稿子,其第一段提到的某件事、某个人,有可能被写出三万字,那么这篇稿子就显得大而无当了。

如果你试图充分展示自己的写作才华,我们也要对您敬而远之。《读库》中受人好评的文章,基本没有读者会说“这个作者太有才了”。我们想展示的,是才华的另一面:老老实实把自己想写的东西交代清楚,认认真真把自己感兴趣的东西琢磨个底儿掉,切切实实尊重读者的习惯和判断。

“用写一本书的力气来写一篇文章”,我们最需要这样的稿件。希望您将投稿的目光投向《读库》时,想的不是“我要写一篇好文章”,而是“我要把一件事情搞清楚,并记下来”。当动笔的时候,你心里有着一定的谦卑和热忱。庶几可矣。

《Flask Web开发实战》签名版开始预售

《Flask Web开发实战》即将下厂印刷,如果想要购买作者签名版,可以访问http://helloflask.com/book/signed。本书定价129,电商平台预计价格为99,签名版为109。

注意,此商品为预售,无现货,具体发货时间取决于本书正式发售日期。更多信息请访问预售页面查看。

Flask-Origin:Flask 0.1版本源码注解

本项目是《Flask Web开发实战》的衍生品。在本书第16章的前半部分,为了让读者快速对Flask的源码结构建立一个初步的认识(以便阅读后面的内容),推荐读者阅读0.1版本的源码。

本项目对0.1版本Flask源码(项目根目录下的flask.py脚本)中的注释和文档字符串进行了翻译,并在必要的地方添加了一些额外的注解,以便于阅读和理解。

项目地址:https://github.com/greyli/flask-origin

欢迎fork项目进行补充和纠错。

阅读前的准备

为了更容易理解Flask的实现原理,你需要对WSGI协议以及HTTP协议有一些了解,建议先简单浏览下面的基本知识:

进一步阅读

Flask内部实现大量依赖于Werkzeug,包括请求和响应对象,路由匹配,URL生成等等,你可以阅读Werkzeug的文档来深入了解这些内容的具体实现。另外,如果你对模板渲染部分的内容感兴趣,也可以考虑阅读Jinja2文档:

注意:新版本的Werkzeug和Jinja2已经发生很大的变化,0.1版本的Flask对应的Werkzeug源码版本为0.6.1,对应的Jinja2源码版本为2.4。上述文档链接分别为0.14和2.9版本,请谨慎参考。

新版本的Flask中如何启动开发服务器和开启调试模式

从Flask 0.11版本开始,官方就建议使用flask run命令来取代app.run()方法运行开发服务器。尽管如此,两年多过去了,仍然有大量新发布的文章和教程在示例中使用app.run()方法启动程序。类似的,虽然内置的命令行支持已经非常完善,但还有很多人在使用Flask-Script。

Added flask and the flask.cli module to start the local debug server through the click CLI system. This is recommended over the old flask.run() method as it works faster and more reliable due to a different design and also replaces Flask-Script.

Flask Changelog 0.11

不得不承认,在某些特殊场景下,app.run()更加方便,比如创建Flask命令在附加Werkzeug提供的性能分析中间件后启动程序,这时通过app.run()可以直接在脚本内启动程序。但是在大多数情况下,flask run更能胜任启动开发服务器的工作。而且,在大型项目中,使用app.run()需要你在项目根目录单独创建一个启动脚本,flask run则没有这个要求;在单脚本程序中,使用flask run也可以省掉脚本末尾的两行代码。

注意 这两种方法都只是用来启动内置(Werkzeug提供)的开发服务器,仅适用于开发用途。在生产环境下,应该使用性能更好,更加完善的开发服务器,比如Gunicorn、uWSGI等。

不同组织形式的程序的启动方式

下面我们来了解一下使用flask run启动开发服务器时在几种方式。

简单的单脚本程序

如果脚本命名为app.pywsgi.py,那么在包含程序脚本的目录下直接调用flask run即可:

$ flask run

Flask会自动探测找到脚本中的程序实例并启动。如果脚本命名为其他名称,比如hello.py,那么需要将脚本名写入环境变量FLASK_APP,然后再调用flask run命令:

$ export FLASK_APP=hello
$ flask run

提示 在Windows系统下,你需要使用set命令来设置环境变量,比如 > set FLASK_APP=hello,后面的命令亦同。

使用包组织的程序

这种情况下,可以将包含程序实例的对应模块的路径写入FLASK_APP

$ export FLASK_APP=my_pkg.app
$ flask run

通常情况下,我们会在包内的__init__.py文件中创建程序实例,所以这时可以直接将包名称写入FLASK_APP

$ export FLASK_APP=my_pkg
$ flask run

使用工厂函数创建程序实例的程序

因为Flask会自动探测程序实例,所以使用工厂函数创建程序实例时不需要进行额外设置。具体来说,Flask会在FLASK_APP变量存储的对应模块/包构造文件中寻找名为create_appmake_app的函数,并调用这个函数来创建一个程序实例。

为了让你的程序能够被探测到,工厂函数的名称需要命名为create_appmake_app,而且要确保工厂函数接受默认值参数。这时启动开发服务器的方式仍然不变:

$ export FLASK_APP=my_pkg
$ flask run

如果你的工厂函数接受的参数不是默认参数,或者你想详细定义调用工厂函数的方式,那么也可以通过FLASK_APP环境变量来定义:

$ export FLASK_APP="my_pkg:create_app('development')" 
$ flask run

提示 Flask的FLASK_APP还接受其他形式的输入值,你可以参考文末给出的文档相关部分链接了解完整内容。

如何避免重复设置FLASK_APP环境变量

在上面的几种方式中,除了包含程序实例的程序脚本命名为app.pywsgi.py的情况外,都需要设置FLASK_APP环境变量。有没有办法避免重启电脑或是新打开命令行会话时重复输入FLASK_APP呢?当然。Flask提供了对一个常用的Python虚拟环境管理工具python-dotenv的支持,我们需要先安装它:

$ pip install python-dotenv

当python-dotenv安装后,执行flask run命令会首先将项目根目录下的.env.flaskenv文件中的环境变量写入。所以,你可以将FLASK_APP写在这两个文件中。按照约定,.env存储包含敏感数据的环境变量,这个文件需要加入到.gitignore中以避免提交到Git仓库中;而.flaskenv时Flask特别支持的文件,这个文件则用来存储和Flask相关的环境变量,比如FLASK_ENVFLASK_DEBUG等,所以我们可以把FLASK_APP写到这个文件中:

FLASK_APP=my_pkg

现在,我们可以仅通过一个命令来启动开发服务器:

$ flask run

使用flask run时如何开启调试模式?

在使用app.run()方法时,我们会通过将debug参数设为True来开启调试模式。而当使用flask run时,则需要通过FLASK_ENV环境变量来设置调试模式。默认情况下,FLASK_ENV的值为production,在开发时我们可以将其设为development来开启调试模式。

同样的,为了避免重复写入这个环境变量,我们也将其写到.flaskenv中:

FLASK_ENV=development

提示 目前已不推荐使用FLASK_DEBUG来开启调试模式,当FLASK_ENV的值为development时调试模式会自动开启。

使用flask run时如何自定义主机和端口

在通过flask run启动开发服务器时,你可以通过命令行选项来自定义监听的主机和端口,示例如下:

$ flask run --port 5001

下面的示例同时指定了端口和主机:

$ flask run --host 0.0.0.0 --port 5001

另外,Flask还支持通过环境变量来定义命令选项,支持的环境变量名称模式为“FLASK_命令_选项”。比如,如果你想设置端口,那么可以定义FLASK_RUN_PORT环境变量,作用和传入--port选项相同。

启动包含程序上下文的Python Shell

你可以通过flask shell命令来启动一个激活了程序上下文的Python Shell,而不是使用python命令:

$ flask shell

app.run()的未来

从0.11版本到现在的1.0.2版本,app.run()始终处于不建议使用状态,而且Flask的命令行系统、flask run命令的程序探测都在逐渐完善,我觉得未来也许会正式”deprecate“这个app.run()方法。不过,因为某些特殊用途仍然需要使用app.run(),未来的变化还不好说。而且,Miguel Grinberg提交了1个PRapp.run()间接调用flask run,如果这个PR被合并,也许app.run()将会重回正轨。

就目前来说,flask run要远比app.run()更加方便、好用、简洁、直观,准备好了吗?穿上新衣服吧。

相关链接

糟糕的提问

维护开源项目最怕的大概就是遇见这种提问者:不知道自己要问什么(XY问题),不提供足够的信息(或是提供不全面的甚至是错误的信息),不思考你给出的观点和建议,只希望你给出最终的解决代码。更要命的是,他们还会不断提供不相干的信息和更多的无关问题,给你制造烟雾弹。

https://github.com/greyli/flask-ckeditor/issues/8

Flask test_client()测试客户端为勾选框传递布尔值数据

今天写单元测试发现了一个常见的问题,即测试时发送POST请求时如何传入布尔值数据(勾选框字段值)?答案是:你没法直接传递布尔值。其实这个答案相当显而易见,客户端当然没法向服务器端发送Python类型的数据,数据的转换是在接受到请求数据后在服务器端进行的。之前在不借助Flask-WTF/WTForms,手动编写表单并处理时就已经注意到了这个问题,不过在测试中不太容易想到。

首先,我们需要了解一下勾选框(<input type="checkbox">)提交的行为:

  • 如果没有勾选,那么勾选框字段的值为空值,而且这个字段不会被序列化到请求中;在服务器端,WTForms会将其转换为False
  • 如果勾选框被勾选,那么传入服务器端的数据会是该字段value属性的值,如果value属性的值为空,那么则提交字符串"on";在服务器端,WTForms会将其转换为True

也就是说,勾选框的数据只要不为空,WTForms就会将其转换为True。所以,在测试中,如果你想让勾选框的值最终转换为True,那么就传入任意字符串;反之则传递空字符串或直接不加入该字段。下面是传入空字符串的示例:

def test_privacy_setting(self):
    self.login()
    response = self.client.post(url_for('user.privacy_setting'), data=dict(
        public_collections='',  # <--
    ), follow_redirects=True)

    user = User.query.get(1)
    self.assertEqual(user.public_collections, False)

顺便说一句,基于勾选框的提交行为,如果没有使用Flask-WTF/WTForms,那么在手动处理提交数据的时候也要进行相应的处理:没有在request.form中获取到勾选框字段(比如,request.form.get('remember')会是None),即表示没有勾选,那么就转换为False;勾选框字段一旦出现,那么就表示勾选,转换为True

在Python Selenium中为Chrome和Firefox浏览器开启headless模式

我们通常会使用Selenium编写UI测试,为浏览器开启Headless模式(执行操作时不显示GUI窗口)会很方便。最新版本的Chrome和Firefox中,均已支持headless模式。

在Selenium中,为这两个浏览器开启headless模式的方式基本相同:

Chrome:

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('headless')
driver = webdriver.Chrome(options=options)

Firefox:

from selenium import webdriver

options = webdriver.FirefoxOptions()
options.add_argument('headless')
driver = webdriver.Firefox(options=options)

我提交的PR#5120添加了和Chrome相同的导入接口,如果你使用Selenium小于3.8.0版本,则需要将上面的webdriver.FirefoxOptions()替换为webdriver.firefox.options.Options()

另外,你也可以使用环境变量MOZ_HEADLESS 来为Firefox开启headless模式:

import os
from selenium import webdriver

os.environ['MOZ_HEADLESS'] = '1'  # <- this line
driver = webdriver.Firefox()

本文基于我在Stack Overflow的这篇回答:https://stackoverflow.com/a/47481793/5511849

Flask Web开发实战番外

《Flask Web开发实战》删减下近8万字的内容,有时间我会把其中有价值的内容整理成文章发布出来。