表单处理¶
📖 章节简介¶
本章将介绍Flask-WTF扩展,学习如何创建和处理Web表单,实现数据验证和文件上传功能。
📝 Flask-WTF基础¶
1. 安装和配置¶
Python
# app/__init__.py
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect()
def create_app():
app = Flask(__name__)
# 配置密钥
app.config['SECRET_KEY'] = 'your-secret-key-here'
# 启用CSRF保护
csrf.init_app(app)
return app
2. 创建表单类¶
Python
# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, TextAreaField, SelectField, FileField, BooleanField, SubmitField, IntegerField, URLField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError
from flask_wtf.file import FileAllowed
class LoginForm(FlaskForm):
"""登录表单"""
username = StringField('用户名',
validators=[DataRequired(), Length(min=3, max=20)])
password = PasswordField('密码',
validators=[DataRequired(), Length(min=6)])
remember_me = BooleanField('记住我')
submit = SubmitField('登录')
class RegistrationForm(FlaskForm):
"""注册表单"""
username = StringField('用户名',
validators=[DataRequired(), Length(min=3, max=20)])
email = StringField('邮箱',
validators=[DataRequired(), Email()])
password = PasswordField('密码',
validators=[DataRequired(), Length(min=6)])
password2 = PasswordField('确认密码',
validators=[DataRequired(), EqualTo('password', message='两次密码不一致')])
submit = SubmitField('注册')
class PostForm(FlaskForm):
"""文章表单"""
title = StringField('标题',
validators=[DataRequired(), Length(max=100)])
content = TextAreaField('内容',
validators=[DataRequired()])
category = SelectField('分类',
choices=[('tech', '技术'), ('life', '生活'), ('other', '其他')])
image = FileField('封面图片',
validators=[FileAllowed(['jpg', 'png'], '只支持JPG和PNG格式')])
submit = SubmitField('发布')
🎨 表单验证¶
1. 内置验证器¶
Python
from wtforms.validators import (
DataRequired, # 必填
Email, # 邮箱格式
Length, # 长度限制
EqualTo, # 相等验证
NumberRange, # 数字范围
Regexp, # 正则表达式
URL, # URL格式
Optional, # 可选
AnyOf, # 值必须在列表中
NoneOf # 值不能在列表中
)
# 使用示例
class UserForm(FlaskForm):
username = StringField('用户名',
validators=[
DataRequired(message='用户名不能为空'),
Length(min=3, max=20, message='用户名长度必须在3-20之间'),
Regexp('^[a-zA-Z0-9_]+$', message='用户名只能包含字母、数字和下划线')
])
age = IntegerField('年龄',
validators=[
NumberRange(min=18, max=100, message='年龄必须在18-100之间')
])
website = URLField('个人网站',
validators=[
Optional(),
URL(message='请输入有效的URL')
])
role = SelectField('角色',
choices=[('user', '用户'), ('admin', '管理员')],
validators=[
AnyOf(['user', 'admin'], message='角色无效')
])
2. 自定义验证器¶
Python
from wtforms.validators import ValidationError
class RegistrationForm(FlaskForm):
username = StringField('用户名',
validators=[DataRequired()])
email = StringField('邮箱',
validators=[DataRequired(), Email()])
def validate_username(self, field):
"""验证用户名是否已存在"""
user = User.query.filter_by(username=field.data).first()
if user:
raise ValidationError('用户名已被使用')
def validate_email(self, field):
"""验证邮箱是否已注册"""
user = User.query.filter_by(email=field.data).first()
if user:
raise ValidationError('邮箱已被注册')
class ChangePasswordForm(FlaskForm):
old_password = PasswordField('当前密码',
validators=[DataRequired()])
new_password = PasswordField('新密码',
validators=[DataRequired(), Length(min=6)])
def validate_old_password(self, field):
"""验证当前密码是否正确"""
if not current_user.check_password(field.data):
raise ValidationError('当前密码错误')
📤 表单处理¶
1. 渲染表单¶
HTML
<!-- templates/auth/login.html -->
{% extends "base.html" %}
{% import "macros/form.html" as form_macros %}
{% block title %}登录{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-box">
<h1>登录</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
{{ form.username.label(class='form-label') }}
{{ form.username(class='form-control', placeholder='请输入用户名') }}
{% if form.username.errors %}
<div class="invalid-feedback">
{{ form.username.errors[0] }}
</div>
{% endif %}
</div>
<div class="form-group">
{{ form.password.label(class='form-label') }}
{{ form.password(class='form-control', placeholder='请输入密码') }}
{% if form.password.errors %}
<div class="invalid-feedback">
{{ form.password.errors[0] }}
</div>
{% endif %}
</div>
<div class="form-group">
<div class="form-check">
{{ form.remember_me(class='form-check-input') }}
{{ form.remember_me.label(class='form-check-label') }}
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">登录</button>
</form>
<div class="login-footer">
<p>还没有账号?<a href="{{ url_for('auth.register') }}">立即注册</a></p>
<p><a href="{{ url_for('auth.forgot_password') }}">忘记密码?</a></p>
</div>
</div>
</div>
{% endblock %}
2. 处理表单提交¶
Python
# app/auth/routes.py
from flask import render_template, redirect, url_for, flash, request
from flask_login import login_user, logout_user, current_user
from app import db
from app.auth.forms import LoginForm, RegistrationForm
from app.models import User
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user, remember=form.remember_me.data)
flash('登录成功!', 'success')
next_page = request.args.get('next')
if not next_page or not next_page.startswith('/'):
next_page = url_for('main.index')
return redirect(next_page)
else:
flash('用户名或密码错误', 'error')
return render_template('auth/login.html', form=form)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
flash('注册成功!请登录', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
flash('您已退出登录', 'info')
return redirect(url_for('main.index'))
📁 文件上传¶
1. 配置上传¶
Python
# config.py
import os
class Config:
# 上传配置
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf', 'doc', 'docx'}
2. 处理文件上传¶
Python
# app/utils.py
from werkzeug.utils import secure_filename
import os
from PIL import Image
def allowed_file(filename):
"""检查文件扩展名是否允许"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
def save_upload_file(file, folder='uploads'):
"""保存上传的文件"""
if file and allowed_file(file.filename):
# 安全的文件名
filename = secure_filename(file.filename)
# 添加时间戳避免重名
name, ext = os.path.splitext(filename)
filename = f"{name}_{int(time.time())}{ext}"
# 创建目录
upload_path = os.path.join(app.config['UPLOAD_FOLDER'], folder)
os.makedirs(upload_path, exist_ok=True)
# 保存文件
file_path = os.path.join(upload_path, filename)
file.save(file_path)
return filename
return None
def resize_image(input_path, output_path, max_size=(800, 600)):
"""调整图片大小"""
with Image.open(input_path) as img:
img.thumbnail(max_size)
img.save(output_path, optimize=True, quality=85)
Python
# app/post/routes.py
from flask import current_app
from app.utils import save_upload_file, resize_image
@post_bp.route('/create', methods=['GET', 'POST'])
@login_required
def create_post():
form = PostForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
content=form.content.data,
category=form.category.data,
author=current_user
)
# 处理图片上传
if form.image.data:
filename = save_upload_file(form.image.data, 'posts')
if filename:
# 调整图片大小
input_path = os.path.join(current_app.config['UPLOAD_FOLDER'], 'posts', filename)
output_path = os.path.join(current_app.config['UPLOAD_FOLDER'], 'posts', f'thumb_{filename}')
resize_image(input_path, output_path, (300, 300))
post.image = filename
db.session.add(post)
db.session.commit()
flash('文章发布成功!', 'success')
return redirect(url_for('post.detail', post_id=post.id))
return render_template('post/create.html', form=form)
💡 最佳实践¶
1. 表单安全¶
Python
# 表单安全最佳实践
form_security = {
'CSRF保护': '使用Flask-WTF的CSRF保护',
'输入验证': '在服务器端验证所有输入',
'文件上传': '限制文件类型和大小',
'密码处理': '使用安全的密码哈希',
'错误处理': '不暴露敏感信息'
}
2. 用户体验¶
Python
# 改善表单用户体验
user_experience = {
'实时验证': '使用JavaScript进行客户端验证',
'清晰提示': '提供明确的错误信息',
'自动填充': '支持浏览器自动填充',
'记住用户': '使用Cookie记住用户',
'进度反馈': '长时间操作显示进度'
}
📝 练习题¶
基础题¶
- 什么是Flask-WTF?
- 如何创建表单类?
- 如何验证表单数据?
进阶题¶
- 实现自定义验证器。
- 处理文件上传。
- 优化表单用户体验。
实践题¶
- 创建用户注册和登录表单。
- 实现文章发布表单。
- 构建完整的表单验证系统。
📚 推荐阅读¶
🔗 下一章¶
数据库集成 - 学习Flask-SQLAlchemy的使用。