Flask-论坛开发-4-知识点补充
对Flask感兴趣的,可以看下这个视频教程:http://study.163.com/course/courseLearn.htm?courseId=1004091002
1. WTForms 表单使用
WTForms
是一个支持多 web
框架的一个插件,主要功能有两个:第一个是做表单的验证,验证用户提交上来的信息是否合法,第二个是模板渲染。
1.1 WTForms 表单验证的基本使用
使用 WTForms
进行表单验证,会更好的管理我们的代码和项目结构,还可以大大提高开发项目时的效率。WTForms
功能强大,将表单定义成一个类,可以实现对表单字段的丰富限制。
使用 WTForms
实现表单验证的功能,主要有以下步骤:
-
从
wtforms
中导入Form
这个类,以及相关字段的数据类型from wtforms import From,StringField,IntegerField,FileField # Form 是一个基类,StringField 用来验证 String 类型的数据
-
从
wrforms.validators
导入一些限制对象(如长度限制)from wrforms.validators import Length,EqualTo # # wrforms.vaildators 是一个验证器,包含 Length 在内的多种验证限制,Length 则专门对参数的长度进行验证,EqualTo 指定必须要和某个值相等
-
创建表单类并继承自
Form
,定义相关字段class RegistForm(Form): # 该类用来验证表单中传递的参数,属性名和参数名必须一致 username = StringField(validators=[Length(min=3,max=10,message='用户名长度必须在3到10位之间')]) # StringField 必须传入关键字参数 validators,且 validators 是一个 List 类型(此处仅对长度作验证) password = StringField(validators=[Length(min=6,max=16)]) password_repeat = StringField(validators=[Length(min=6,max=16),EqualTo('password')]) # 验证长度和相等
-
在视图函数中使用该 RegistForm
form = RegistForm(request.form) # request.form 会拿到所有提交的表单信息 if form.validate(): # form.validate() 方法会匹配表单信息并返回 True 或 False return '注册成功!' else: return '注册失败!'
完整代码如下:
# regist.html
<form action="" method="post">
<table>
<tbody>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>确认密码:</td>
<td><input type="password" name="password_repeat"></td>
</tr>
<tr>
<td></td>
<td><input type="submit" value="点击提交"></td>
</tr>
</tbody>
</table>
</form>
# 后端程序
from wtforms import Form,StringField
from wtforms.validators import Length,EqualTo
class RegistForm(Form):
username = StringField(validators=[Length(min=3,max=10,message='输入的用户名不符合长度规范')])
password = StringField(validators=[Length(min=6,max=16)])
password_repeat = StringField(validators=[Length(min=6,max=16),EqualTo('password')])
@app.route('/regist/',methods=['GET','POST'])
def regist():
if request.method == 'GET':
return render_template('regist.html')
else:
form = RegistForm(request.form)
if form.validate():
return '注册成功'
else:
print(form.errors)
for message in form.errors:
return '注册成功'
1.2 WTForms 的相关验证器
除了上面使用到的两个验证器(StringField
和EqualTo
)外,WTForms 中还有很多常用的验证器:
-
Email
:验证上传的数据是否为邮箱(格式)email = StringField(validators=[email()])
-
EqualTo
:验证上传的数据是否与另一个字段相等,常用在注册时的两次密码输入上password_repeat = StringField(validators=[Length(min=6,max=16),EqualTo('password')])
-
InputRequired
:该字段必须输入参数,且只要输入了,那么该字段就是True
。如果不是特数据情况,应该使用InputRequired
password = StringField(validators=[InputRequired()]) # 不管你的值是什么,只要输入了就是 True
-
Length
:长度限制,由min
和max
两个值进行限制password = StringField(validators=[Length(6,16)])
-
NumberRange
:数字的区间,由min
和max
两个值进行限制(包括min
和max
)age = IntegerField(validators=[NumberRange(12,100)])
-
Regexp
:自定义正则表达式,比如手机号码的匹配phone = StringField(validators=[Regexp(r'1[34578]\d{9}')])
-
URL
:必须要是URL
的形式homepage = StringField(validators=[URL()])
-
UUID
:验证UUID
uuid = StringField(validators=[UUID()])
注意在使用验证器的时候,后面要加上 ()
。
1.3 自定义验证器
如果以上介绍的验证器不满足项目当中的需求,那么还可以根据需求自定义相关的验证器。如果想要对表单中的某个字段进行更加细致的验证,那么可以根据需求对该字段定进行单独的验证,步骤如下:
- 在表单验证类中定义一个方法,方法的命名规则为:
validate_字段名(self,field)
。 - 在方法中使用
field.data
获取到用户上传到这个字段上的值。 - 对于验证的判断:若验证成功,可以什么都不做;若验证失败,则必须跑出
wtforms.validators.ValidationError
异常,并填入验证失败的原因。
示例代码如下所示:
from wtforms import Form,StringField
from wtforms.validators import Length,ValidationError
class LoginForm(Form):
captcha = StringField(validators=[Length(4,4)])
def validate_captcha(self,field): # 用 validate_captcha 来指定该验证器是针对 captcha 字段的
if field.data != 'aw7e':
raise ValidationError('验证码输入错误!')
1.4 WTForms 渲染模板
这个功能可以让我们的前端代码少写一点点,但是实际上用处不大。主要使用方法如下:
-
在
forms
文件中定义一个表单类:class SettingsForms(Form): username = StringField(validators=[Length(4,10)])
-
在视图函数中返回模板时传递相关参数:
@app.route('/settings/',methods=['GET','POST']) def Settings(): if request.method == 'GET': form = SettingsForms() return render_template('settings.html',my_form=form) else: pass
-
在前端模板中调用
<form action="" method="post"> <table> <tbody> <tr> <td>{{ my_form.username.label }}</td> <td>{{ my_form.username() }}</td> </tr> <tr> <td></td> <td><input type="submit" value="提交"></td> </tr> </tbody> </table> </form>
其中,第五第六两行相当于:
<td>用户名:</td> <td><input type="text" name='username'></td>
实际上,这个功能在生产环境中几乎没有任何作用,很鸡肋。
2. 文件上传和访问
2.1 文件上传
上传文件时需要注意以下几点:
-
在模板中,
form
表单内,要指定encotype='multipart/form-data'
才能实现文件的上传:<form action="" method="post" enctype="multipart/form-data"> ... </form>
-
在后台获取文件,需要使用
request.files.get('标签名')
才能获取到上传的文件:avatar = request.files.get('avatar')
-
保存文件使用
avatar.save(路径)
实现,推荐在保存文件时先对文件进行安全封装:from werkzueg.utils import secure_filename import os UPLOAD_PATH = os.path.join(os.path.dirname(__file__),'images') # UPLOAD_PATH = 当前路径/images avatar.save(UPLOAD_PATH,secure_filename(avatar.filename))
-
后台完整代码如下:
from werkzeug.utils import secure_filename import os UPLOAD_PATH = os.path.join(os.path.dirname(__file__),'images') # 定义文件保存路径:UPLOAD_PATH = 当前路径/images @app.route('/upload/',methods=['GET','POST']) def upload(): if request.method == 'GET': return render_template('upload.html') else: avatar = request.files.get('avatar') filename = secure_filename(avatar.filename) # 对文件名进行安全过滤 avatar.save(os.path.join(UPLOAD_PATH,filename)) desc = request.form.get('desc') print(desc) return '上传成功!'
2.2 文件访问
实现了文件上传,那么用户肯定会需要对文件进行访问。在 Flask
中,实现文件的访问必须要定义一个单独的 url
与视图函数的映射,并且要借助 send_from_directory
方法返回文件给客户端。
-
从
flask
导入send_from_directory
from flask import send_from_directory
-
定义视图函数并映射到文件的 url
UPLOAD_PATH = os.path.join(os.path.dirname(__file__),'images') @app.route('/getfile/<filename>/') def getfile(filename): return send_from_directory(UPLOAD_PATH,filename) # send_from_directory 要传入路径和文件名 # 用户可以访问 http://domainname/filename 对文件进行访问
2.3 使用验证器对验证上传的文件
在验证文件的时候,同样要定义一个验证的类,然后用该验证类去验证上传的文件。主要分为以下几个步骤:
-
导入
FileField
和文件验证器:FileRequired
、FileAllowed
from forms import FileField from flask_wtf.file import FileRequired,FileAllowed # 注意这两个针对文件的验证器是从 flask_wtf_file 中导入的,而不是从之前的 wtforms.validators 中导入
-
定义表单类并继承自
Form
,然后定义相关字段class UpLoadForm(Form): avatar = FileField(validators=[FileRequired(),FileAllowed(['jpg','png','gif'])]) # FileRequired() 要求必须传入文件,FileAllowed() 则指定了允许的文件类型 desc = StringField(validators=[InputRequired()])
-
在主
app
文件中引用from werkzeug.datastructures import CombinedMultiDict # CombinedMultiDict 用来合并两个不可变的 dict form =UpLoadForm(CombinedMultiDict([request.form,request.files])) # 传入用户提交的信息,其中 request.form 是表单中的信息,request.files 是上传的文件
-
完整代码如下:
# forms.py 文件 from wtforms import Form,StringField,FileField from flask_wtf.file import FileRequired,FileAllowed class UpLoadForm(Form): avatar = FileField(validators=[FileRequired(),FileAllowed(['jpg','png','gif'])]) desc = StringField(validators=[InputRequired()]) # 主 app 文件 from forms import UpLoadForm from werkzeug.utils import secure_filename from werkzeug.datastructures import CombinedMultiDict import os UPLOAD_PATH = os.path.join(os.path.dirname(__file__),'images') @app.route('/upload/',methods=['GET','POST']) def upload(): if request.method == 'GET': return render_template('upload.html') else: form =UpLoadForm(CombinedMultiDict([request.form,request.files])) if form.validate(): avatar = request.files.get('avatar') filename = secure_filename(avatar.filename) avatar.save(os.path.join(UPLOAD_PATH,filename)) desc = request.form.get('desc') print(desc) return '上传成功!' else: return '上传失败!'
3. Cookie 的使用
3.1 设置 Cookie
设置 Cookie
是 Response
类中有的方法,用法是:在视图函数中
resp = Response('MYYD') # 创建一个 Response 对象,传入的字符串会被显示在网页中
resp.set_cookie('username','myyd')
return resp
其中,set_cookie
() 中的参数有:
key 键
value 值
max_age IE8 以下不支持,优先级比 expires 高
expires 几乎所有浏览器都支持,必须传入 datetime 的数据类型,并且默认加 8 个小时(因为我们是东八区)
path 生效的 URL,'/' 代表该域名下所有 URL 都生效,一般默认就好
domian 域名,若没设置,则只能在当前域名下使用
secure 默认 False,若改为 True 则只能在 https 协议下使用
httponly 默认 False,若改为 True 则只能被浏览器所读取,不能被 JavaScript 读取(JavaScript可以在前端处理一些简单逻辑)
使用时依次传入即可,如果有些选项要跳过则需要指定一下参数名。
完整代码如下所示:
from flask import Flask,Response
app = Flask(__name__)
@app.route('/')
def hello_world():
resp = Response('首页')
resp.set_cookie('username','MYYD')
return resp
if __name__ == '__main__':
app.run()
3.2 删除 Cookie
删除 Cookie
时需要另外指定一条 URL
和视图函数,也是使用 Response
来创建一个类,并使用 resp.delete_cookie()
来完成这个需求。代码如下所示:
from flask import Flask,Response
app = Flask(__name__)
@app.route('/delCookie/')
def delete_cookie():
resp = Response('删除Cookie')
resp.delete_cookie('username')
return resp
if __name__ == '__main__':
app.run()
3.3 设置 Cookie 的有效期
设置 Cookie
的有效期,可以有两种方法:使用 max_age
或 expires
。
-
使用
max_age
使用 max-age 时要注意,max-age 不支持 IE8 及以下版本的浏览器,并且只能相对于现在的时间往后进行推迟(单位是秒s),而不能指定具体的失效时间。使用方法如下代码所示:
resp.set_cookie('username','myyd',max_age=60) # 设置该 cookie 60s 之后失效。
-
使用
expires
使用
expires
时要注意,必须要使用格林尼治时间,因为最后会自动加上 8 小时(中国是东八区)。expires
的兼容性要比max_age
要好,尽管在新版的http
协议中指明了expires
要被废弃,但现在几乎所有的浏览器都支持expires
。expire
设置失效时间,可以针对当前时间往后推移,也可以指定某一个具体的失效时间。具体如下所示:-
针对当前时间推移
from datetime import datetime,timedelta expires = datetime.now() + timedelta(days=30,hours=16) # 当下时间往后推移 31 天失效,注意这里给的参数是减了 8 小时的 resp.set_cookie('username','MYYD',expires=expires)
-
指定具体日期
from datetime import datetime resp = Response('首页') expires = datetime(year=2018,month=12,day=30,hour=10,minute=0,second=0) # 实际上的失效时间是 2018-12-30-18:0:0 resp.set_cookie('username','MYYD',expires=expires) return resp
-
-
其他注意事项
此外,还要注意几点:
- 当同时使用
max_age
和expires
的时候,会优先使用max_age
指定的失效时间 - 若同时不使用
max_age
和expires
的时候,默认的cookie
失效时间为浏览器关闭的时间(而不是窗口关闭的时间) expires
要设置为格林尼治时间,同时导入datetime.datetime
和datetime.timedelta
- 当同时使用
4. RFCS
防范 CSRF 攻击的措施:
实现:在返回一些危险操作的页面时,同时返回一个 csrf_token
的 cookie
信息,并且在返回的页面表单中也返回一个带有 csrf_token
值的 input
标签。
原理:当用户提交该表单时,若表单中 input
标签的 csrf_token
值存在并且和 cookie
中的 csrf_token
值相等则允许操作;若不满足该条件,则操作不被允许。
原因:因为 csrf_token
这个值是在返回危险操作页面时随机生成的,黑客是无法伪造出相同的 csrf_token
值的,因为黑客不能操作非自己域名下的 cookie
,即不知道 cookie
中的 csrf_token
值的内容。
具体实现:
主app文件:
- from flask_wtf import CSRFProtect
- CSRFProtect(app)
模板文件(表单中):
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
注意这里是要在所有危险操作页面的表单内都需要加入。
浏览器:F12 -> Network -> Disable Cache
// 整个文档加载完毕后才会执行这个函数
$(function () {
$('#submit').click(function (event) {
// 阻止默认的表单提交行为
// event.preventDefault();
var email = $('input[name=email]').val();
var password = $('input[name=password]').val();
var csrftoken = $('input[name=csrf_token]').val();
// $.post() 方法用来提交表单
$.post({
'url':'/login/',
'data':{
'email': email,
'password': password,
'csrftoken': csrftoken
},
'success':function (data) {
console.log(data);
},
'fail':function (error) {
console.log(error);
}
});
})
});
5. Flask Restful
5.1 Restful API 介绍
Restful API
是用于在前端与后台进行通信时使用的一套传输规范,这些规范可以使后台开发变得更加轻松。
其采用的协议是 http
或 https
。
传输数据格式采用 json
而不是 xml
。使用 json
传输数据会变得更加简单高效,而不是像 xml
那样伴随有众多的固定代码(类似于 html
的格式),即每次传输时 xml
占的资源更多。
并且其url 链接中,不能包含动词,只能包含名词;并且对于名词,若出现复数,则必须加上 s
。
HTTP 的请求方法主要有以下 5
种,但实际上 get
和 post
就够用了。
get
:获取服务器上的一个资源post
:在服务器上创建一个紫爱云put
:在服务器上更新资源(客户端需要提交更新后的所有数据)patch
:在服务器上更新资源(客户端只需要提交所更新的数据)delete
:在服务器上删除一个资源
5.2 Flask-Restful 插件
-
安装
Flask-Restful
需要在Flask 0.8
以上版本运行,在python 2.6
以上版本运行,通过pip install flask-restful
即可安装。 -
使用
使用之前必须从
flask_restful
中导入Api
和Resource
;然后用Api
将初始化的app
绑定起来;再定义一个类视图,定义类视图必须继承自Resource
;最后用add_resource
方法将接口(URL
)与视图绑定起来。完整代码如下:from flask import Flask from flask_restful import Api,Resource # Api 用来绑定 app,Resource 用来创建类视图 app = Flask(__name__) api = Api(app) class LoginView(Resource): def post(self): # 定义了什么样的方法,才能用什么样的请求 return {'username':'MYYD'} # 可以直接返回字典类型的数据(因为字典数据已经自动转换成Json格式了) api.add_resource(LoginView,'/login/',endpoint='login') # 映射类视图和接口,endpoint 用来指定 url_for 反转到类视图时的关键字 if __name__ == '__main__': app.run()
-
注意事项:
- 映射类视图和接口时不指定
endpoint
,则进行url_for
反转时默认使用视图名称的小写,即上例中的loginview
。
add_resource
方法的第二个参数,用来指定访问这个类视图的接口,与之前不同的是,这个地方可以传入多个接口。
- 映射类视图和接口时不指定
5.3 Flask-Restful 参数验证
-
基本使用
Flask-Restful
插件为我们提供了类似之前的WTForm
表单验证的包,可以用来验证提交的数据是否合法,叫做reqparse
。基本用法如下(3步骤):parser = reqparse.RequestParser() # 初始化一个 RequestParser 对象 parser.add_argument('password',type=int,help='password input error') # 指定验证的参数名称,类型以及验证不通过时的提示信息 args = parser.parse_args() # 执行验证
完整代码如下:
from flask_restful import Api,Resource,reqparse class LoginView(Resource): def post(self): # post 方法提交数据时传入的 username 和 password,这里不需要定义 parser = reqparse.RequestParser() parser.add_argument('username',type=str,help='用户名格式错误') # 如果提交数据时没传入,默认为 None parser.add_argument('age',type=int,help='密码错误') args = parser.parse_args() print(args) return {'username':'MYYD'}
-
add_argument
解析在使用
add_argument
对上传的数据进行验证时,可以根据需求使用不同的选项进行验证,常用的选项有:default
:默认值,如果没有传入该参数,则使用default
为该参数指定默认的值。required
:置为True
时(默认为False
),该参数必须传入值,否则抛出异常。type
:指定该参数的类型,并进行强制转换,若强制转换失败则抛出异常。choices
:相当于枚举类型,即该传入的参数只能为choices
列表中指定的值。help
:当验证失败时抛出的异常信息。trim
:置为True
时对上传的数据进行去空格处理(只去掉字符串前后的空格,不去掉字符串之间的空格)。
其中,
type
选项除了可以指定python
自带的一些数据类型外,还可以指定flask_restful.inputs
下的一些特定类型来进行强制转换。常用的类型如下:url
:会判断上传的这个参数是不是一个url
,若不是则抛出异常。regex
:会判断上传的这个参数是否符合正则表达式中的格式,若不符合则抛出异常。date
:将上传的这个参数强制转换成datetime.date
类型,若转换不成功则抛出异常。
在使用
type
指定flask_restful.inputs
数据类型时的用法如下:parser.add_argument('birthday',type=inputs.date,help='日期输入错误')
5.4 Flask-Restful 类视图返回内容
返回数据时候可以使用最原始的方法,返回一个字典。但是 Restful
推荐我们使用 Restful
方法,如下:
-
先定义一个字典,该字典定义所有要返回的参数
-
再使用
marshal_with
(字典名) 传入字典名称 -
最后返回数据就行了,如下:
from flask_restful import Api,Resource,fields,marshal_with api = Api(app) class Article(object): def __init__(self,title,content): self.title = title self.content = content artilce = Article('MYYD','wuba luba dub dub') class LoginView(Resource): resource_field = { 'title': fields.String, 'content': fields.String } @marshal_with(resource_field) def get(self): return artilce # 可以直接返回 Article 的实例,会拿到 article 对象的两个属性并返回 api.add_resource(LoginView,'/login/',endpoint='login')
这样做的好处是:
- 可以少写代码
- 可以规范输出,即如果
article
对象只有title
属性而没有content
属性,也会返回content
的值,只不过该值被置为None
。
5.5 Flask-Restful 标准返回
5.5.1 复杂结构
对于一个类视图,可以指定好一些数据字段用于返回。指定的这些数据字段,在此后使用 ORM
模型或者自定义模型时,会自动获取模型中的相应字段,生成 Json
数据,并返回给客户端。对于拥有子属性的字段而言,若想成功获取其属性并返回给客户端,需要引用 fields.Nested
并在其中定义子属性的字段。整个例子如下:
-
模型关系
class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer,primary_key=True) username = db.Column(db.String(50),nullable=False) email = db.Column(db.String(50),nullable=False) article_tag_table = db.Table( 'article_tag', db.Column('article_id',db.Integer,db.ForeignKey("article.id"),primary_key=True), db.Column('tag_id',db.Integer,db.ForeignKey("tag.id"),primary_key=True) ) class Article(db.Model): __tablename__ = 'article' id = db.Column(db.Integer,primary_key=True) title = db.Column(db.String(50),nullable=False) content = db.Column(db.Text) author_id = db.Column(db.Integer,db.ForeignKey('user.id')) author = db.relationship('User',backref='articles') tags = db.relationship('Tag',secondary=article_tag_table,backref='articles') class Tag(db.Model): __tablename__ = 'tag' id = db.Column(db.Integer,primary_key=True) name = db.Column(db.String(50),nullable=False)
-
返回时定义的数据字段
注意这里有三点必须实现:
- 导入相关包并初始化
app
- 定义返回数据的字段
- 使用装饰器
marshal_with
传入定义的数据字段
from flask_restful import Api,Resource,fields,marshal_with api = Api(app) class ArticleView(Resource): article_detail = { 'article_title': fields.String(attribute='title'), 'content': fields.String, 'author': fields.Nested({ # 返回有子属性的字段时要用 fields.Nested() 'username': fields.String, 'email': fields.String, 'age': fields.Integer(default=1) }), 'tags': fields.Nested({ # 返回有子属性的字段时要用 fields.Nested() 'name': fields.String }) } @marshal_with(article_detail) def get(self,article_id): article = Article.query.filter_by(id=article_id).first() return article
- 导入相关包并初始化
5.5.2 重命名属性
重命名属性很简单,就是返回的时候使用不同于模型本身的字段名称,此操作需要借助 attribute
选项。如下所示代码:
article_detail = {
'article_title': fields.String(attribute='title')
}
Article
模型中的属性原本是 title
,但是要返回的字段想要命名为 article_title
。如果不使用 attribute
选项,则在返回时会去 Article
模型中找 article_title
属性,很明显是找不到的,这样以来要返回的 article_title
字段会被置为 Null
。使用 attribute
选项后,当返回 article_title
字段时,会去 Article
模型中找 attribute
选项指定的 title
属性,这样就可以成功返回了。
5.5.3 默认值
当要返回的字段没有值时,会被置为 Null
,如果不想置为 Null
,则需要指定一个默认的值,此操作需要借助 default
选项。如下代码所示:
article_detail = {
'article_title': fields.String(attribute='title')
'readed_number': fields.Integer(default=0)
}
当想要返回一篇文章的阅读量时,若没有从模型中获取到该字段的值,若不使用 default
选项则该字段会被置为 Null
;若使用了该选项,则该字段会被置为 0
。
5.6 Flask-restful 细节
实际上,flask-restful
还可以嵌套在蓝图中使用,也能返回一个 html
模板文件。
-
嵌套蓝图使用
搭配蓝图使用时,在注册
api
时就不需要使用app
了,而是使用蓝图的名称,如下:article_bp = Blueprint('article',__name__,url_prefix='/article') api = Api(article_bp)
其他的和之前一样,不过要在主 app 文件中注册一下蓝图。
-
渲染模板
如果想使用
flask-restful
返回html
模板,则必须使用api.representation()
装饰器来转换返回数据的类型,并根据该装饰器定义一个函数,用于返回该模板,如下:from flask import render_template,make_response @api.representation('text/html') def outPrintListForArticle(data,code,headers): # 这里要传入这三个参数 resp = make_response(data) # 其中,data 就是模板的 html 代码 return resp class ListView(Resource): def get(self): return render_template('list.html') api.add_resource(ListView,'/list/',endpoint='list')