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中使用requiredpattern等属性
  • 合理的错误提示:为用户提供清晰、友好的错误信息
  • 文件大小限制:配置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)