Flask
快速入门¶
配置与惯例¶
Flask配置选项的默认值都是合理的,有一些是默认的,如templates和static默认在源码目录下,这些配置可以修改但一般并不需要。
Flask使用了Thread-Local变量,避免了同一request间的参数传递,这样很方便,但是使得程序依赖于合法的request请求,依赖于request的数据导致程序无法复用。
安装¶
依赖¶
Flask依赖Werkzeug和Jinja2。
Werkzeug是一个WSGI工具集。
Jinja2负责渲染模板。
virtualenv¶
sudo pip install virtualenv
mkdir myproject
cd myproject
virtualenv venv
. venv/scripts/activate
pip install Flask
HelloWorld¶
# 文件hello.py
from flask import Flask
# Flask通过__name__找到包路径及其下的static和templates目录,避免限制为当前目录
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
运行:
python hello.py
通过环境变量运行:
# windows下使用set设置环境变量
export FLASK_APP=hello.py
flask run
通过python module运行:
export FLASK_APP=hello.py
python -m flask run
访问:http://127.0.0.1:5000
定义HttpServer绑定IP¶
app.run(host='0.0.0.0', port=80)
flask run --host=0.0.0.0
调试模式(交互式调试器)¶
app.debug = True
app.run()
# 或
app.run(debug=True)
export FLASK_DEBUG=1
flask run
URL路由¶
基本示例:
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
return 'Hello World'
带变量的URL:
@app.route('/user/<username>')
def show_user_profile(username):
return 'User %s' % username
带变量的URL并转换数据类型:
int # 整形
float # 浮点型
string # 不接受斜线
path # 接受斜线的string
any # 任意
uuid # UUID
@app.route('/post/<int:post_id>')
def show_post(post_id):
return 'Post %d' % post_id
遵守唯一URL管理,避免重定向行为:
/projects/ # 若访问/projects会被Flask重定向到/projects/
/about # 若访问/about/会产生"404 Not Found"
建议使用带斜线的URL,保证URL唯一性,避免搜索引擎索引同一个页面两次
使用url_for()用处理函数逆向构造URL:
url_for的第一个参数是处理函数的名称,后面命名参数匹配的部分会应用到url,不符合的部分会作为查询参数。
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/')
def index: pass
@app.route('/login')
def login(username): pass
@app.route('/user/<username>')
def profile(username): pass
# 使用test_request_context()构造request
with app.test_request_context():
print(url_for('index')) # 输出:/
print(url_for('login')) # 输出:/login
print(url_for('login', next='/') # 输出:/login?next=/
print(url_for('profile', username='John Doe')) # 输出:/user/John%20Doe
# url_for默认生成相对地址,外部链接需要使用绝对地址
print(url_or('index', _external=True)) # 输出 http://host:port/
使用逆向构造URL的用途:
- 可以自动在Flask工程中查询URL
- Flask会自动转译特殊字符和Unicode
- 如果应用不位于URL根路径,url_for会自动处理
HTTP方法:
默认处理为GET请求,使用route装饰器的methods指定HTTP方法
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
do_the_login()
else:
show_the_login_form()
GET # 获取页面
HEAD # 获取信息,只关心消息头,Flask会自动按GET处理
POST # 发送信息,发送表单
PUT # 类似POST,但可以多次覆盖旧数据,POST只能触发一次
DELETE # 删除
OPTIONS # 查询URL支持哪些HTTP方法,Flask会自动处理
HTTP4和XHTML1只支持GET和POST,但是JavaScript和HTML5支持其他方法
静态文件¶
文件路径:包或者模块的所在目录中static文件夹
使用URL:/static
生成URL:url_for('static', filename='style.css'),static是特殊参数
模板渲染¶
模板路径¶
模块路径:
/application.py
/templates
/hello.html
包路径:
/application
/__init__.py
/templates
/hello.html
渲染:
from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None)
return render_template('hello.html', name=name)
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{name}}!</h1>
{% else %}
<h1>Hello World!</h1>
{% endif %}
在模板里可以让问request、session和g对象以及get_flashed_message()
模板继承可以使每个页面的header、navigation和footer保持一致。
变量中包含HTML会被转译,例如以上代码{{name}}中包含的HTML会被自动转译。
自动转译是默认对.html .htm .xml .html模板文件开启,模板字符串自动转译是关闭的。
使用|safe过滤器或flask.Markup可以标记字符串为安全。
from flask import Markup
# 标记为安全字符串
Markup('<strong>Hello %s!</strong>') % '<blink>hacker</blink>'
# 输出Markup(u'<strong>Hello <blink>hacker</blink>!</strong>')
# 手动转译
Markup.escape('<blink>hacker</blink>')
# 输出 Markup(u'<blink>hacker</blink>')
# 清除HTML TAG
Markup('<em>Marked up</em> » HTML').striptags()
# 输出 u'Marked up \xbb HTML'
访问Request数据¶
Request上下文与单元测试¶
Request使用时看起来是全局的,其实Request是线程(或协程)本地的
单元测试时没有请求,所以没有Request对象
使用test_request_context()环境管理器:
from flask import request
with app.test_request_context('/hello', method='POST')
assert request.path == '/hello'
assert request.method = 'POST'
或将整个WSGI环境传递给request_context()方法
from flask import request
with app.request_context(environ):
assert request.method == 'POST'
使用Reqeust对象¶
从POST方法的form中获取信息 request.form:
from flask import request
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'], request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
return render_template('login.html', error=error)
获取查询字符串 request.args:
# ?key=value
searchword = request.args.get('q', '')
获取json字段:request.args
获取header字段:request.headers
异常处理¶
若form中属性不存在则会抛出KeyError异常。不处理KeyError异常会显示一个"HTTP 400 Bad Request错误页面”。
上传文件 request.files¶
from flask import request
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
# 获取文件名
f.save('/var/www/uploads/' + secure_filename(f.filename))
上传文件需要HTML表单中设置enctype="multipart/form-data"
Cookies 读取与存储¶
from flask import request
# 获取cookie
@app.route('/')
def index():
username = request.cookies.get('username')
username = request.cookies.get['username']
# 设置cookie
@app.route('/')
def index():
# 如果不设置cookie只需要返回字符串,若要设置cookie需要自行构造response
# 若不想自行构造response,可以使用“延迟请求回调”
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp
重定向与错误¶
redirect() # 重定向到其他url
from flask import abort, redirect, url_for
@app.route('/')
def index():
return redirect(url_for('login'))
abort() # 发送错误
@app.route('/login')
def login():
abort(401)
this_is_never_executed()
定制错误页面:
from flask import render_template
# 返回模板
@app.errorhandler(404)
def page_not_found(error):
return render_templete('page_not_found.html'), 404
response¶
url处理函数的返回值会被自动转换为response对象。
如果返回值是字符串,会被转换为字符串为body、状态码为200 OK、MIME是text/html的response。
Flask返回值处理逻辑:
1. 如果返回的是一个合法的response对象,则从url处理函数直接返回。
2. 如果返回的是一个字符串,Flask会用字符串和默认参数创建response。
3. 如果返回一个tuple,格式为(response, status, headers),Flask会用tuple覆盖默认设置。tuple至少要有一个值。headers是一个list或dict。
4. 如果以上都不是,Flask会假设返回值是一个WSGi应用程序,并发起请求。
如果不想让Flask自动处理,可以使用make_response自行处理。
# 返回tuple
@app.errorhandler(404)
def not_found(error):
return render_template('error.html'), 404
# 自行处理
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp
会话 session¶
会话是在不同请求间存储特定用户的信息。
Flask使用Cookies字段存储Session,并对Cookies字段做了Hash校验,用户只能查看Cookies内容但不能修改。
from flask import Flask, session, redirect, url_for, escape, request
app = Flask(__name__)
app.secret_key = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
# 查询session
@app.route('/')
def index():
if 'username' in session:
return 'Logged in as %s' % escape(session['username'])
return 'You are not logged in'
# 插入session
@app.route('/login', methods=['GET', 'POSt'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form action="" method="post">
<p><input type=text name=username/></p>
<p><intpu type=submit value=Login></p>
</form>
'''
# 删除session
@app.route('/logout')
def logout():
session.pop('username', None)
return redirect(url_for('index'))
生成密钥:
import os
os.urandom(24)
闪现消息(Flashing)¶
闪现消息是在请求结束时响应给客户端的一种消息。
当且仅当下一条请求时访问信息。
使用flask()记录消息。
在下一条请求或模版中使用get_flashed_messages()获取消息。
日志记录¶
app.logger.debug('')
app.logger.warning('', 42)
app.logger.error('')
# logger是标准的python Logger
整合WSGI中间件¶
使用Werkzeug的中间件避免lighttpd的bugs
from werkzeug.contrib.fixers import LighttpdCGIRootFix
app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app)
使用Flask扩展¶
见扩展
部署到Web服务器¶
见部署
简单完整示例教程¶
示例功能¶
单用户评论系统
使用sqlite作为数据库
倒叙显示所有评论
评论内容HTTP不安全
工程目录¶
static存放静态文件
template存放jinja2模板html
/flaskr
/static
/templates
数据库¶
schema.sql
存放评论的id,标题,内容
drop table if exists entries;
create table entries (
id integer primary key autoincrement,
title string not null,
text string not null
);
创建数据库方法一:使用shell
sqlite3 /tmp/flaskr.db < schema.sql
创建数据库方法二:python
def init_db():
# 在没有请求的时候,flask.g全局变量无法得知应用信息,使用app_context()创建环境。
with app.app_context():
db = get_db()
# open_resource从flask应用目录打开文件。
with app.open_resource('schema.sql', mode='r') as f:
# 读取sql脚本并执行
db.cursor().executescript(f.read())
db.commit()
@app.cli.command('initdb')
def initdb_command():
init_db()
print('Initialized the database.')
Flask应用启动、配置、数据库连接¶
从环境变量导入路径¶
Flask没有当前工作目录的概念,因为一个进程中可能会运行多个应用。
可以使用app.root_path获取应用路径,用os.path辅助。
可以使用app.config.from_envvar("FLASKR_SETTINGS", silent=True)从环境变量中获取配置。
使用环境变量传递信息¶
request是当前请求的环境变量
g是当前request共享信息的变量
数据库连接代码¶
import os
import sqlite3
from flask import Flask, request, session, g, redirect, url_for, abort, render_template, flash
app = Flask(__name__)
# 默认配置,配置必须使用大写
app.config.update(dict(
DATABASE=os.path.join(app.root_path, 'flaskr.db'),
DEBUG=True,
SECRET_KEY='development key',
USERNAME='admin',
PASSWORD='default'
))
# 从环境变量覆盖配置
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
def connect_db():
'''根据配置文件连接数据库'''
rv = sqlite3.connect(app.config['DATABASE'])
rv.row_factory = sqlite3.Row
return rv
def get_db():
'''保存数据库连接'''
if not hasattr(g, 'sqlite_db'):
g.sqlite_db = connect_db()
return g.sqlite_db
# 使Flask应用结束时销毁的装饰器
@app.teardown_appcontext
def close_db(error):
if hasattr(g, 'sqlite_db'):
g.sqlite_db.close()
if __name__ == '__main__':
app.run()
flask应用,url处理¶
显示评论¶
@app.route('/')
def show_entries():
db = get_db()
# 从数据库中读取所有评论
cur = db.execute('select title, text from entries order by id desc')
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
# 渲染页面
return render_template('show_entries.html', entries=entries)
添加评论¶
@app.route('/add', methods=['POST'])
def add_entry():
# 在session中查询
if not session.get('logged_in'):
abort(401)
# 写入数据库
db = get_db()
db.execute('insert into entries (title, text) values (?, ?)',
[request.form['title'], request.form['text']])
db.commit()
# 发送falsh消息
flash('New entry was successfully posted')
# 重定向到评论显示页面
return redirect(url_for('show_entries'))
登录页面及登录POST¶
@app.route('/login', methods=['GET', 'POST'])
def login():
error = None
if request.method == 'POST':
# 登录验证
if request.form['username'] != app.config['USERNAME']:
error = 'Invalid username'
elif request.form['password'] != app.config['PASSWORD']:
error = 'Invalid password'
# 登录成功写入session
else:
session['logged_in'] = True
flash('You were logged in')
return redirect(url_for('show_entries'))
# GET请求返回登录页
return render_template('login.html', error=error)
退出¶
@app.route('/logout')
def logout():
# 从Session中删除
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('show_entries'))
HTML模板¶
layout.html¶
<!doctype html>
<title>Flaskr</title>
<link rel=stylesheet type=text/css href="{{url_for('static', filename='style.css')}}">
<div class=page>
<h1>Flaskr</h1>
<div class=metanav>
{% if not session.logged_in %}
<a href="{{url_for('login')}}">log in</a>
{% else %}
<a href="{{url_for('logout')}}">log out</a>
{% endif %}
</div>
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
{% endfor %}
{% block body %}
{% endblock %}
</div>
show_entries.html¶
使用safe过滤器是jajin2不做安全转换
{% extends "layout.html" %}
{% block body %}
{% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
<dl>
<dt>Title:</dt>
<dd><input type=text size=30 name=title></dd>
<dt>Text:</dt>
<dd><textarea name=text rows=5 clos=40></textarea></dd>
<dd><input type=submit value=Share></dd>
<dl>
</form>
{% endif %}
<ul class=entires>
{% for entry in entries %}
<li><h2>{{ entry.title }}</h2>{{ entry.text | safe }}</li>
{% else %}
<li><em>Unbelievable. No entries here so far</em></li>
{% endfor %}
</ul>
{% endblock %}
login.html¶
{% extends "layout.html" %}
{% block body %}
<h2>Login</h2>
{% if error %}
<p class=error><strong>Error:</strong>{{ error }}</p>
{% endif %}
<form action="" method=post>
<dl>
<dt>Username:</dt>
<dd><input type=text name=username></dd>
<dt>Passwrod:</dt>
<dd><input type=password name=password></dd>
<dd><input type=submit value=Login></dd>
</dl>
</form>
{% endblock %}
CSS样式¶
style.css¶
body {font-family: sans-serif; background: #eee;}
a, h1, h2 {color: #377BA8;}
h1, h2 {font-family: 'Georgia', serif; margin:0;}
h1 {border-bottom: 2px solid #eee;}
h2 {font-size: 1.2em;}
.page {margin: 2em auto; width: 35em; border: 5px solid #ccc; padding:0.8em; background: white;}
.entries {list-style: none; margin: 0; padding: 0;}
.entries li {margin: 0.8em 1.2em;}
.entries li h2 {margin-left: -1em;}
.add-entry {font-size: 0.9em; border-bottom: 1px solid #ccc;}
.add-entry dl {font-weight: bold;}
.metanav {text-align: right; font-size: 0.8em; padding: 0.3em; margin-bottom: 1em; background: #fafafa;}
.flash {background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2;}
.error {background: #F0D6D6; padding: 0.5em;}
单元测试¶
测试工程结构¶
flaskr/
flaskr/
__init__.py
static/
templates/
tests/
test_flaskr.py
setup.py
MANIFEST.in
运行测试¶
测试的大框架¶
第一个测试¶
登录和登出¶
测试消息的添加¶
其他测试技巧¶
伪造资源和上下文¶
保存上下文¶
访问和修改Sessions¶
模板¶
Jinja配置¶
默认配置:
所有扩展名为.html .htm .xml .xhtml的木板会开启自动转译
在模板中可以利用{% autoescape false %}标签选择转译是否开启。
Flask在Jinja2上下文中插入了几个全局函数和助手,另外还有一些目前默认的值。
标准上下文,默认在Jinja2模板中可用的值¶
可以使用的变量和方法¶
Flask内置变量及方法:
config:flask.config
request:flask.request
session:flask.session
g:flask.g
url_for():flask.url_for()
get_flasked_message():flask.get_flashed_message()
python变量:
<p>Dict item: {{ my_dict['key'] }}</p>
<p>List item: {{ my_list[3] }}</p>
<p>List with a variable index: {{ my_list[my_index] }}</p>
<p>Object's method {{ my_object.method() }}</p>
导入宏(Jinja2的宏)的方法¶
由于以上变量是被添加到request中的,而且不是全局变量,所以想要导入一个需要访问request的宏,需要显式地传入request或变量作为宏的参数。
或者导入宏时手动带上context:
{% from '_helpers.html' import my_macro with context %}
标准过滤器,Jinja2自带的过滤器¶
tojson():
<script type=text/javascript>
doSomethingWith({{ user.username | tojson | safe }});
</script>
safe:渲染时不转译
capitalize:句子首字母大写
lower:全部小写
upper:全部大写
title:句子中所有词首字母大写
trim:去掉首位空格
striptags:取消所有HTML标签
其他内置过滤器:http://jinja.pocoo.org/docs/templates/#builtin-filters
控制自动转译¶
Jinja2会自动转译HTML中的特殊字符,如:& > < " '
。
但是有时会需要插入HTML,例如插入markdown转换成的HTML,有以下三种方法。
1. 在传递到模板之前,将HTML字符串转换成Markup对象。推荐使用这种方法。
2. 在模版中使用|safe过滤器将变量标记为安全,如{{ var | safe }}
。
3. 临时完全禁用自动转译如
{% autoescape false %}
<p>autoescaping is disabled here</p>
<p>{{ will_not_be_escaped_var }}</p>
{% endautoescape %}
注册过滤器¶
使用装饰器标记自定义Jinja2过滤器¶
@app.template_filter('reverse')
def reverse_filter(s):
return s[::-1]
手动添加过滤器到Jinja2¶
def reverse_filter(s):
return s[::-1]
app.jinja_env.filters['reverse'] = reverse_filter
在模板中使用过滤器¶
{% for x in mylist | reverse %}
{% endfor %}
使用context_processor添加变量、函数添加到模板上下文¶
context_processor是一个返回dict的函数,dict中的值可以是变量也可以是函数
@app.context_processor
def inject_user():
return dict(user=g.user)
@app.context_processor
def utility_processor():
def format_price(amount, currency=u'$'):
return u'{0:.2f}{1}.format(amount, currency)
return dict(format_price=format_price)
{{ format_price(0.33) }}
模板结构控制¶
条件¶
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
循环¶
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
宏¶
定义:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
使用:
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
从其他文件中导入:
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
继承¶
父模板base.html:
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
子模板:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello, World!</h1>
{% endblock %}
使用super()
引用原模板内容,用于向原有块中添加新内容
日志、邮件、调试模式¶
Flask默认不会写任何日志。
配置Flask发送错误邮件¶
ADMINS = ['yourname@example.com']
if not app.debug:
import logging
from logging.handlers import SMTPHandler
mail_handler = SMTPHandler('127.0.0.1', 'server-error@example.com', ADMINS, 'YourApplication Failed')
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
记录到文件¶
日志记录器:
- FileHandler 记录文件日志
- RotatingFileHandler 循环覆盖记录日志
- NTEventLogHandler 记录日志到Windows系统事件
- SysLogHandler 记录日志到Unix系统日志
if not app.debug:
import logging
from themodule import TheHandlerYouWant
file_handler = TheHandlerYouWant(...)
file_handler.setLevel(logging.WARNING)
app.logger.addHandler(file_handler)
控制日志格式¶
邮件格式¶
from logging import Formatter
mail_handler.setFormatter(Formatter('''
Message type: %(levelname)s
Location: %(pathname)s:%(lineno)d
Module: %(module)s
Function: %(funcName)s
Time: %(asctime)s
Message:
%(message)s
'''))
日志格式¶
from logging import Formatter
file_handler.setFormatter(Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
常用日志变量¶
格式 | 描述 |
---|---|
%(levelname)s | 消息文本的记录等级 ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). |
%(pathname)s | 发起日志记录调用的源文件的完整路径(如果可用) |
%(filename)s | 路径中的文件名部分 |
%(module)s | 模块(文件名的名称部分) |
%(funcName)s | 包含日志调用的函数名 |
%(lineno)d | 日志记录调用所在的源文件行的行号(如果可用) |
%(asctime)s | LogRecord 创建时的人类可读的时间。 |
%(message)s | 记录的消息,视为 msg % args |
默认情况下,asctime格式为 "2003-07-08 16:49:45,896" (逗号后的数字时间的毫秒部分)。这可以通过继承 :class:~logging.Formatter,并重载 formatTime() 改变。
format(): 处理实际格式,输入LogRecord,输出格式化后的字符串。
formatTime(): 控制asctime格式。
formatException(): 控制异常的格式,输入exc_info,输出字符串。
其他的库¶
不建议使用Flask.logging统一配置所有日志,因为这样运行多个应用时不能分别配置。
建议使用getLogger获取日志记录,然后遍历日志记录器:
from logging import getLogger
logger = [app.logger, getLogger('sqlalchemy'), getLogger('otherlibrary']
from logger in loggers:
logger.addHandler(mail_handler)
logger.addHandler(file_handler)
开启Debug¶
app.debug = True
或
run(debug=True)
调试器配置¶
Flask默认调试器可能会与其他调试器冲突。
- debug: 是否开启调试
- use_debugger: 是否使用内部Flask调试器
- use_reloader: 是否在异常时重新再如并创建子进程
开启外部调试器(Aptana/Eclipse)配置示例:
config.yaml
Flask:
DEBUG: True
DEBUG_WITH_APTANA: True
if __name__ == '__main__':
app = create_app(config='config.yaml')
if app.debug:
# 默认使用Flask内部调试器
use_debugger = True
try:
# 当外部调试器开启时,关闭Flask内部调试器
use_debugger = not(app.config.get('DEBUG_WITH_APTANA'))
except:
pass
app.run(use_debugger=use_debugger, debug=app.debug, use_reloader=use_debugger, host='0.0.0.0')
配置处理¶
配置基础¶
内置的配置值¶
从文件配置¶
配置的最佳实践¶
开发/生产¶
实例文件夹¶
信号¶
订阅信号¶
创建信号¶
发送信号¶
信号与Flask的请求上下文¶
基于装饰器的信号订阅¶
核心信号¶
Pluggable Views¶
基本原则¶
方法提示¶
基于调度的方法¶
装饰试图¶
基于API的方法视图¶
应用上下文¶
应用上下文的作用¶
在一个Python进程中可以运行多个Flask应用。
为了不使用参数层层传递context,Flask使用current_app指向当前请求所属的应用。
创建应用上下文¶
当一个request的context被放入处理栈时,如果有需要,应用context就会被一起创建。
显示调用app_context()方法可以创建应用context,如:
from flask import Flask, current_app
app = Flask(__name__)
with app.app_context():
# 在这里就可以通过current_app访问应用context
print current_app.name
print current_app.url_map
应用上下文局部变量¶
flask.current_app # 程序上下文,当前程序
flask.g # 存储在程序上下文,处理请求时用作临时存储对象,每次请求会重设
flask.request # 请求上下文,请求对象
flask.session # 请求上下文,用户会话
上下文用法¶
显式创建使用资源¶
可以在上下文中缓存数据库连接。
import sqlite3
from flask import g
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = connect_to_database()
return db
@app.teardown_appcontext
def teardown_db(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
隐式创建使用资源¶
使用LocalProxy存储变量
from werkzeug.local import LocalProxy
db = LocalProxy(get_db)
# 用户可以直接通过db访问数据,db已经在内部完成get_db调用
请求上下文¶
上下文作用域¶
请求context仅在处理请求时存在。例如以下代码在没有请求时调用会发生错误:
from flask import reqeust, url_for
def redirect_url():
return request.args.get('next') or request.referrer or url_for('index')
可以使用text_request_context('/?next=http://example.com/'
模拟请求。
使用with的方式:
with app.text_request_context('/?next=http://example.com/')
redirect_url()
手动入栈请求的方式:
ctx = app.text_request_context('/?next=http://example.com/')
# 入栈
ctx.push()
# 处理请求
redirect_url()
# 出栈
ctx.pop()
Flask请求context内部处理¶
def wsgi_app(self, environ):
with self.request_context(environ):
try:
response = self.full_dispatch_request()
except Exception, e:
response = self.make_response(self.handle_exception(e))
return response(environ, start_response)
回调和错误¶
- 在所有请求前调用
before_first_request()
。 - 每个请求之前,执行
before_request()
上绑定的函数,若某个函数返回了一个response
则其他函数将不会被调用。 - 如果
before_request()
绑定的函数没有返回response
,则url处理函数被正常调用。 - url处理函数返回后被转化成
response
对象,之后会调用after_request()
- 最后会调用
teardown_request()
上绑定的函数,即使发生异常也会被执行。
teardown_request()¶
@app.teardown_request
def teardown():
pass
with app.text_client() as client:
resp = client.get('/foo')
# teardown_request()在这里被调用
留意代理¶
错误时的上下文保护¶
用蓝图实现模块化¶
为什么使用蓝图¶
- 把一个应用分成几个模块。
- 用URL前缀或子域名模块化管理URL。
- 可以使一个模块获得多种URL规则。
- 可以使用蓝图模块化静态文件、模块化模板、模板化过滤器。
- 在Flask扩展中使用。
蓝图的设想¶
- 注册式地集成到应用。
- 按模块分配请求和处理URL。
创建蓝图(模块)¶
from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound
simple_page = Blueprint('simple_page', __name__, template_folder='templates')
@simple_page.route('/', defaults={'page': 'index'})
@simple_page.route('/<page>')
def show(page):
try:
return render_template('pages/%s.html' % page)
except TemplateNotFound:
abort(404)
蓝图绑定到app时,@simple_page.route
会将show
注册为simple_page.show
注册(使用)蓝图(模块)¶
from flask import Flask
from yourapplication.simple_page import simple_page
app = Flask(__name__)
# 挂载在根目录
app.register_blueprint(simple_page)
# 挂载到/pages
app.register_blueprint(simple_page, url_prefix='/pages')
蓝图使用(模块)资源¶
蓝图资源文件夹¶
蓝图若使用单独文件夹(Python包),这个文件夹就是蓝图的资源文件夹。
蓝图若共用文件夹(Python模块),模块所在文件夹就是资源文件夹。
可以使用simple_page.root_path
获取资源路径。
可以使用simple_page.open_resource()
从文件夹打开文件。如:
with simple_page.open_resource('static/style.css') as f:
code = f.read()
静态文件¶
可以使用static_folder
参数指定静态文件夹路径,如:
admin = Blueprint('admin', __name__, static_folder='static')
若admin模块加载在根路径'/',静态资源路径就是'/static/xxx'
若admin模块加载在'/admin',静态资源路径就是'/admin/static/xxx'
可以使用admin.static访问static参数,如:
url_for('admin.static', filename='style.css')
模板路径¶
admin = Blueprint('admin', __name__, template_folder='templates')
构造URL¶
# 不同模块构造url
url_for('admin.index')
# 相同模块构造url
url_for('.index')
Flask扩展¶
寻找扩展¶
使用扩展¶
Flask 0.8以前¶
Flask-Script¶
命令交互式操作Flask
from flask import Flask
# 废弃
from flask.ext.script import Manager
# 建议
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app)
if __name__ == '__main__':
manager.run()
py -3 hello.py --help
py -3 hello.py shell --help
py -3 hello.py runserver --help
自动导入上下文:
from flask_script import Shell
def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command('shell', Shell(make_context=make_shell_context))
Flask-Bootstrap¶
flask_bootstrap提供bootstrap,google analytics,WTForm定义等模板
base.html定义的块¶
块名 | 说明 |
---|---|
doc | 整个HTML文档 |
html_attribs | 标签的属性 |
html | 标签中的内容 |
head | 标签中的内容 |
title | |
metas | 一组标签 |
styles | 层叠样式表定义 |
body_attribs | 标签的属性 |
body | 标签中的内容 |
navbar | 用户定义的导航条 |
content | 用户定义的页面内容 |
scripts | 文档底部的Javascript声明 |
避免覆盖模板原有内容:
{% block scripts %}
{{ super() }}
<script type="text/javascript" src="my-script.js"></script>
{% endblock %}
python初始化¶
from flask import app
from flask_bootstrap import Bootstrap
app = Flask(__name__)
bootstrap = Bootstrap(app)
app.run()
模板继承¶
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
<div class="page-header">
<h1>Hello, {{ name }}!</h1>
</div>
</div>
{% endblock %}
Flask-Moment¶
moment.js提供本地化时间
http://momentjs.com/docs/#/displaying/
安装:
pip install flask-moment
python:
from datetime import datetime
from flask.ext.moment import Moment
moment = Moment(app)
@app.route('/')
def index():
return render_template('index.html', current_time=datetime.utcnow())
moment需要输入不带时区的utc时间
模板:
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
{% block page_content %}
<p>The local date and time is {{ moment(current_time).format('LLL') }}.</p>
<p>That was {{ moment(current_time).fromNow(refresh=True) }}.</p>
{% endblock %}
moment.format显示时间
moment.fromNow显示距离当前多久,动态刷新
语言本地化:
{{ moment.lang('es') }}
{{ moment.lang('zh-cn') }}
Flask-WTF¶
flask-wtf默认开启防止csrf攻击,即发送表单时发送一个key要求响应时带回,key错误时为伪造响应。
安装:
pip install flask-wtf
启动:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'flask-wtf需要使用secret_key'
定义:
from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')
wtf手动生成表单:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.name.label }} {{ form.name(id='my-text-field') }} # 指定ID等用于样式表
{{ form.submit() }}
</form>
flask-bootstrap + flask-wtf自动生成表单:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
表单验证:
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.date = ''
return render_template('index.html', form=form, name=name)
GET POST二合一,POST时通过validate_on_submit判断获取输入
POST后刷新处理:
POST后刷新,浏览器会重复POST,可以通过POST响应URL重定向避免。
from flask import Flask, render_template, session, redirect, url_for
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'))
字段:
字段类型 | 说明
---------|-----
StringField | 文本字段
TextAreaField | 多行文本字段
PasswordField | 密码文本字段
HiddenField | 隐藏文本字段
DateField | 文本字段,值为datetime.date格式
DateTimeField | 文本字段,值为datetime.datetime格式
IntegerField | 文本字段,值为整数
DecimalField | 文本字段,值为decimal.Decimal
FloatField | 文本字段,值为浮点数
BooleanField | 复选框,值为True和False
RadioField | 一组单选框
SelectField | 下拉列表
SelectMultipliField | 下拉列表,可选择多个值
FileField | 文件上传字段
SubmitField | 表单提交按钮
FormField | 把表单作为字段嵌入另一个表单
FieldList | 一组指定类型的字段
验证:
验证函数 | 说明
---------|-----
Email | 验证电子邮件地址
EqualTo | 比较两个字段的值,常用于要求输入两次密码进行确认
IPAddress | 验证IPv4网络地址
Length | 验证输入字符串的长度
NumberRange | 验证输入的值在数字范围内
Optional | 无输入时跳过其他验证函数
Required | 确保字段中有数据
Regexp | 使用正则表达式验证输入值
URL | 验证URL
AnyOf | 确保输入值在列表中
NoneOf | 确保输入值不在列表中
Flask-SQLAlchemy¶
安装:
pip install flask-sqlalchemy
数据库URI配置:
export SQLALCHEMY_DATABASE_URI=mysql://username:password@hostname/database
数据库引擎 | URL |
---|---|
MySQL | mysql://username:password@hostnmae/database |
Postgres | postgresql://username:password@hostname/database |
SQLite(*nix) | sqlite:///absolute/path/to/database |
SQLite(win) | sqlite:///c:/absolute/path/to/database |
初始化:
from flask_salalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
# 自动提交
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
# 跟踪数据库变更
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
模型定义:
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
__repr()__返回一个可读性字符串表示模型,用于调试
操作:
from app.py import db, Role, User
# 不存在则创建数据库
db.create_all()
# 删除
db.drop_all()
# 创建对象
admin_role = Role(name='Admin')
mod_role = Role(name='Moderator')
user_role=Role(name='User')
user_john=User(username='john', role=admin_role)
user_susan = User(username='susan', role=user_role)
user_david = User(username='david', role=user_role)
# 插入数据库
db.session.add(admin_role)
db.session.add(mod_role)
db.session.add(user_role)
db.session.add(user_john)
db.session.add(user_susan)
db.session.add(user_david)
# 批量插入数据库
db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])
# 提交,session是原子操作,避免不一致性
db.session.commit()
# 回退
db.session.rollback()
# 修改
admin_role.name = 'Administrator'
db.session.add(admin_role)
db.session.commit()
# 删除
db.session.delete(mod_role)
db.session.commit()
# 查询
Role.query.all()
User.query.all()
User.query.filter_by(role=user_role)
Role.query.filter_by(name='User').first()
# 显示SQL语句
str(User.query.filter_by(role=user_role))
# 排序
user_role.users.order_by(User.username).all()
# 统计
user_role.users.count()
数据类型:
类型名 | Python类型 | 说明
-------|------------|------
Integer | int | 普通整数,一般是32位
SmallInteger | int | 取值范围小的整数,一般是16位
BigInteger | int或long | 不限制精度的整数
Float | float | 浮点数
Numeric | decimal.Decimal | 定点数
String | str | 变长字符串
Text | str | 变长字符串,对较长或不限长度的字符串做了优化
Unicode | unicode | 变长Unicode字符串
UnicodeText | unicode | 变长Unicode字符串,对较长或不限长度的字符串做了优化
Boolean | bool | 布尔值
Date | datetime.date | 日期
Time | datetime.time | 时间
DateTime | datetime.datetime | 日期和世界
Interval | datetime.timedelta | 时间间隔
Enum | str | 一组字符串
PickleType | 任何Python对象 | 自动使用Pickle序列化
LargeBinary | str | 二进制文件
列选项:
选项名 | 说明
-------|-----
primary_key | 主键
unique | 不允许重复
index | 创建索引
nullable | 允许使用空值
default | 定义默认值
一对多关系:
class Role(db.Model):
users = db.relationship('User', backref='role')
class User(db.Model):
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
db.relationship定义一种特殊属性users,访问users属性返回对应的User列表
backref在User模型中添加一个role属性,访问role访问的是对象,访问role_id只能访问外键的值
relationship()选项:
选项名 | 说明
-------|-----
backref | 在关系的另一个模型中添加反向作用
primaryjoin | 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中需要定义。
lazy | 加载方式:
... | select:首次访问时按需加载
... | immediate:源对象加载则加载
... | join:使用联结加载
... | subquery:立即加载,使用子查询
... | noload:永不加载
... | dynamic:不加载,提供查询
uselist | True使用列表,False一对一不使用列表
order_by | 指定关系中记录的排序方式
secondary | 指定多对多关系中关系表的名称
secondaryjoin | SQLAlchemy无法决定时,指定多对多关系中的二级联结条件
查询过滤器:
过滤器 | 说明
-------|-----
filter() | 把过滤器添加到原查询上,返回一个新查询
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询
limit() | 限制查询结果数量,返回一个新查询
offset() | 便宜查询结果,返回一个新查询
order_by() | 排序,返回新查询
group_by() | 分组,返回新查询
查询执行函数:
方法 | 说明
-----|-----
all() | 返回所有结果list
first() | 返回第一个结果或None
first_or_404() | 返回第一个结果或404
get() | 返回指定主键对应行或None
get_or_404() | 返回指定主键对应行或404
count() | 返回查询结果的数量
paginate() | 返回一个Paginate对象,包含范围内的结果
数据库迁移¶
SQLAlchemy Alembic:https://alembic.readthedocs.org/en/latest/index.html
Flask-Migrate(Alembic轻量级包装):http://flask-migrate.readthedocs.org/en/latest/,依赖Flask-Script操作。
安装:
pip install flask-migrate
启动代码:
from flask_migrate import Migrate, MigrateCommand
app = Flask(__name__)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
初始化命令:
python hello.py db init
手动创建迁移(自行实现upgrade()
和downgrade()
):
python hello.py db revision
自动创建迁移(需要检查)
python hello.py db migrate -m "initial migration"
升级数据库
python hello.py db upgrade
Flask-Mail¶
包装python标准smtplib
安装:
pip install flask-mail
配置项:
配置 | 默认值 | 说明
-----|--------|-----
MAIL_SERVER | localhost | 电子邮件服务器域名或IP
MAIL_PORT | 25 | 电子邮件服务器端口
MAIL_USE_TLS | False | 启用TLS
MAIL_USE_SSL | False | 启用SSL
MAIL_USERNAME | None | 邮件账户的用户名
MAIL_PASSWORD | None | 邮件账户的密码
配置启动:
import os
from flask import Flask
from flask_mail import Mail
app = Flask(__name__)
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.evviron.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
mail = Mail(app)
发送邮件:
from flask.ext.mail import Message
msg = Message('test subject', sender='you@example.com', recipients=['you@example.com'])
msg.body = 'text body'
msg.html = '<b>HTML</b> body'
mail.send(msg)
发送邮件(模板渲染):
from flask.ext.mail import Message
msg = Message('test subject', sender='you@example.com', recipients=['you@example.com'])
msg.body = render_template(template + '.txt', parm1, parm2)
msg.html = render_template(template + '.html', parm1, parm2)
mail.send(msg)
Celery任务队列¶
特性¶
Brokers(消息丢列,代理): RabbitMQ, Redis, Amazon SQS
Result Stores(结果存储): AMQP, Redis, Memcached, SQLAlchemy, Django ORM, Apache Cassandra, Elasticsearch
Concurrency(并发): prefork(multiprocessing), Eventlet, gevent, solo(single threaded)
Serialization(序列化): pickle, json, yaml, msgpack. zlib, bzip2, 消息签名
Monitoring: worker实时监控事件
Work-flows: 工作流支持,分组、数据链、块
Time & Rate Limits: 按时间、处理时长进行流控
Scheduling: 计划任务,相对时间、时间间隔、定时任务
安装¶
pip install celery
pip install celery[redis]
# 其他Broker
pip install celery[librabbitmq, redis, auth, msgpack]
定义任务¶
task.py
from celery import Celery
# redis url 格式:'redis://:password@hostname:port/db_number'
# 默认port为6379,数据库id为0
# redis socket 格式: 'redis+socket:///path/to/redis.sock?virtual_host=db_number'
# 只执行不返回结果
# app = Celery('tasks', broker='redis://localhost')
# 配置backend保存结果
app = Celery('tasks', backend='redis://localhost', broker='redis://localhost')
@app.task()
def add(x, y)
return x + y
启动Worker任务¶
celery -A task worker # 启动任务
celery -A task worker --loglevel=info # 带日志选项启动任务
celery worker --help # 查看worker选项帮助
celery help # 帮助
发送任务¶
client.py
from task import add
result = add.delay(1, 2) # 返回一个AsyncResult对象,用于检查状态、获取结果或异常
result = add.apply_async((1, 2))
# 检查状态
result.ready()
# 获取结果
result.get() # 必须设置backend保存及诶过
# 关闭异常重现,默认若出现异常get会重新抛出异常
result.get(propagate=False)
# 访问异常调用栈
result.traceback
Celery Flower¶
https://flower.readthedocs.io/en/latest/
特性:
- 实时任务执行监控工具web界面
- 远程控制
- Broker监控
- HTTP API
- 基本用户认证,支持Google OpenID
pip install flower
flower -A proj --port=5555
curl http://localhost:5555/
Flask-Admin¶
与Shell共舞¶
创建一个请求上下文¶
激发请求发送前后的调用¶
Flask代码模式¶
大型应用(批量导入url处理类)¶
修改前目录结构¶
/application
/application.py
/static
/style.css
/templates
/layout.html
/index.html
/login.html
...
修改后目录结构¶
/application
/runserver.py
/application
/__init__.py
/views.py
/static
/templates
添加runserver.py
from application import app
app.run(debug=True)
添加init.py
from flask import Flask
app = Flask(__name__)
# 在Flask生成后导入模块中所有view
import application.views
添加views.py
from application import app
@app.route('/')
def index():
return 'Hello World!'
修改步骤:
1. 使用python包(目录)替换python模块(py文件)管理代码
2. 添加启动代码runserver.py
3. 在包init.py中创建Flask应用,导入模块所有view。
4. 在views中引用app,实现url处理程序。
这样修改有循环依赖的嫌疑,其实在__init__.py中只导入并没有使用,并且实在文件末尾导入的
应用程序的工厂函数¶
应用调度¶
实现API异常处理¶
使用URL处理器¶
使用Setuptools部署、分发¶
使用Fabric部署¶
在Flask中使用sqlite3¶
在Flask中使用sqlalchemy¶
上传文件¶
缓存¶
视图装饰器¶
使用WTForms进行表单验证¶
模板继承¶
消息闪现¶
用jQuery实现Ajax¶
自定义错误页面¶
延迟加载视图¶
在Flask中使用MongoKit¶
添加Favicon¶
特性¶
16*16像素的ICO文件
新标准¶
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
旧标准¶
放在root路径下的favicon.ico
app.add_url_rule('/favicon.ico', redirect_to=url_for('static', filename='favicon.ico'))
或者
import os
from flask import send_from_directory
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon,ico', mimetype='image/vnd.microsoft.icon')
模板¶
templates/base.html
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }} type="image/x-icon">
{% endblock %}
数据流¶
延迟请求回调¶
添加HTTP Method Overrides¶
请求内容校验码¶
基于Celery的后台任务¶
继承Flask¶
部署选择¶
托管配置¶
主机配置¶
mod_wsgi(Apache)¶
独立WSGI容器¶
Gunicorn:
Gevent:
Twisted Web:
Proxy Setups: