FlaskWeb表单处理
Flask表单处理通常使用Flask-WTF扩展,它提供了表单验证、CSRF保护和文件上传等功能。
1. 安装Flask-WTF
首先需要安装Flask-WTF扩展:
pip install Flask-WTF
2. 基本表单示例
2.1 创建表单类
# app.py
from flask import Flask, render_template, redirect, url_for, flash
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, TextAreaField, SelectField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo
from flask_wtf.file import FileField, FileAllowed, FileRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here' # 用于CSRF保护和会话
# 创建登录表单类
class LoginForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired(message='用户名不能为空'),
Length(min=3, max=20, message='用户名长度必须在3-20个字符之间')
])
password = PasswordField('密码', validators=[
DataRequired(message='密码不能为空'),
Length(min=6, max=128, message='密码长度必须在6-128个字符之间')
])
remember = BooleanField('记住我')
submit = SubmitField('登录')
# 创建注册表单类
class RegistrationForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired(),
Length(min=3, max=20)
])
email = StringField('邮箱', validators=[
DataRequired(),
Email(message='请输入有效的邮箱地址')
])
password = PasswordField('密码', validators=[
DataRequired(),
Length(min=6)
])
confirm_password = PasswordField('确认密码', validators=[
DataRequired(),
EqualTo('password', message='两次输入的密码不一致')
])
submit = SubmitField('注册')
# 创建联系表单类
class ContactForm(FlaskForm):
name = StringField('姓名', validators=[DataRequired()])
email = StringField('邮箱', validators=[DataRequired(), Email()])
category = SelectField('咨询类型', choices=[
('general', '一般咨询'),
('technical', '技术支持'),
('billing', '账单问题'),
('other', '其他')
])
message = TextAreaField('消息', validators=[
DataRequired(),
Length(min=10, max=500, message='消息长度必须在10-500个字符之间')
])
subscribe = BooleanField('订阅新闻邮件', default=True)
submit = SubmitField('发送')
# 创建文件上传表单类
class UploadForm(FlaskForm):
file = FileField('选择文件', validators=[
FileRequired(message='请选择文件'),
FileAllowed(['jpg', 'png', 'gif', 'pdf', 'doc', 'docx'],
message='只允许上传图片、PDF和Word文档')
])
description = StringField('文件描述')
submit = SubmitField('上传')
2.2 创建视图函数
# app.py (续)
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# 这里应该验证用户名和密码
username = form.username.data
password = form.password.data
remember = form.remember.data
# 模拟用户验证
if username == 'admin' and password == 'password123':
flash('登录成功!', 'success')
return redirect(url_for('dashboard'))
else:
flash('用户名或密码错误', 'danger')
return render_template('login.html', form=form)
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# 这里应该将用户数据保存到数据库
username = form.username.data
email = form.email.data
password = form.password.data
flash('注册成功!请登录。', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm()
if form.validate_on_submit():
# 这里应该处理表单数据,如发送邮件或保存到数据库
name = form.name.data
email = form.email.data
category = form.category.data
message = form.message.data
subscribe = form.subscribe.data
flash('消息已发送!我们会尽快回复您。', 'success')
return redirect(url_for('contact'))
return render_template('contact.html', form=form)
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = UploadForm()
if form.validate_on_submit():
file = form.file.data
description = form.description.data
# 保存文件
import os
from werkzeug.utils import secure_filename
filename = secure_filename(file.filename)
file.save(os.path.join('uploads', filename))
flash(f'文件 {filename} 上传成功!', 'success')
return redirect(url_for('upload'))
return render_template('upload.html', form=form)
@app.route('/dashboard')
def dashboard():
return '欢迎来到控制面板'
if __name__ == '__main__':
os.makedirs('uploads', exist_ok=True)
app.run(debug=True)
3. 创建HTML模板
3.1 基础模板 (base.html)
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title if title else "Flask应用" }}</title>
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="{{ url_for('index') }}">Flask表单示例</a>
<div class="navbar-nav">
<a class="nav-link" href="{{ url_for('login') }}">登录</a>
<a class="nav-link" href="{{ url_for('register') }}">注册</a>
<a class="nav-link" href="{{ url_for('contact') }}">联系我们</a>
<a class="nav-link" href="{{ url_for('upload') }}">文件上传</a>
</div>
</div>
</nav>
<div class="container mt-4">
<!-- 闪存消息 -->
{{ get_flashed_messages() }}
<div id="flash-messages">
@% for category, message in get_flashed_messages(with_categories=true) %>
<div class="alert alert-{{ category }} alert-dismissible fade show">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@% endfor %>
</div>
@% block content %>
@% endblock %>
</div>
<script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>
注意:Blade模板中使用{{ }}转义,而Jinja2模板使用{{ }}。这里我们展示的是Jinja2模板代码。
3.2 登录表单模板 (login.html)
<!-- templates/login.html -->
@% extends "base.html" %>
@% block content %>
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">用户登录</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('login') }}">
{{ form.hidden_tag() }} <!-- CSRF令牌 -->
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
@% if form.username.errors %>
@% for error in form.username.errors %>
<div class="invalid-feedback">
{{ error }}
</div>
@% endfor %>
@% endif %>
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
@% if form.password.errors %>
@% for error in form.password.errors %>
<div class="invalid-feedback">
{{ error }}
</div>
@% endfor %>
@% endif %>
</div>
<div class="mb-3 form-check">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
<div class="mt-3 text-center">
<p>还没有账号? <a href="{{ url_for('register') }}">立即注册</a></p>
</div>
</div>
</div>
</div>
</div>
@% endblock %>
3.3 注册表单模板 (register.html)
<!-- templates/register.html -->
@% extends "base.html" %>
@% block content %>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">用户注册</h4>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('register') }}">
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6 mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control" + (" is-invalid" if form.username.errors else "")) }}
@% if form.username.errors %>
<div class="invalid-feedback">
{{ form.username.errors[0] }}
</div>
@% endif %>
<div class="form-text">用户名长度3-20个字符</div>
</div>
<div class="col-md-6 mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control" + (" is-invalid" if form.email.errors else "")) }}
@% if form.email.errors %>
<div class="invalid-feedback">
{{ form.email.errors[0] }}
</div>
@% endif %>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control" + (" is-invalid" if form.password.errors else "")) }}
@% if form.password.errors %>
<div class="invalid-feedback">
{{ form.password.errors[0] }}
</div>
@% endif %>
<div class="form-text">密码至少6个字符</div>
</div>
<div class="col-md-6 mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control" + (" is-invalid" if form.confirm_password.errors else "")) }}
@% if form.confirm_password.errors %>
<div class="invalid-feedback">
{{ form.confirm_password.errors[0] }}
</div>
@% endif %>
</div>
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-success") }}
</div>
</form>
<div class="mt-3 text-center">
<p>已有账号? <a href="{{ url_for('login') }}">立即登录</a></p>
</div>
</div>
</div>
</div>
</div>
@% endblock %>
4. 表单字段类型
| 字段类型 |
描述 |
示例 |
| StringField |
文本输入框 |
name = StringField('姓名') |
| PasswordField |
密码输入框 |
password = PasswordField('密码') |
| TextAreaField |
多行文本框 |
message = TextAreaField('消息') |
| IntegerField |
整数输入框 |
age = IntegerField('年龄') |
| FloatField |
浮点数输入框 |
price = FloatField('价格') |
| BooleanField |
复选框 |
agree = BooleanField('同意条款') |
| SelectField |
下拉选择框 |
gender = SelectField('性别', choices=[('M','男'),('F','女')]) |
| RadioField |
单选按钮组 |
level = RadioField('等级', choices=[('1','初级'),('2','中级')]) |
| DateField |
日期选择框 |
birthday = DateField('生日') |
| FileField |
文件上传框 |
file = FileField('上传文件') |
| SubmitField |
提交按钮 |
submit = SubmitField('提交') |
5. 表单验证器
| 验证器 |
描述 |
示例 |
| DataRequired |
字段不能为空 |
validators=[DataRequired()] |
| Email |
验证邮箱格式 |
validators=[Email()] |
| Length |
限制输入长度 |
validators=[Length(min=6, max=20)] |
| EqualTo |
比较两个字段值 |
validators=[EqualTo('password')] |
| Regexp |
正则表达式验证 |
validators=[Regexp('^[A-Za-z][A-Za-z0-9_]*$')] |
| URL |
验证URL格式 |
validators=[URL()] |
| NumberRange |
数值范围验证 |
validators=[NumberRange(min=0, max=100)] |
| Optional |
字段可选(可为空) |
validators=[Optional()] |
6. 自定义验证器
# 自定义验证器示例
from wtforms.validators import ValidationError
def validate_username_blacklist(form, field):
"""自定义用户名黑名单验证器"""
blacklist = ['admin', 'root', 'test']
if field.data.lower() in blacklist:
raise ValidationError('该用户名已被系统保留,请选择其他用户名')
def validate_password_complexity(form, field):
"""自定义密码复杂度验证器"""
password = field.data
errors = []
if len(password) < 8:
errors.append('密码至少8个字符')
if not any(c.isupper() for c in password):
errors.append('密码必须包含至少一个大写字母')
if not any(c.islower() for c in password):
errors.append('密码必须包含至少一个小写字母')
if not any(c.isdigit() for c in password):
errors.append('密码必须包含至少一个数字')
if errors:
raise ValidationError('; '.join(errors))
class CustomRegistrationForm(FlaskForm):
username = StringField('用户名', validators=[
DataRequired(),
Length(min=3, max=20),
validate_username_blacklist # 使用自定义验证器
])
password = PasswordField('密码', validators=[
DataRequired(),
validate_password_complexity # 使用自定义验证器
])
# ... 其他字段
7. 表单验证示例代码
# 完整的表单验证示例
@app.route('/validate-form', methods=['GET', 'POST'])
def validate_form():
form = ContactForm()
if form.validate_on_submit():
# 所有验证都通过
name = form.name.data
email = form.email.data
message = form.message.data
# 处理表单数据...
flash('表单提交成功!', 'success')
return redirect(url_for('success'))
else:
# 验证失败,form.errors包含所有错误信息
if form.errors:
for field, errors in form.errors.items():
for error in errors:
flash(f'{getattr(form, field).label.text}: {error}', 'warning')
return render_template('contact.html', form=form)
# 手动验证示例
@app.route('/manual-validate', methods=['POST'])
def manual_validate():
form = ContactForm()
# 手动调用验证
if form.validate():
# 验证通过
pass
else:
# 获取特定字段的错误
if form.name.errors:
print("姓名错误:", form.name.errors)
if form.email.errors:
print("邮箱错误:", form.email.errors)
8. 最佳实践
-
使用CSRF保护:始终包含
form.hidden_tag()来防止CSRF攻击
-
客户端验证:在HTML5中使用
required、pattern等属性
-
合理的错误提示:为用户提供清晰、友好的错误信息
-
文件大小限制:配置
MAX_CONTENT_LENGTH限制上传文件大小
-
安全的文件名处理:使用
secure_filename()处理上传文件名
9. 配置选项
# Flask-WTF配置
app.config['SECRET_KEY'] = 'your-secret-key-here' # 必须配置
app.config['WTF_CSRF_ENABLED'] = True # 启用CSRF保护(默认)
app.config['WTF_CSRF_SECRET_KEY'] = 'different-from-secret-key' # CSRF密钥
app.config['WTF_CSRF_TIME_LIMIT'] = 3600 # CSRF令牌有效期(秒)
# 文件上传配置
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 最大16MB
app.config['UPLOAD_FOLDER'] = 'uploads' # 上传目录
app.config['ALLOWED_EXTENSIONS'] = {'jpg', 'png', 'gif', 'pdf'}
# 自定义错误消息
app.config['WTF_I18N_ENABLED'] = True
from flask_babel import Babel
babel = Babel(app)