原文地址:[Python web 开发实战]
安装 Flask
我们先安装 Flask
1 | pip install Flask |
从 Hello World 开始
我们从最小的应用开始:
1 | # coding:utf-8 |
启动它:
1 | > python hello.py |
打开浏览器,访问 "http://127.0.0.1:9000/"
,就可以看到熟悉的 "Hello world!"
了.
我们来深入的按行解析这段代码及其背后发生的事情:
第1行代码,
"# coding=utf-8"
是声明 Python 源文件编码的语法。该编码信息后续会被 Python 解析器用于解析源文件。如果没有特殊原因,应该统一地使用 utf-8,而不要使用 gb18030,gb2312 等类型。为了节省篇幅,之后的实例都不再写出这个声明。第2行代码,引入 Flask 类,Flask 类实现了一个 WSGI 应用。
第3行代码,app 是 Flask 的实例,它接收包或者模块的名字作为参数,但一般都是传递
__name__
。让flask.helpers.get_root_path
函数通过传入这个名字确定程序的根目录,以便获得静态文件和模板文件的目录。第4~6行代码,使用
app.route
装饰器会将 URL 和执行的视图函数的关系保存到app.url_map
属性上。处理 URL 和视图函数的关系的程序就是路由,这里的视图函数就是hello_world
。第7行代码,使用这个判断可以保证当其他文件引用这个文件的时候(例如:
"from hello import app"
) 不会执行这个判断内的代码,也就是不会执行app.run
函数。第8行代码,执行
app.run
就可以启动服务了。默认 Flask 只监听本地127.0.0.1
这个地址,端口为5000
。通过--host
和--port
参数可以指定监听哪个地址以及端口。服务启动后,会调用werkzeug.serving.run_simple
进入轮询,默认使用单进程单线程的werkzeug.serving.BaseWSGIServer
处理请求,实际上还是是使用标准库BaseHTTPServer.HTTPServer
,通过select.select
做0.5
秒的"while True"
的事件轮询。当我们访问"http://127.0.0.1:9000/"
,通过app.url_map
找到注册的"/"
这个URL 模式,就找到了对应的hello_world
函数执行,返回"Hello World!"
,状态码为200
。如果访问一个不存在的路径,如访问"http://127.0.0.1:9000/a"
,Flask 找不到对应的模式,就会向浏览器返回"Not Found"
,状态码为404
。
这里需要说明的是,默认的 app.run
的启动方式只适合调试,不要在生产环境中使用,生产环境应该使用 Gunicorn
或者 uWSGI
。
其他的 werkzeug 自带类型还包括 ThreadedWSGIServer
和 ForKingWSGIServer
.
配置管理
复杂的项目需要配置各种环境。如果设置项很少,可以直接硬编码进来,比如下面的方式:
1 | app = Flask(__name__) |
app.config
是flask.config.Config
类的实例,继承自 Python 内置数据结构 dict
,所以可以使用 update
方法:
1 | app.config.update( |
app.config
内置的全部配置变量可以参看 Builtin Configuration Values
(http://bit.ly/28UUgW3
)。如果设置选项很多,想要几种管理设置项,应该将他们存放到一个文件里面。app.config
支持多种更新配置的方式。假设现在有个叫做 settings.py
的配置文件,其中内容如下:
1 | A = 1 |
可以选择如下三种方式加载
- 通过配置文件加载
1 | # 通过字符串的模块名字 |
- 通过文件名字加载。直接传入文件名字,但是不限于只使用 .py 后缀的文件名。
1 | app.config.from_pyfile('settings.py', silent=True) |
- 通过环境变量加载。这种方式依然支持
silent
参数,获得路径后其实还是使用from_pyfile
的方式加载
1 | export YOURAPPLICATION_SETTINGS='settings.py' |
调试模式
虽然 app.run 这样的方式适用于启动本地的开发服务器,但是每次修改代码后都要手动重启的话,既不方便也不够优雅。如果启用了调试模式,服务器会在代码修改后自动重新载入,并在发生错误时提供一个能获得错误上下文及可执行代码的调试页面。
有两种途径来启用调试模式
- 直接在应用对象上设置
1 | app.debug = True |
- 作为 run 的参数传入
1 | app.run(debug=True) |
需要注意的是,开启调试模式会成为一个巨大的安全隐患,因此它绝对不能用于生产环境中。
Werkzeug 从 0.11 版本开始默认启用了 PIN(全称 Personal Identification Number)码的身份验证,旨在让调试环境下的攻击者更难利用调试器。启动程序时可以看到类似的启动提示:
1 | % python chapter3/section1/hello.py |
当程序有异常而进入错误堆栈模式,第一次点击某个堆栈想查看对应变量的值得时候,浏览器会弹出一个要去你输入这个 PIN 值得输入框。这个时候需要在输入框中输入 122-287-384,然后确认,Werkzeug 会把这个 PIN 作为 cookie 的一部分存储起来(失效时间默认是8小时),失效之前不需要重复输入。而这个 PIN 码攻击者是无法知道的。
当然,也可以使用指定 PIN 码的值:
1 | % WERKZEUG_DEBUG_PIN=123 python chapter3/section1/hello.py |
动态 URL 规则
URL 规则可以添加变量部分,也就是将符合同种规则的 URL 抽象成一个 URL 模式,如 /item/1/, /item/2/, /item/3/......
假如不抽象,我们就得这样写:
1 |
|
正确的用法是:
1 |
|
尖括号中的内容是动态的,凡是匹配到 /item/
前缀的 URL 都会被映射到这个路由上,在内部把 id
作为参数而获得。
它使用了特殊的字段标记 <variable_name>
,默认类型是字符串,如果需要指定参数类型需要标记成 <converter:variable_name>
这样的格式,converter
有下面几种:
- string: 接受任何没有斜杠 ”/“ 的文本(默认);
- int: 接受整数;
- float: 同 int,但是接受浮点数;
- path: 和默认的相似,但也接受斜杠;
- uuid: 只接受 uuid 字符串;
- any: 可以指定多种路径,但是需要传入参数。
1 | app.route('/<any(a, b):page_name>/') |
如果不希望定制子路径,还可以通过传递参数的方式。比如 /people/?name=a, /people/?name=b, 这样就可以通过 “name = request.args.get(‘name’)” 获得传入的 name 值。
如果使用 POST 方法,表单参数需要通过 request.form.get(‘name’) 获得.
自定义 URL 转换器
Reddit 可以通过在 URL 中用一个加号(+
) 隔开各个社区名字,方便同时查看来自多个社区的帖子。比如访问 "http://reddit.com/r/flask+lisp"
,就可以同时看 flask 和 lisp 两个社区的帖子。我们自定义一个转换器来实现这个功能,它还可以设置所使用的分隔符,不一定要用加号 "+"
1 | import urllib |
这样我们访问 “/list1/a+b” 和 “/list2/a|b” 就能实现同样的功能了。自定义转换器需要继承至 BaseConverter
,要设置 to_python
和 to_url
两个方法。
- to_python: 把路径转换成一个 Python 对象
- to_url:把参数转换成为符合 URL 的形式
HTTP 方法
HTTP 有多个访问 URL 方法,默认情况下,路由只回应 GET 请求,但是通过 app.route
装饰器传递 methods
参数可以改变这个行为:
1 |
如果存在 GET,那么也会自动地添加 HEAD 方法,无需干预。它会确保遵照 HTTP RFC(描述 HTTP 协议的文档)(http://bit.ly/2932liA
)处理 HEAD 请求,所以你完全可以忽略这部分的 HTTP 规范。从 Flask 0.6 起,它也实现了 OPTIONS 的自动处理。
下面简要介绍 HTTP 方法和使用场景:
- GET:获取资源,GET 操作应该是幂等的。
- HEAD:想要获取信息,但是只关系消息头,应用应该像处理 GET 请求一样来处理它,但是不返回实际内容;
- POST:创建一个新的资源;
- PUT:完整的替换资源或者创建资源。PUT 操作虽然有副作用,但应该是幂等的;
- DELETE:删除资源。DELETE 操作有副作用,但也是幂等的;
- OPTIONS:获取资源支持的所有 HTTP 方法;
- PATCH:局部更新,修改某个已有的资源。
幂等表示在相同的数据和参数下,执行一次或多次产生的效果是一样的。
唯一 URL
Flask 的 URL 规则基于 Werkzeug 的路由模块。这个模块背后的思想是基于 Apache 以及更早的 HTTP 服务器的主张,希望保证优雅且唯一的 URL。
举个例子:
1 |
|
上述例子很像一个文件系统中的文件夹,访问一个结尾不带斜线的 URL 会被重定向到带斜线的规范的 URL 上去,这样也有助于避免搜索引擎索引同一个页面两次。
再看一个例子:
1 |
|
URL 结尾不带斜线,很像文件的路径,但是当访问带斜线的 URL (/about/) 会产生一个 404 “Not Found” 错误。
构造 URL
用 url_for 构建 URL,它接受函数名作为第一个参数,也接受对应的 URL 规则的变量部分的命名参数,未知的变量部分会添加到 URL 末尾作为查询参数。构建 URL 而不选择直接在代码中拼接 URL 的原因有两点:在未来有更改的时候,只需要一次性修改 URL,而不用导出去替换;URL 构建会自动转义特殊字符和 Unicode 数据,这些工作不需要我们自己处理。
感受下面的这个例子:
1 | from flask import Flask, url_for |
test_request_context 帮助我们在交互模式下产生请求上下文
执行它:
1 | % python chapter3/section1/url.py |
跳转和重定向
跳转(状态码 301)多用于旧网址在废弃前转向新网址以保证用户的访问,有页面被永久性移走的概念。重定向(状态码 302)表示页面是暂时性的跳转。但是也不建议经常性使用重定向。在 Flask 中它们都是通过 flask.redirect
实现的:
1 | redirect(location) # 默认是 302 |
Flask 还支持 303,305,307 重定向,但是较少被用到。
基于前面所讲的内容,我们来看一下更全面的例子。首先是存放配置的 config.py
:
1 | DEBUG = False |
local_settings.py
文件是可选存在的,它不进入版本库。这是常用的通过本地配置文件重载版本库配置的方式。
基于上面所讲的内容,我们来看一个更复杂的应用 simple.py
1 | from flask import Flask, request, abort, redirect, url_for |
这个例子有如下细节:
- 第4行代码,访问 /people 的请求会被 301 跳转到 /people/ 上,保证了 URL 的唯一性。
- 第9行代码,request.headers 存放了请求头的头信息,通过它可以获取 UA 值。
- 第13行代码,request.method 的值就是请求的类型。
- 第25行代码,执行 abort(401) 会放弃请求并返回错误代码 401,表示禁止访问。之后的语句永远不会被执行。
- 第28行代码,能使用 debug=app.debug 是因为 flask.config.ConfigAttribute 在 app 中做了配置代理,目前存在的配置代理项有:
- app.debug -> DEBUG
- app.testing -> TESTING
- app.secret_key -> SECRET_KEY
- app.session_cookie_name -> SESSION_COOKIE_NAME
- app.permanent_session_lifetime -> PERMANENT_SESSION_LIFETIME
- app.use_x_sendfile -> USE_X_SENDFILE
- app.logger_name -> LOGGER_NAME
上面例子中的 app.debug 其实就是 app.config[‘DEBUG’]。
响应
视图函数的返回值会被自动转换为一个响应对象, 转换的逻辑如下:
- 如果返回的是一个合法的响应对象,它会从视图直接返回。
- 如果返回的是一个字符串,会用字符串数据和默认参数创建以字符串为主体,状态码为 200,MIME 类型是 text/html 的 werkzeug.wrappers.Response 响应对象。
- 如果返回的是一个元组,且元组中的元素可以提供额外的信息。这样的元组必须是(response, status, headers)的形式,但是需要至少包含一个元素。status 值会覆盖状态代码,headers 可以是一个列表或字典,作为额外的消息头。
- 如果上述条件均不满足,Flask 会假设返回值是一个合法的 WSGI 应用程序,并通过 Response.force_type(rc, request, environ) 转换为一个请求对象。
下面的视图函数:
1 |
|
可以改成如下显式地调用 make_response 的方式
1 |
|
第二种方法很灵活,可以添加一些额外的工作,比如设置 cookie,头信息等。
API 都是返回 JSON 格式的响应,需要包装 jsonify。可以抽象一下,让 Flask 自动帮我们做这些工作(app_response.py)
1 | from flask import Flask, jsonify |
启动它之后,就可以在另一个终端看看自定义头信息的效果了。看效果之前,先安装 httpie(https://github.com/jkbrzt/httpie
):
1 | pip install httpie |
httpie 是一个使用 Python 编写的,提供了语法高亮,JSON 支持,可以替代 curl 的工具,它也可以方便地集成到 Python 项目中。
现在请求 /custom_headers:
1 | http http://0.0.0.0:9000/custom_headers |
视图中也可以直接指定状态字符串,如使用 ‘201 CREATED’ 替代数字的 201.
静态文件管理
Web 应用大多会提供静态文件服务以便给用户更好的访问体验。静态文件主要包含 CSS 样式文件,JavaScript脚本文件,图片文件和字体文件等静态资源。Flask 也支持静态文件访问,默认只需要在项目根目录下创建名字为 static
的目录,在应用中使用 "/static"
开头的路径就可以访问。但是为了获得更好的处理能力,推荐使用 Nginx 或者其他 web 服务器管理静态文件。
不要直接在模板中写死静态文件的路径,应该使用 url_for
生成路径。举个例子:
1 | url_for('static', filename='style.css') |
生成的路径就是 '/static/style.css'
。让然我们也可以定制静态文件的真实目录:
1 | app = Flask(__name__, static_folder='/tmp') |
那么访问 "http://localhost:9000/static/style.css"
,也就是访问 /tmp/style.css
这个文件。