请求里能取的东西有四种:查询串、表单、JSON、文件,各有各的取法。
同名参数出现两次,dict() 只留第一个,要全的得用 getlist。
WTForms 把校验规则写成清单;注意 DataRequired 会把 0 判成「没填」。
这一篇对应教材 1.2.7 与 1.3 节前三小节:请求内容、表单规格、Python 套件规格、 程序与模板间的数据传递。 这是全书第一次处理「用户输入」,也是第一次遇到「输入不可信」这件事。
一、四种取法的分工
| 来源 | 取法 | 典型场景 |
|---|---|---|
| 查询串 | request.args | 搜索、分页、筛选 |
| 表单 | request.form | 登录、新增、编辑 |
| JSON | request.get_json() | 前端异步提交、API |
| 文件 | request.files | 头像、图片上传 |
取法不能混用。表单用 args 取永远是空的,JSON 用 form 取也是空的,而且都不会报错,
只是返回空——这类 bug 最难查。
判断依据看请求头:
Content-Type 是 application/x-www-form-urlencoded 或 multipart/form-data 才是表单,
application/json 才是 JSON。
二、同名参数那个坑
?tag=a&tag=b 这种地址很常见——勾选框多选传上来就是这样。
用 dict(request.args) 拿到的只有 {'tag': 'a'},第二个丢了,而且不报错。
要拿全必须用 request.args.getlist('tag')。表单那边对应的是 request.form.getlist(...)。
这条对分页、筛选这类功能是致命的:用户选了五个标签,系统只认第一个,结果看起来「部分生效」, 最难报 bug。
三、JSON 与 silent
request.get_json() 在 Content-Type 不是 JSON 时会抛 415,内容不是合法 JSON 时会抛 400。
加 silent=True 之后,两种情况都返回 None,不抛异常。
要不要加 silent,取决于你想不想自己兜底。做 API 时,自己判空并返回一个带错误码的 JSON,
比让框架吐一个 HTML 错误页好。
注意它还有一种情况:force=True 会忽略 Content-Type 直接解析。
只有当调用方不受你控制、又不肯设对头时才用。
四、文件上传只做了一半
request.files 里拿到的是 FileStorage 对象,有 .filename 和 .read()。
只有 multipart/form-data 编码时它才有内容。这次实测传了 2048 字节,读出来正好 2048。
但「收到」不等于「存好」。这一节只到收到为止,存盘、改名、限制类型、防重名,都在第 7 章。
顺带一句:不校验文件类型就存盘,等于允许别人往你服务器上写任意文件。
五、WTForms:把校验写成清单
手写 if not title: 报错 这种方式,字段一多就散了。WTForms 把规则声明成列表:
class BookForm(FlaskForm):
title = StringField(u"书名", validators=[DataRequired(),
Length(1, 10)])
price = IntegerField(u"价格", validators=[DataRequired(),
NumberRange(1, 9999)])
validate() 会跑完所有规则,把错误收进 form.errors:键是字段名,值是错误信息的列表。
实测一个陷阱:价格填 0,报的是「必填」。因为 DataRequired 判真假值,0 在 Python 里等于假。
要允许 0,得把校验器换成 InputRequired——它只判「有没有提交」,不管值是不是假。
这个区别在价格、数量、折扣这类字段上经常踩。
六、动手 Demo:表单与请求内容:四种取数姿势
这一节把四种取法各跑一遍,再让 WTForms 校验三种输入:空的、超范围的、填 0 的。
@app.route("/form", methods=["GET", "POST"])
def form_():
if request.method == "POST":
return jsonify(dict(request.form))
return "用 POST 提交"
# 校验
with app.test_request_context(method="POST", data={}):
f = BookForm()
print(f.validate(), f.errors)
实跑输出:
--------------------------------------------------------------------
表单与请求内容:四种取数姿势
--------------------------------------------------------------------
① 查询串:request.args
--------------------------------------------------------------------
GET /args?name=张三&tag=a&tag=b
dict(request.args) -> {"name": "张三", "tag": "a"}
request.args.getlist('tag') -> ['a', 'b']
! 同名参数出现两次,dict() 只留下第一个。
要用 getlist 才拿得到全部——这就是「勾选框多选」传上来的样子。
② 表单:request.form
--------------------------------------------------------------------
POST 表单 -> {"ct": "application/x-www-form-urlencoded", "got": {"pwd": "1234", "user": "李四"}}
GET /form -> 200 用 POST 提交
! GET 不带正文。表单必须用 POST 提交,用 GET 取 form 永远是空的——这是新手第一个坑。
③ JSON:request.get_json
--------------------------------------------------------------------
POST JSON -> {"parsed": {"a": 1, "b": [2, 3]}, "raw_len": 21}
POST 一个坏 JSON,silent=True 时 -> {"parsed": null, "raw_len": 14}
! 不加 silent,坏 JSON 会直接抛 400。
加了就变成 None,要不要兜底由你决定——接口里通常自己兜底并返回错误码。
④ 文件:request.files
--------------------------------------------------------------------
POST 一个 2048 字节的文件 -> {"bytes": 2048, "filename": "a.png"}
不传文件 -> 400 {"err":"没收到文件"}
! request.files 只有在 multipart/form-data 时才有内容。第 7 章的上传功能就是这一节加一个存盘。
⑤ 校验:WTForms 接手
--------------------------------------------------------------------
空表单 -> validate()=False,
错误 {'title': ['This field is required.'], 'price': ['This field is required.']}
书名太长且价格 10000 -> validate()=False,
错误 {'title': ['Field must be between 1 and 10 characters long.'], 'price': ['Number must be between 1 and 9999.']}
价格填 0 -> validate()=False,错误 {'price': ['This field is required.']}
正常输入 -> validate()=True,title=Flask,price=89
! 注意价格填 0 那行:它明明有值,报的却是「必填」。
因为 DataRequired 判的是真假值,0 在 Python 里等于假。要允许 0,得把校验器换成 InputRequired。
! validators 不是装饰,是校验规则的清单。每个字段可以挂一串,全部通过 validate() 才返回 True。
⑥ 校验错了怎么回到页面
--------------------------------------------------------------------
| 字段 | 错误信息 |
|---|---|
| title | 必填 |
| price | 要在 1 到 9999 之间 |
f.errors 是一个字典:键是字段名,值是错误信息的列表。
模板里把它渲染在字段下方,就是教材 1.3 节那张表单的样子。
dict(request.args) 只留第一个 tag,getlist 才拿到两个——这一条对多选筛选是致命的。
表单那一段特意同时打印 Content-Type,就是为了说明「取法要看编码」。
价格填 10000 报的是「要在 1 到 9999 之间」,填 0 报的却是「必填」,两个错误信息不一样,
原因也不一样。
文件那一段传了 2048 字节,读出来也是 2048——但存盘还没做。
30 秒实验
- 把
NumberRange(1, 9999)换成NumberRange(0, 9999),价格填 0 还会报「必填」吗? - 给
title再加一个Regexp(r'^[^<>]+$'),试试输入<b>x</b>会报什么。 - 用
request.values取一次,看它能不能同时兼容 args 和 form——想一想为什么正式代码里很少用它。
常见误区
- 用
args取表单数据:永远是空字典,而且不报错。 dict(request.args)处理多选:只留第一个值,静默丢数据。- 信任用户输入:长度、类型、范围、格式,一样都不能省。
DataRequired用在数字字段上:0 会被判成没填,价格、数量都会中招。
自测三问(点开对答案)
1. 表单数据该用哪个对象取? —— `request.form`。`request.args` 只读查询串,取表单永远是空的。 2. 多选参数怎么拿全? —— 用 `getlist`——args 和 form 都有这个方法,`dict()` 只留第一个。 3. 为什么价格填 0 会报「必填」? —— `DataRequired` 判的是真假值,0 等于假。要允许 0 就用 `InputRequired`。数据收进来了,下一步是告诉用户「成没成」。下一篇讲 flash 消息与日志,一个给人看,一个给排查用。
- ☐ (本篇完,下一篇 1.7 Flash 消息与日志记录)