第 62 课 - Flask 表单处理与用户输入
学习目标
完成本课学习后,你将能够:
- 理解 Web 应用中表单处理的基本原理和 HTTP 请求方法(GET/POST)。
- 使用 WTForms 库创建、渲染和验证 HTML 表单。
- 在 Flask 路由中安全地接收、处理和响应用户的表单输入。
- 实现包含文件上传功能的表单处理。
核心概念
1. 表单是什么?
表单就像你在网页上填写的"问卷"或"订单"。用户输入信息(比如用户名、密码、选择文件),然后点击"提交",这些信息就会被发送到服务器进行处理。在 Web 开发中,表单是用户与应用交互最主要的方式之一。
2. GET vs POST 方法
当表单提交数据时,主要有两种方式:
- GET:将参数附加到 URL 后面。适用于数据获取、查询(如搜索框)。不适用于敏感数据或大量数据。
- POST:将数据放在 HTTP 请求体中发送。适用于数据提交、修改、删除,尤其是包含密码、文件上传等敏感或大量数据的场景。
3. 为什么使用 WTForms?
直接手动处理 HTML 表单和请求数据既繁琐又容易出错(例如,忘记验证用户输入)。WTForms 是一个强大的 Python 库,它为我们提供了:
- 表单类定义:用 Python 类来定义表单有哪些字段和验证规则。
- HTML 渲染:自动生成 HTML 表单代码。
- 数据验证:内置多种验证器(如必填、长度、邮箱格式等)。
- CSRF 保护:自动防止跨站请求伪造攻击(一种常见的安全漏洞)。
- 与 Flask 集成:通过
Flask-WTF扩展无缝集成。
4. 表单处理流程
典型的流程是:
- 用户访问页面,看到一个空表单。
- 用户填写表单并点击提交。
- 浏览器将数据以 POST 请求发送到服务器。
- 服务器用 WTForms 接收数据,进行验证。
- 如果验证通过,处理数据(如存入数据库),然后给用户一个成功响应或重定向。
- 如果验证失败,将错误信息和之前填写的数据一起返回给用户,让他修改。
代码示例
下面是一个完整的示例,演示如何用 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,包含以下字段和验证:
username: 用户名,必填,长度3-20。email: 邮箱,必填,需为有效邮箱格式。password: 密码,必填,长度6-50。confirm_password: 确认密码,必须与password字段相同(使用EqualTo验证器)。submit: "注册"按钮。 并为这个表单创建对应的路由/register和模板register.html。
练习 3:增强文件上传 修改文件上传功能,增加以下限制:
- 只允许上传图片文件(扩展名为
.jpg,.jpeg,.png,.gif)。 - 限制文件大小不超过 2MB。
- 提示:你可能需要在
UploadForm类中自定义验证,或在视图函数中进行检查。
常见错误
- 忘记 CSRF 保护:在模板的表单中忘记添加
{{ form.hidden_tag() }},会导致提交时出现400 Bad Request错误。 - 混淆 GET 和 POST:试图在 GET 请求的视图函数中访问
form.data来获取用户提交的数据,这是无效的。POST 数据只在POST请求中且form.validate_on_submit()为True时才可安全使用。 - 文件上传表单缺少
enctype:上传文件时,HTML 表单必须设置enctype="multipart/form-data",否则文件内容不会被发送。 - 保存上传文件时未使用
secure_filename:直接使用用户上传的原始文件名保存文件,可能导致路径遍历攻击(例如文件名是../../hack.py)。 - 忘记处理验证错误并回显:当表单验证失败时,需要将
form对象重新传回模板,以便显示用户之前输入的内容和具体的错误信息。否则用户体验会很差。
小结
在本课中,我们学习了 Flask 应用中处理用户输入的核心技能:
- 表单基础:理解了表单是用户输入的载体,以及
GET与POST请求的区别。 - WTForms/Flask-WTF 的威力:通过定义
FlaskForm子类,我们能够以结构化、安全的方式定义表单字段和验证规则。 - 处理流程:掌握了
form.validate_on_submit()这个关键方法,它同时检查请求方法和数据有效性,是处理表单提交的黄金标准。 - 文件上传:学会了如何处理文件上传,包括安全文件名和目录管理。
- 安全与验证:强调了 CSRF 保护和用户输入验证的重要性,这是构建安全 Web 应用的基础。
表单处理是连接用户与后端逻辑的桥梁。熟练掌握它,你就能开始构建真正可交互的 Web 应用程序了。下一课,我们将学习如何将这些用户数据持久化存储到数据库中。
练习编辑器
python
Loading...