Python 现代开发学习笔记

(九)表单文本转成业务数据

1. 勾选框和标签需要解释

保存《Python Web》时,浏览器提交的不是 Python 数据类。书名是文本;勾选框勾选时提交 finished=1,没有勾选时通常根本没有这个字段;标签输入框提交一整段逗号分隔的文本。

本章规定书名去两端空白后为 1 至 100 个字符;标签用英文逗号分隔,去空白、去空项并合并重复项,最多 10 个不同标签,每项最多 30 个字符。所有者由登录身份决定。先把这些规则写成能独立调用的函数:

# 项目文件:shelf_project/src/shelfweb/validation.py
# 先清理书名和标签,再把表单的已读标记转换成数据库保存的整数。
import re
from dataclasses import dataclass


@dataclass(frozen=True)
class BookDraft:
    title: str
    finished: bool
    tags: list[str]


def read_book_form(form):
    values = {
        "title": form.get("title", ""),
        "finished": form.get("finished") == "1",
        "tags": form.get("tags", ""),
    }
    errors = {}
    for name in ["title", "finished", "tags"]:
        if len(form.getlist(name)) > 1:
            errors[name] = "同一字段请只提交一次"
    if set(form) - {"title", "finished", "tags", "csrf_token"}:
        errors["_form"] = "表单中有未识别的字段"
    title = values["title"].strip()
    if not 1 <= len(title) <= 100:
        errors["title"] = "书名请填写 1 至 100 个字符"
    if form.get("finished") not in {None, "1"}:
        errors["finished"] = "已读状态请使用表单中的勾选框"
    tags = list(
        dict.fromkeys(
            piece.strip() for piece in values["tags"].split(",") if piece.strip()
        )
    )
    if len(tags) > 10 or any(len(tag) > 30 for tag in tags):
        errors["tags"] = "最多填写 10 个标签,每个标签不超过 30 个字符"
    draft = None if errors else BookDraft(title, values["finished"], tags)
    return draft, values, errors

values 保留这次表单的显示值,失败时继续放回输入框;errors 按字段保存说明;只有检查通过,才构造 BookDraft。这份数据类和前一章的 Pydantic 输入模型承担相似的业务角色,校验动作在这里由函数显式执行。

form.getlist 检查同名字段,set(form) 取得字段名集合,再用集合差找出额外字段。伪造一个 owner_id 会得到表单错误,不会改变归属。form.get("finished") == "1" 则把一次明确的勾选转换为布尔值。

# 项目文件:demos/form_parse.py
# 表单字段按单值读取,保留用户填写内容供校验失败后重新渲染页面。
from werkzeug.datastructures import MultiDict

from shelfweb.validation import read_book_form

for data in [
    MultiDict(
        {"title": "  Python Web  ", "tags": " 网络, 网络, 基础 ", "finished": "1"}
    ),
    MultiDict({"title": "A", "tags": ""}),
    MultiDict([("title", "A"), ("title", "B")]),
]:
    draft, values, errors = read_book_form(data)
    print("表单值", values)
    print("处理结果", draft if not errors else errors)

第一份数据得到书名 Python Web、已读 True、标签 ["网络", "基础"];第二份没有勾选框字段,得到 False 和空标签;第三份有两个书名,保留错误信息,业务对象为 None。

同一份表单先保留显示文本,再去空白拆标签检查重复字段,成功构造 BookDraft,失败保留 values 和 errors 返回页面

2. HTML 检查与服务器检查连接

页面的 required、maxlength 会在浏览器提交前帮助用户检查。但测试客户端可以直接发送请求,所以服务端仍执行上面的规则。后面的表单片段把错误放在对应输入框旁边,不把失败输入写进数据库。

笔记正文与全部题解 · Ctrl / ⌘ K