8、重定向与会话
Web开发中经常需要处理重定向和会话,Flask中内建了「redirect」和「session」来对它们进行处理。
0x1 重定向
页面重定向非常常见,最常用的就是登陆状态判断,如果没登陆将网页重定向到登录页,Flask中可以使用redirect 对象对其进行处理,状态码默认为302,可以传入code参数来修改,一般是:301,302,303,305和307, 简单的代码示例如下:
# coding=utf-8 from flask import Flask, redirect app = Flask(__name__) user_name = '' @app.route('/article') def article(): if user_name == '': # 如果用户名为空,重定向跳转到登录页 return redirect('/login') else: return "文章页" @app.route("/login") def login(): global user_name user_name = 'admin' return "登录成功!" if __name__ == "__main__": app.run(debug=True)
运行后操作流程如下:
浏览器键入:http://127.0.0.1:5000/article 自动跳转到:http://127.0.0.1:5000/login,显示登录成功 再次访问:http://127.0.0.1:5000/article,显示文章页
0x2 会话
我们可以把数据存储在用户会话(session)中,用户会话是一种私有存储,默认情况下保存在客户端cookie中。 会话主要为了解决两个问题:「访问者的标识和访问者信息记录」。浏览器第一次访问服务器,服务器在其 cookie中设置一个唯一的会话ID,浏览器后续对服务器的访问头中将自动包含该信息,服务器通过这个ID号来 区分不同的访问者。session依赖于cookie,一般存储在服务器,Flask提供了session对象来操作用户会话, 可以使用[]操作符读取或者设置指定键值,默认情况下,Flask将会话对象加密后存储在客户端的cookie里, 因此必须要应用实例的secret_key属性配置一个加密种子才能使用session。用法示例如下:
# 设置session session['name'] = 'jay' # 读取session session.get('name') # 配置加密种子(两种方法二选一) app.secret_key = '123456' app.config['SECRET_KEY']='123456'
9、静态文件管理
静态文件就是那些不会被改变的文件,例如:图片,CSS样式文件,JavaScript脚本文件和字体文件等。 Flask默认会在根目录中名为static的子目录中寻找静态文件,所以如果需要用到静态文件可以创建一个 static的文件夹,然后把静态文件丢里面。可以参考下面这样的结构来组织项目:
static/ css/ lib/ bootstrap.css style.css home.css js/ lib/ jquery.js chart.js home.js img/ logo.svg favicon.ico
另外,不要在模板中写死静态文件路径,应该使用url_for函数生成路径,示例如下:
url_for('static', filename='css/style.css')
当然,如果你想修改静态文件的真实目录,可以在Flask构造函数中传入参数:static_folder='文件夹名'。 另外,为了获得更好的处理能力,建议使用Nginx或其他Web服务器管理静态文件,图片这类资源可以 托管到CDN平台上。(比如七牛云)
10、蓝图
蓝图(Blueprint),定义了可用于单个应用的视图,模板,静态文件等等的集合。通俗点理解就是 一个实现应用模块化的好工具,使用蓝图能使得项目层次更加清晰,更易于开发和维护,通常作用于 相同URL前缀的路由。先来看一个没使用蓝图的示例:
# coding=utf-8 from flask import Flask app = Flask(__name__) @app.route('/user/index') def user_index(): return 'user_index' @app.route('/user/show') def user_show(): return 'user_show' @app.route('/user/add') def user_add(): return 'user_add' @app.route('/admin/index') def admin_index(): return 'admin_index' @app.route('/admin/show') def admin_show(): return 'admin_show' @app.route('/admin/add') def admin_add(): return 'admin_add' if __name__ == "__main__": app.run(debug=True)
上面的代码挺整齐的,不过有几个问题:
- 如果user和admin的功能不止上面的几个,而是好几百呢,代码会非常庞大臃肿。
- 大型的项目都是多人协作的,所有人都在这里文件里开发的话,处理合并冲突会很头痛。
- 如果哪天这两个用户模块不要了,还需要一行行的去找,然后删代码。
我们使用蓝图来把user和admin拆分成两个不同的.py文件,admin.py 文件内容如下:
# coding=utf-8 from flask import Blueprint admin = Blueprint('admin', __name__,url_prefix='/admin') @admin.route('/index') def admin_index(): return 'admin_index' @admin.route('/show') def admin_show(): return 'admin_show' @admin.route('/add') def admin_add(): return 'admin_add'
user.py 文件内容如下:
# coding=utf-8 from flask import Blueprint user = Blueprint('user',__name__) @user.route('/index') def user_index(): return 'user_index' @user.route('/show') def user_show(): return 'user_show' @user.route('/add') def user_add(): return 'user_add'
注册蓝图,hello.py的代码内容如下:
# coding=utf-8 from flask import Flask from admin import * from user import * app = Flask(__name__) app.register_blueprint(admin) app.register_blueprint(user, url_prefix='/user') if __name__ == "__main__": print(app.url_map) app.run(debug=True)
利用app.url_map函数,查看所有的路由,打印结果如下:
Map([<Rule '/admin/index' (GET, HEAD, OPTIONS) -> admin.admin_index>, <Rule '/admin/show' (GET, HEAD, OPTIONS) -> admin.admin_show>, <Rule '/admin/add' (GET, HEAD, OPTIONS) -> admin.admin_add>, <Rule '/user/index' (GET, HEAD, OPTIONS) -> user.user_index>, <Rule '/user/show' (GET, HEAD, OPTIONS) -> user.user_show>, <Rule '/user/add' (GET, HEAD, OPTIONS) -> user.user_add>, <Rule '/static/<filename>' (GET, HEAD, OPTIONS) -> static>])
url_prefix
这个参数用于设置request.url
中的url前缀,另外只有满足前缀的请求才会 通过注册的蓝图的视图方法处理请求并返回。可以写在子模块中,也可以在register_blueprint
注册蓝图的时候传入,只需传入一次!之后打开http://127.0.0.1:5000/admin/index,可以看到 如图所示的结果:
10、g对象和钩子函数
有时在处理请求前后,执行某些特定代码是非常有用的,这就用到了请求钩子,比如:请求前创建db链接, 验证用户身份等,flask提供了注册通用函数的功能,只需要写一个请求钩子——函数,整个程序实例全局都被应用, 比如请求前验证用户状态的例子,没登陆跳转登录页面等。钩子函数需要借助Flask的全局变量g,g作为中间变量, 在钩子函数和视图函数间传递数据。
0x1 g对象
g,global,g对象是专门用来保存用户数据的,存储的数据在全局都可以使用。代码示例如下:
from flask import g, request @app.route('/') def index(): user = request.args.get('user') g.user = user # 保存用户数据
0x2 钩子函数
Flask提供下述四种钩子函数:
- before_first_request:在第一次请求前调用,可以在此方法内做一些初始化操作。
- before_request:在每次请求前调用,一般做校验,如果校验不成功,可以在这个方法内直接响应,直接return的话,不会执行视图函数。
- after_request:在执行完视图函数之后会调用,并把视图函数生成的响应传入,可以在此方法中对响应做最后一步同意的处理。
- teardown_request:每一次请求后都会调用,会接收一个参数——服务器出现的错误信息。
写一个程序来验证下钩子函数的执行流程(运行后,访问两次):
127.0.0.1 - - [30/Aug/2018 10:53:42] "GET / HTTP/1.1" 200 - before_first_request before_request after_request teardown_request 127.0.0.1 - - [30/Aug/2018 10:53:45] "GET / HTTP/1.1" 200 - before_request after_request teardown_request
11、上下文
上下文相当于一个容器,保存了Flask程序运行过程中的一些信息,根据管理机制分为两种:
- 请求上下文(RequestContext)Request:请求的对象,封装了Http请求的内容;Session:根据请求中的cookie,重载访问者相关的会话信息。
- 程序上下文(AppContext)g:处理请求时用作临时存储的对象,保存的是当前请求的全局变量,不同的请求会有不同的全局变量!current_app:当前运行程序的程序实例,保存的是应用程序的变量,比如可以使用current_app.name获取当前应用的名称, 也可以在current_app中存储一些配置信息,变量等,使用示例:current_app.text = 'value'。
Flask中,而关于上下文的管理可以分为三个阶段:
- 请求进来时:将request,session封装到RequestContext类中,app, g封装在AppContext类中, 并通过LocalStack将RequestContext和AppContext放入Local类中。
- 视图函数:通过localproxy -> 偏函数 -> localstack -> local取值。
- 请求结束前:执行save.session() -> 各自执行pop() -> 清除local中的数据。
12、异常处理
在开发中,后台发生异常,但又不想把异常显示给用户看,或者需要同一处理的时候,可以使用abort()函数 主动抛出异常,再捕获异常,然后返回一个美化后的页面,最常见的就是404了,代码示例如下:
@app.route("/test") def test(): abort(404) @app.errorhandler(404) def error(e): return "一个精美的404页面"
代码执行后,浏览器键入:http://127.0.0.1:5000/test,可以看到如图所示的结果:
另外,你也可以把所有异常处理些写到一个蓝图中,代码示例如下:
# coding=utf-8 from flask import Blueprint, abort exception = Blueprint('exception', __name__) @exception.errorhandler(404) def error(e): return "一个精美的404页面" # 注册蓝图 from error import exception app.register_blueprint(exception, url_prefix='/error')
13、ORM框架——SQLAlchemy
使用对象映射关系(Object-Relational Mapper)ORM框架来操作数据库。所谓的ORM框架就是:
「将底层的数据操作指令抽象成高层的面向对象操作」
不用再写繁琐的SQL操作语句,利用ORM框架可以简化成对Python对象的操作。
表映射成类
,行作为实例
,字段作为属性
.
ORM在执行对象操作时会将对应操作转换为数据库原生语句。Python中用得最广泛的ORM框架是SQLAlchemy。
0x1 安装flask-sqlalchemy
直接使用pip命令安装即可
pip install flask-sqlalchemy
另外SQLAlchemy本身无法操作数据库,依赖于pymysql等第三方库,里面有个Dialect模块专门用于 和数据API交流,根据配置文件的不同而调用不同的数据库API,从而实现对数据库的操作。数据库使用 URL限定,常见的数据库引擎与其对应的URL如下表所示:
数据库引擎 | URL |
MySQL | mysql+pymysql://username:password@hostname/database |
Postgres | postgresql://username:password@hostname/database |
SQLite (Unix,开头四个斜线) | sqlite:////absolute/path/to/database |
SQLite (Windows) | sqlite:///c:/absolute/path/to/database |
Postgres | postgresql://username:password@hostname/database |
Oracle | oracle://username:password@hostname/database |
参数简述:
- username:登录数据库的用户名。
- password:登录数据库的密码。
- hostname:SQL服务所在的主句,可以是本地也可以是远程。
- database:使用的数据库。
0x2 连接数据库
连接MySQL数据库的代码示例如下:
from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://jay:zpj12345@localhost:3306/todo') with engine.connect() as con: rs = con.execute("SELECT 1") print(rs.fetchone())
输出结果如下:
(1,)
另外还可以通过下面这样的方法来初始化SQLAlchemy,代码示例如下:
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() app = Flask(__name__) db.init_app(app)
0x3 使用原生SQL
sqlalchemy支持直接执行原生的SQL语句,代码示例如下:
from sqlalchemy import create_engine engine = create_engine('mysql+pymysql://jay:zpj12345@localhost:3306/todo') with engine.connect() as con: con.execute("CREATE TABLE IF Not Exists note(_id INT AUTO_INCREMENT PRIMARY KEY, tran TEXT, status int)") con.execute("INSERT INTO note(tran, status) VALUES ('吃饭', 1)") con.execute("INSERT INTO note(tran, status) VALUES ('睡觉', 1)") rs = con.execute("SELECT * FROM note") for row in rs: print(row)
代码输出结果如下:
(1, '吃饭', 1) (2, '睡觉', 1)
0x4 ORM模型与基本操作
演示一波Flask-SQLAlchemy的基本操作:
1.建表(create_all,对应的删除表可以用drop_all)
# models.py from manager import db class Note(db.Model): __tablename__ = 'note' _id = db.Column(db.INTEGER, primary_key=True, autoincrement=True) tran = db.Column(db.TEXT) status = db.Column(db.INT,default=0) db.create_all() # 创建表
2.插入数据
from model import Note from manager import db def create_note(): # 创建一个新对象 note1 = Note() note1.tran = "吃饭" note1.status = "1" note2 = Note() note2.tran = "睡觉" note2.status = "2" # 将新建笔记添加到数据库会话中 db.session.add(note1) db.session.add(note2) # 将数据库会话中的变动提交到数据库中,如果不commit,数据库内容是不会变化的 db.session.commit() create_note()
3.删除数据
def delete_note(): # 获取笔记对象(这里是获取_id=1的记录) note = Note.query.filter_by(_id=1).first() # 删除笔记 db.session.delete(note) # 提交数据库会话 db.session.commit() delete_note()
4.修改数据
def update_note(): # 获取笔记对象(这里是获取_id=2的记录) note = Note.query.filter_by(_id=2).first() # 修改笔记内容 note.tran = "打豆豆" # 提交数据库会话 db.session.commit() update_note()
5.查询数据
说到查询,必然有查询条件,SQLAlchemy提供了如下表所示的常用查询函数:
函数 | 描述 | 使用示例 |
filter_by | 精确查询 | filter_by(xxx='xxx') |
filter | 模糊查询 | filter(xxx.endWith('xxx')) |
get(主键) | 根据主键查询,一般为id | get(1) |
not_() | 逻辑非,也可以直接把==换成!= | not_(xxx='xxx') |
and_() | 逻辑与 | and_(xxx='xxx') |
or_() | 逻辑或 | or_(xxx='xxx') |
in_() | 在某个范围里 | XXX.xxx.in_((1,2,3)) |
notin_() | 不在某个范围内 | XXX.xxx.notin_((1,2,3)) |
first() | 返回查询到的一个对象 | XXX.query.first() |
all() | 返回查询到的所有对象 | XXX.query.all() |
order_by() | 排序 | XXX.order_by(xxx.xxx.desc()) |
limit() | 限制返回条数 | XXX.limit(3) |
offset() | 设置偏移量 | XXX.offset() |
count() | 返回记录的总条数 | xxx.count() |
代码示例如下:
from sqlalchemy import not_, or_ def query_all(): notes = Note.query.all() print("查询全部数据:", notes) note = Note.query.filter(not_(or_(Note.tran == '吃饭', Note.tran == '睡觉'))).first() print(note._id, ":", note.tran, ":", note.status) if __name__ == '__main__': # 先插入几条数据 create_note() create_note() query_all()
输出结果如下:
查询全部数据: [<Note 2>, <Note 3>, <Note 4>, <Note 5>, <Note 6>] 2 : 打豆豆 : 2
14.Web表单插件——Flask-WTF
Flask中一般不会直接用原始表单,而是通过Flask-WTF扩展,它简单继承了WTForms,包括CSRF (跨域请求伪造),验证表单数据的功能,文件上传以及Google内嵌的验证码。
0x1 WTForms支持的HTML标准字段
字段类型 | 说明 |
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
DateField | 文本字段,值为datetime.date格式 |
DateTimeField | 文本字段,值为datetime.datetime格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为True和False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
0x2 WTForms验证函数
验证函数 | 说明 |
验证电子邮件地址 | |
EqualTo | 比较两个字段的值,常用于要求输入两次密码进行确认的情况 |
IPAddress | 验证IPv4网络地址 |
Length | 验证输入字符串的长度 |
NumberRange | 验证输入的值在数字范围内 |
Optional | 无输入值时跳过其他验证函数 |
Required | 确保字段中有数据 |
Regexp | 使用正则表达式验证输入值 |
URL | 验证URL |
AnyOf | 确保输入值在可选值列表中 |
NoneOf | 确保输入值不在可选列表中 |
0x3 写个简单的例子
接着我们来编写一个注册表单的例子:
# coding=utf-8 from flask import Flask, request, render_template from flask_wtf import FlaskForm # 导入自定义表单需要的字段 from wtforms import SubmitField, StringField, PasswordField # 导入表单验证 from wtforms.validators import DataRequired, EqualTo app = Flask(__name__) app.config['SECRET_KEY'] = '123456' # 自定义表单类 # StringField和PasswordField用于区分文本框类型 # 第一个参数是label值,第二个参数validators是要验证的内容 class RegisterForm(FlaskForm): username = StringField('用户名:', validators=[DataRequired()]) password = PasswordField('密码:', validators=[DataRequired()]) password2 = PasswordField('确认密码:', validators=[DataRequired(), EqualTo('password', '两次输入的密码不一致!')]) submit = SubmitField('注册') @app.route("/register", methods=['GET', 'POST']) def register(): # 实例化注册表单类 register_from = RegisterForm() if request.method == 'POST': # 获取请求参数参数 username = request.form.get('username') password = request.form.get('password') password2 = request.form.get('password2') # 调用validation_on_submit,一次性执行完所有验证函数的逻辑 if register_from.validate_on_submit(): return '注册成功!' else: return '前后密码不一致!' return render_template('register.html', form=register_from) if __name__ == "__main__": print(app.url_map) app.run(debug=True)
# templates/register.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post"> {# 设置scrf_token #} {{ form.csrf_token() }} {{ form.username.label }} {{ form.username }}<br> {{ form.password.label }} {{ form.password }}<br> {{ form.password2.label }} {{ form.password2 }}<br> <br> {{ form.submit }} </form> </body> </html>
输入一波一样的密码和不一样的密码,浏览器的输出结果如下所示:
另外有一点要注意:
使用Flask-WTF需要配置参数SECRET_KEY,CSRF_ENABLED是为了**CSRF(跨站请求伪造)**保护。 SECRET_KEY用于生成加密令牌,当CSRF激活的时候,该设置会根据设置的密钥生成加密令牌。
15.一个简单通用的Flask项目结构
Flask基础学得差不多了,接着我们来规范下项目结构,一个比较简单通用的项目结构如图:
简述下结构:
- app:整个项目的包目录。
- models:数据模型。
- static:静态文件,css,JavaScript,图标等。
- templates:模板文件。
- views:视图文件。
- config.py:配置文件。
- venv:虚拟环境。
- manage.py:项目启动控制文件。
- requirements.txt:项目启动控制文件。
创建流程:
在__init__.py
中初始化app实例,代码如下:
from flask import Flask app = Flask(__name__)
views.py中写个简单的index路由:
from app import app from flask import render_template @app.route('/') def index(): return render_template("index.html")
templates文件夹创建一个index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>Hello Flask!</h1> </body> </html>
接着键入:python manage.py runserver,浏览器打开:http://127.0.0.1:5000/
项目结构基本就弄好了,接着我们把项目丢到远程服务器上。
16.把Flask项目部署到云服务器上
0x1 代码上传至云服务器
有两种可选的方法,最简单的就是通过FTP/SFTP工具直接传。另外一种是把项目托管到 类似于Github这类代码托管平台,然后直接ssh连云服务器,通过git clone命令把项目 拷贝到服务器上,这里笔者直接用的第一种方法,把项目上传到云服务器上。
0x2 安装nginx
Nginx是一款轻量级、性能强、占用资源少,能很好的处理高并发的反向代理软件。 Flask自带的Web Server只能开单线程,自己测试还行,放到线上就不行了,这里 我们用到Nginx,直接通过apt-get命令进行安装,命令如下:
apt-get install nginx
安装完后,外网访问服务器的公网ip,出现下述页面说明安装成功:
Nginx安装完,会默认创建一个目录:/var/www/,直接通过命令把我们的 项目移动到这个路径下。
mv AutoPubNews /var/www/AutoPubNews
0x3 安装配置uwsgi
WSGI是一种WEB服务器,或者叫网关接口,Web服务器(如nginx)与应用服务器 (如uWSGI)通信的一种规范(协议)。而uWSGI实现了WSGI的所有接口,是一个 快速、自我修复、开发人员和系统管理员友好的服务器。uWSGI代码完全用C编写, 效率高、性能稳定。举个例子:uWSGI把HTTP协议转化成WSGI协议,让Python可以 直接使用。直接键入pip命令安装:
pip install uwsgi
一般都是能直接安装完成的,如果出错了可以试试先安装libpython3.x-dev, 比如笔者的版本是3.5:
apt-get install libpython3.5-dev
接着配置一下uwsgi,在项目里新建一个config.ini作为配置文件:
vim config.ini
添加下述内容:
[uwsgi] # uwsgi 启动时所使用的地址与端口 socket = 127.0.0.1:8001 # 可以使用其他端口 # 指向网站目录 chdir = /var/www/AutoPubNews # python 启动程序文件 wsgi-file = manage.py # python 程序内用以启动的 application 变量名 callable = app # 处理器数 processes = 4 # 线程数 threads = 2 #状态检测地址 stats = 127.0.0.1:5000
接着执行下述命令:
uwsgi config.ini
出现: Stats server enabled on 127.0.0.1:5000,代表正常启动。
0x4 配置Nginx
接着配置下Nginx,不要去动默认的nginx.conf,直接将:/etc/nginx/sites-available/default
覆盖掉,新建default文件,添加下述内容:
server { listen 80; server_name _; location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; # 指向uwsgi 所应用的内部地址,所有请求将转发给uwsgi 处理 uwsgi_param UWSGI_PYHOME /home/www/AutoPubNews/venv; # 指向虚拟环境目录 uwsgi_param UWSGI_CHDIR /home/www/AutoPubNews; # 指向网站根目录 uwsgi_param UWSGI_SCRIPT manager:app; # } }
接着键入下述命令重启加载下nginx配置:
sudo service nginx restart
接着启动uwsgi,然后就可以通过服务器的公网ip直接访问我们的项目了:
0x5 域名解析
每次访问项目都用ip,显得有些繁琐,我们可以买个域名做下映射耍耍。 域名直接买就好,需要备案,搞定后打开域名管理页,找到刚买的域名 点击解析
然后点击「添加记录」会出现如图所示的对话框,记录值那里填你的云服务器公网ip即可。
此时就可以直接通过域名来访问我们的项目了。
行吧,关于Flask速成就这么都,下节我们来利用Flask编写API接口,动态生成页面等。 有疑问的欢迎在评论区留言,谢谢~