跳转至

表单处理

📖 章节简介

本章将介绍Flask-WTF扩展,学习如何创建和处理Web表单,实现数据验证和文件上传功能。

📝 Flask-WTF基础

1. 安装和配置

Bash
# 安装Flask-WTF
pip install flask-wtf

# requirements.txt
flask-wtf==1.2.1
email-validator==2.1.0
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记住用户',
    '进度反馈': '长时间操作显示进度'
}

📝 练习题

基础题

  1. 什么是Flask-WTF?
  2. 如何创建表单类?
  3. 如何验证表单数据?

进阶题

  1. 实现自定义验证器。
  2. 处理文件上传。
  3. 优化表单用户体验。

实践题

  1. 创建用户注册和登录表单。
  2. 实现文章发布表单。
  3. 构建完整的表单验证系统。

📚 推荐阅读

🔗 下一章

数据库集成 - 学习Flask-SQLAlchemy的使用。