62·模块十三:Web开发基础进阶

Flask 表单处理与用户输入

flaskformswtforms

第 62 课 - Flask 表单处理与用户输入

学习目标

完成本课学习后,你将能够:

  1. 理解 Web 应用中表单处理的基本原理和 HTTP 请求方法(GET/POST)。
  2. 使用 WTForms 库创建、渲染和验证 HTML 表单。
  3. 在 Flask 路由中安全地接收、处理和响应用户的表单输入。
  4. 实现包含文件上传功能的表单处理。

核心概念

1. 表单是什么?

表单就像你在网页上填写的"问卷"或"订单"。用户输入信息(比如用户名、密码、选择文件),然后点击"提交",这些信息就会被发送到服务器进行处理。在 Web 开发中,表单是用户与应用交互最主要的方式之一。

2. GET vs POST 方法

当表单提交数据时,主要有两种方式:

  • GET:将参数附加到 URL 后面。适用于数据获取、查询(如搜索框)。不适用于敏感数据或大量数据。
  • POST:将数据放在 HTTP 请求体中发送。适用于数据提交、修改、删除,尤其是包含密码、文件上传等敏感或大量数据的场景。

3. 为什么使用 WTForms?

直接手动处理 HTML 表单和请求数据既繁琐又容易出错(例如,忘记验证用户输入)。WTForms 是一个强大的 Python 库,它为我们提供了:

  • 表单类定义:用 Python 类来定义表单有哪些字段和验证规则。
  • HTML 渲染:自动生成 HTML 表单代码。
  • 数据验证:内置多种验证器(如必填、长度、邮箱格式等)。
  • CSRF 保护:自动防止跨站请求伪造攻击(一种常见的安全漏洞)。
  • 与 Flask 集成:通过 Flask-WTF 扩展无缝集成。

4. 表单处理流程

典型的流程是:

  1. 用户访问页面,看到一个空表单。
  2. 用户填写表单并点击提交。
  3. 浏览器将数据以 POST 请求发送到服务器。
  4. 服务器用 WTForms 接收数据,进行验证。
  5. 如果验证通过,处理数据(如存入数据库),然后给用户一个成功响应或重定向。
  6. 如果验证失败,将错误信息和之前填写的数据一起返回给用户,让他修改。

代码示例

下面是一个完整的示例,演示如何用 Flask 和 WTForms 处理一个简单的登录表单和一个文件上传表单。

首先,安装必要的库:

pip install flask flask-wtf

项目结构:

my_flask_app/
│
├── app.py            # 主应用文件
├── templates/
│   ├── base.html     # 基础模板
│   ├── login.html    # 登录表单模板
│   └── upload.html   # 文件上传模板
└── uploads/          # 上传文件保存目录 (需手动创建)

app.py:

from flask import Flask, render_template, redirect, url_for, flash, request
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, FileField
from wtforms.validators import DataRequired, Length, Email, EqualTo
from werkzeug.utils import secure_filename
import os

# 配置应用
app = Flask(__name__)
# 设置一个密钥,用于保护表单的CSRF令牌和会话,生产环境应使用更复杂的密钥
app.config['SECRET_KEY'] = 'a_very_secret_key_for_csrf'
# 设置上传文件夹路径
app.config['UPLOAD_FOLDER'] = 'uploads'
# 确保上传文件夹存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

# 1. 定义登录表单类
class LoginForm(FlaskForm):
    """用户登录表单"""
    email = StringField('邮箱', validators=[
        DataRequired(message='请输入邮箱'),
        Email(message='请输入有效的邮箱地址')
    ])
    password = PasswordField('密码', validators=[
        DataRequired(message='请输入密码'),
        Length(min=6, message='密码长度至少为6个字符')
    ])
    submit = SubmitField('登录')

# 2. 定义文件上传表单类
class UploadForm(FlaskForm):
    """文件上传表单"""
    file = FileField('选择文件', validators=[DataRequired(message='请选择一个文件')])
    submit = SubmitField('上传')

# 3. 定义路由和视图
@app.route('/')
def index():
    return render_template('base.html')

# 登录路由
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm() # 创建表单实例
    # 如果是POST请求且表单验证通过
    if form.validate_on_submit():
        # 在这里处理登录逻辑(例如查询数据库验证用户)
        email = form.email.data
        password = form.password.data
        # 模拟成功登录
        flash(f'登录成功!欢迎,{email}。', 'success')
        return redirect(url_for('index'))
    # 如果是GET请求,或者POST但验证失败,渲染表单
    return render_template('login.html', form=form)

# 文件上传路由
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    if form.validate_on_submit():
        # 获取上传的文件对象
        uploaded_file = form.file.data
        # 生成安全的文件名
        filename = secure_filename(uploaded_file.filename)
        # 保存文件
        filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
        uploaded_file.save(filepath)
        flash(f'文件 "{filename}" 上传成功!', 'success')
        return redirect(url_for('index'))
    return render_template('upload.html', form=form)

if __name__ == '__main__':
    app.run(debug=True)

templates/base.html:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Flask 表单处理</title>
</head>
<body>
    <nav>
        <a href="{{ url_for('index') }}">首页</a> |
        <a href="{{ url_for('login') }}">登录</a> |
        <a href="{{ url_for('upload') }}">文件上传</a>
    </nav>
    <hr>
    <!-- 显示 Flash 消息 -->
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div style="padding: 10px; margin: 10px 0; background-color: {% if category == 'success' %}#d4edda{% else %}#f8d7da{% endif %};">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
    {% endwith %}

    {% block content %}{% endblock %}
</body>
</html>

templates/login.html:

{% extends "base.html" %}

{% block content %}
<h2>用户登录</h2>
<!-- 开启 CSRF 保护 -->
<form method="POST" action="">
    {{ form.hidden_tag() }}
    <div>
        {{ form.email.label }}<br>
        {{ form.email(size=30) }}
        <!-- 显示该字段的验证错误信息 -->
        {% if form.email.errors %}
            <ul>
                {% for error in form.email.errors %}
                    <li style="color: red;">{{ error }}</li>
                {% endfor %}
            </ul>
        {% endif %}
    </div>
    <div>
        {{ form.password.label }}<br>
        {{ form.password(size=30) }}
        {% if form.password.errors %}
            <ul>
                {% for error in form.password.errors %}
                    <li style="color: red;">{{ error }}</li>
                {% endfor %}
            </ul>
        {% endif %}
    </div>
    <div>
        {{ form.submit() }}
    </div>
</form>
{% endblock %}

templates/upload.html:

{% extends "base.html" %}

{% block content %}
<h2>文件上传</h2>
<!-- 表单必须设置 enctype="multipart/form-data" 才能上传文件 -->
<form method="POST" action="" enctype="multipart/form-data">
    {{ form.hidden_tag() }}
    <div>
        {{ form.file.label }}<br>
        {{ form.file() }}
        {% if form.file.errors %}
            <ul>
                {% for error in form.file.errors %}
                    <li style="color: red;">{{ error }}</li>
                {% endfor %}
            </ul>
        {% endif %}
    </div>
    <div>
        {{ form.submit() }}
    </div>
</form>
{% endblock %}

实践练习

练习 1:修改登录表单 在现有的 LoginForm 中,为 password 字段添加一个验证器,确保密码包含至少一个大写字母(提示:WTForms 有 Regexp 验证器)。

  • 要求:使用正则表达式 r'[A-Z]'
  • 预期效果:当用户输入全小写密码时,页面应显示错误信息。

练习 2:创建注册表单 创建一个新的表单类 RegisterForm,包含以下字段和验证:

  1. username: 用户名,必填,长度3-20。
  2. email: 邮箱,必填,需为有效邮箱格式。
  3. password: 密码,必填,长度6-50。
  4. confirm_password: 确认密码,必须与 password 字段相同(使用 EqualTo 验证器)。
  5. submit: "注册"按钮。 并为这个表单创建对应的路由 /register 和模板 register.html

练习 3:增强文件上传 修改文件上传功能,增加以下限制:

  1. 只允许上传图片文件(扩展名为 .jpg, .jpeg, .png, .gif)。
  2. 限制文件大小不超过 2MB。
  • 提示:你可能需要在 UploadForm 类中自定义验证,或在视图函数中进行检查。

常见错误

  1. 忘记 CSRF 保护:在模板的表单中忘记添加 {{ form.hidden_tag() }},会导致提交时出现 400 Bad Request 错误。
  2. 混淆 GET 和 POST:试图在 GET 请求的视图函数中访问 form.data 来获取用户提交的数据,这是无效的。POST 数据只在 POST 请求中且 form.validate_on_submit()True 时才可安全使用。
  3. 文件上传表单缺少 enctype:上传文件时,HTML 表单必须设置 enctype="multipart/form-data",否则文件内容不会被发送。
  4. 保存上传文件时未使用 secure_filename:直接使用用户上传的原始文件名保存文件,可能导致路径遍历攻击(例如文件名是 ../../hack.py)。
  5. 忘记处理验证错误并回显:当表单验证失败时,需要将 form 对象重新传回模板,以便显示用户之前输入的内容和具体的错误信息。否则用户体验会很差。

小结

在本课中,我们学习了 Flask 应用中处理用户输入的核心技能:

  • 表单基础:理解了表单是用户输入的载体,以及 GETPOST 请求的区别。
  • WTForms/Flask-WTF 的威力:通过定义 FlaskForm 子类,我们能够以结构化、安全的方式定义表单字段和验证规则。
  • 处理流程:掌握了 form.validate_on_submit() 这个关键方法,它同时检查请求方法和数据有效性,是处理表单提交的黄金标准。
  • 文件上传:学会了如何处理文件上传,包括安全文件名和目录管理。
  • 安全与验证:强调了 CSRF 保护和用户输入验证的重要性,这是构建安全 Web 应用的基础。

表单处理是连接用户与后端逻辑的桥梁。熟练掌握它,你就能开始构建真正可交互的 Web 应用程序了。下一课,我们将学习如何将这些用户数据持久化存储到数据库中。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「Flask 与数据库集成(SQLAlchemy)」 以巩固所学知识。