Web后端学习笔记 Flask(8) WTForms 表单验证,文件上传
Flask-WTF是简化了WTForms操作的一个第三方库。WTForms表单的两个主要功能是验证用户提交数据的合法性以及渲染模板。同时还包含一些其他的功能。例如CSRF保护,文件上传等功能,安装flask-wtf也会默认安装WTForms,通过pip方式安装:
pip install flask-wtf
表单验证
1. 自定义一个表单类,继承自wtform.Form类。
2. 定义好需要验证的字段,字段的名字必须和模板中input标签的name字段名字相同
3. 在需要验证的字段上,需要指定好具体的数据类型
4. 在相关的字段上,指定好验证器
5. 在视图中就只需要表单类的对象,进行表单验证。
from wtforms import Form, StringField from wtforms.validators import Length, EqualTo # 表单验证类 class RegisterForm(Form): # 导入验证器 # message为验证错误时抛出的错误信息 username = StringField(validators=[Length(min=3, max=10, message="用户名长度必须3到10位")]) password = StringField(validators=[Length(min=6, max=10, message="密码长度必须6到10位")]) repeat_password = StringField(validators=[Length(min=6, max=10, message="密码错误"), EqualTo("password")])
视图函数的验证:
@app.route('/register/', methods=["GET", "POST"]) def register(): """ 表单验证 :return: """ if request.method == "GET": return render_template("html/register.html") else: form = RegisterForm(request.form) if form.validate(): return "success" else: print(form.errors) # 获取具体的错误信息 return "Fail"
WTForm常用的验证器:
Email:验证上传的数据是否为邮箱
EqualTo:验证上传的数据是否和另一个字段相等
InputRequired:原始数据的需要验证,如果不是特殊情况,应该使用InputRequired,即指定这个字段必须是要传的,否则会报错
Length:长度限制
NumberRange:数字的区间
Regexp:自定义正则表达式
URL:必须要是url的形式
UUID:验证uuid
# -*- coding: utf-8 -*- from wtforms import Form, StringField, IntegerField from wtforms.validators import Length, EqualTo, InputRequired, Email, NumberRange, URL, UUID, Regexp # 表单验证类 class RegisterForm(Form): # 导入验证器 # message为验证错误时抛出的错误信息 username = StringField(validators=[Length(min=3, max=10, message="用户名长度必须3到10位")]) password = StringField(validators=[Length(min=6, max=10, message="密码长度必须6到10位")]) repeat_password = StringField(validators=[Length(min=6, max=10, message="密码错误"), EqualTo("password")]) # 表单验证类 class LoginForm(Form): # email = StringField(validators=[Email(message="邮箱格式错误")]) username = StringField(validators=[InputRequired(message="用户名不能为空")]) age = IntegerField(validators=[NumberRange(20, 55)]) phone = StringField(validators=[Regexp(r"1[38745]\d{9}")])
自定义验证器:
当WTForm中的验证器不能满足业务需求的时候,用户可以自定义自己的验证器:
例如自定义验证码的验证器:
1. 定义一个方法,方法的命名规则是 validate_字段名(self,field)
2. 在方法中使用field.data可以获取到字段的具体值
3. 如果验证成功,那么可以什么都不做
4. 如果验证失败,需要抛出一个异常
# 表单验证类 class LoginForm(Form): # email = StringField(validators=[Email(message="邮箱格式错误")]) # username = StringField(validators=[InputRequired(message="用户名不能为空")]) # age = IntegerField(validators=[NumberRange(20, 55)]) # phone = StringField(validators=[Regexp(r"1[38745]\d{9}")]) captcha = StringField(validators=[Length(4, 4)]) # 首先验证长度 # 假设验证码为1234, 自定义captcha验证器,函数名必须以validate开头 # 所以在验证captcha的时候,除了验证Length,还会自动调用下面自定义的函数进行验证 def validate_captcha(self, field): # 针对具体的字段做验证 # 通过field.data获取数据 if field.data == "1234": return "Success" else: raise ValidationError("验证码错误")
WTForm渲染模板(不推荐使用)
WTForm可以结合jinja2模板渲染表单元素,例如可以渲染各种input, radio等标签
1. 首先,在form.py文件中定义相关的表单类,定义相关的字段
class SettingForm(Form): username = StringField("用户名", validators=[InputRequired()]) age = IntegerField("年龄", validators=[NumberRange(20, 60)]) remember = BooleanField("记住我: ") tags = SelectField("标签", choices=[("1", "Java"), ("2", "C++"), ("3", "Android")]
2. 在视图函数中,将实例化的form对象作为参数,传递到模板文件中:
@app.route('/settings/', methods=["GET", "POST"]) def setting(): if request.method == "GET": form = SettingForm() # 利用form渲染模板 return render_template("html/settings.html", form=form) # 传递参数 else: form = SettingForm(request.form) pass
3. 在模板文件中,可以通过变量绑定来使用,同时,可以对相应的字段传入样式参数,id, class
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>设置</title> <style> .username-input { background: blue; } #age-input { background: pink; } </style> </head> <body> <form action="" method="post"> <table> <tbody> <tr> <!--利用form渲染页面--> <td>{{ form.username.label }}</td> {# <td>用户名</td>#} {# <td><input type="text" name="username"></td>#} <td>{{ form.username(class="username-input") }}</td> </tr> <tr> <!--传入参数--> <td>{{ form.age.label }}</td> <td>{{ form.age(id="age-input") }}</td> </tr> <tr> <td>{{ form.remember.label }}</td> <td>{{ form.remember() }}</td> </tr> <tr> <td>{{ form.tags.label }}</td> <td>{{ form.tags() }}</td> </tr> <tr> <td></td> <td><input type="submit" value="提交"></td> </tr> </tbody> </table> </form> </body> </html>
渲染的效果:
文件上传
1. 在模板中,需要指定input标签的enctype才能上传文件。upload.html文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>上传</title> </head> <body> <form action="" method="post" enctype="multipart/form-data"> <!--上传文件要写enctype--> <table> <tbody> <tr> <td>头像: </td> <td><input type="file" name="avatar"></td> </tr> <tr> <td>描述: </td> <td><input type="text" name="desc"></td> </tr> <tr> <td></td> <td><input type="submit" value="提交"></td> </tr> </tbody> </table> </form> </body> </html>
完成上传界面:
2. 在后台如果想要获取上传的文件,那么应该使用request.files.get(name)获取上传的文件,这里的name值表示的是对应的input标签的name属性的值。
@app.route('/upload/', methods=["GET", "POST"]) def upload(): if request.method == "GET": return render_template("html/upload.html") else: # 接受用户上传的数据 desc = request.form.get("desc") avatar = request.files.get("avatar") # 获取文件 # 保存文件 file_name = secure_filename(avatar.filename) # 对文件名进行安全检测 avatar.save("uploaded/" + file_name) print(desc) return "文件上传成功"
3. 获取到上传的文件后,可以使用save方法保存文件
【注】: 在保存文件之前,需要使用secure_filename对文件名进行过滤,消除安全隐患:
from werkzeug.utils import secure_filename # 对文件名进行包装
4. 在服务器上读取文件,应该定义一个url来获取指定文件,在视图函数中使用send_from_directory(文件目录,文件名)来获取服务器上的文件:
@app.route('/show/<file_name>/') def show_image(file_name): return send_from_directory("uploaded/", filename=file_name)
完整的app.py文件如下:
from flask import Flask, url_for, request, render_template import config from exts import db from forms import RegisterForm, LoginForm, SettingForm import os from werkzeug.utils import secure_filename # 对文件名进行包装 from flask import send_from_directory app = Flask(__name__) app.config.from_object(config) db.init_app(app) # db获取app中数据库的连接方式 @app.route('/') def hello_world(): return 'Hello World!' @app.route('/profile/') def profile(): return "profile page" @app.route('/register/', methods=["GET", "POST"]) def register(): """ 表单验证 :return: """ if request.method == "GET": return render_template("html/register.html") else: form = RegisterForm(request.form) if form.validate(): return "success" else: print(form.errors) # 获取具体的错误信息 return "Fail" @app.route('/login/', methods=["GET", "POST"]) def login(): if request.method == "GET": return render_template("html/login.html") else: form = LoginForm(request.form) if form.validate(): return "success" else: print(form.errors) return "Failed" @app.route('/settings/', methods=["GET", "POST"]) def setting(): if request.method == "GET": form = SettingForm() # 利用form渲染模板 return render_template("html/settings.html", form=form) else: form = SettingForm(request.form) pass @app.route('/upload/', methods=["GET", "POST"]) def upload(): if request.method == "GET": return render_template("html/upload.html") else: # 接受用户上传的数据 desc = request.form.get("desc") avatar = request.files.get("avatar") # 获取文件 # 保存文件 file_name = secure_filename(avatar.filename) # 对文件名进行安全检测 avatar.save("uploaded/" + file_name) print(desc) return "文件上传成功" @app.route('/show/<file_name>/') def show_image(file_name): return send_from_directory("uploaded/", filename=file_name) if __name__ == '__main__': app.run()
利用Flask_wtf验证上传的文件:
1. 对上传的问价使用表单验证,在定义表单的时候,对文件的字段需要采用FileField进行验证
2. 验证器应该从flask_wtf.file中导入,需要使用的是flask_wtf.file.FileRequired验证文件是否为空,flask_wtf.file.FileAllowed
验证文件的格式是否满足要求。
3. 在进行验证的时候,因为上传的字符串需要使用request.form进行获取,上传的文件需要使用request.file进行获取,所以在进行验证的时候,需要对这两个数据进行组合,这两个数据都是immutableDict,不可变字典的形式,所以在视图函数中使用CombineMultiDict将字符串和文件组合在一起,作为表单验证的输入参数。
示例代码:
form.py中定义的验证表单
from flask_wtf.file import FileRequired, FileAllowed # 验证文件必须上传,以及上传的类型 # 定义上传表单验证 class UploadForm(Form): avatar = FileField(validators=[FileRequired(), FileAllowed(["jpg", "png", "gif"])]) desc = StringField(validators=[InputRequired()])
视图函数:
from werkzeug.datastructures import CombinedMultiDict # 将两个不可变的字典组合到一起 @app.route('/upload/', methods=["GET", "POST"]) def upload(): if request.method == "GET": return render_template("html/upload.html") else: # 接受用户上传的数据 # form = UploadForm(request.form) # 因为这里字符串是根据request.form获取的 文件时根据request.form获取的,需要组合 form = UploadForm(CombinedMultiDict([request.form, request.files])) if form.validate(): desc = request.form.get("desc") avatar = request.files.get("avatar") # 获取文件 # 另一种数据获取方法 # desc_ = form.desc.data # avatar_ = form.avatar.data # 保存文件 file_name = secure_filename(avatar.filename) # 对文件名进行安全检测 avatar.save("uploaded/" + file_name) print(desc) return "文件上传成功" else: print(form.errors) return "文件上传失败"