← 中文资源

简体中文版本

查询字符串多值解析器

把重复出现的查询参数解析为多值映射,稳定重建查询串,并安全读取单值参数与布尔开关。

阅读英文原文

功能说明

浏览器的表单、标签筛选和多选控件会产生同名多值参数,例如 tag=python&tag=go。直接使用 dict 解析会丢失前面的值,因此这里把每个键映射到值列表,保留原始顺序。多值映射还便于重建规范的查询串,用于分页链接、缓存键或签名前的参数归一化。

build_multimap 支持按键排序,保证同一组参数在不同调用中生成完全相同的字符串;get_one 和 get_bool 则在读取单值参数时提供显式校验,避免多值覆盖或误判真假值。

关键代码

parse_qsl 负责解码百分号转义并把加号还原为空格,keep_blank_values 让空字符串也保留,从而区分未传参数与传了空值。build_multimap 先按键排序,再按值顺序展开成键值对,最后交给 urlencode。get_one 在值多于一个时抛出 ValueError;get_bool 把常见真值和假值归一化,其他输入直接拒绝,而不是静默当作假。

使用注意

查询串长度有限制,超长多值参数应考虑改走 POST 或压缩。键名大小写敏感,需要与后端约定一致。空值会被解析为空字符串,业务上要明确它与缺失参数的区别。get_bool 不接受空字符串,若前端可能提交空值,应先做默认值处理。重建查询串会重新编码,原有参数的顺序和转义形式可能与输入不同,签名场景务必约定排序规则。

验证方式

示例查询包含两个 tag、一个 page、一个 debug 和一个空值参数。打印结果中 tag 对应 python 和 go 两项,page 为字符串 2,debug 解析为 True,empty 是空字符串而非 None。重建后的查询串按 debug、empty、page、tag 顺序排列,两个 tag 依次出现。对重复的 tag 调用 get_one 会输出 rejected 提示。

代码示例

from collections import defaultdict
from collections.abc import Iterable
from urllib.parse import parse_qsl, urlencode

Params = dict[str, list[str]]


def parse_multimap(query: str) -> Params:
    grouped: dict[str, list[str]] = defaultdict(list)
    for key, value in parse_qsl(query, keep_blank_values=True):
        grouped[key].append(value)
    return dict(grouped)


def build_multimap(params: dict[str, Iterable[str]], *, sort_keys: bool = True) -> str:
    pairs: list[tuple[str, str]] = []
    keys = sorted(params) if sort_keys else list(params)
    for key in keys:
        for value in params[key]:
            pairs.append((key, value))
    return urlencode(pairs)


def get_one(params: Params, key: str, default: str | None = None) -> str | None:
    values = params.get(key)
    if not values:
        return default
    if len(values) > 1:
        raise ValueError(f"{key} must not repeat")
    return values[0]


def get_bool(params: Params, key: str, default: bool = False) -> bool:
    value = get_one(params, key)
    if value is None:
        return default
    normalized = value.strip().lower()
    if normalized in {"1", "true", "yes", "on"}:
        return True
    if normalized in {"0", "false", "no", "off"}:
        return False
    raise ValueError(f"{key} is not a boolean")


def main() -> None:
    query = "tag=python&tag=go&page=2&debug=1&empty="
    params = parse_multimap(query)
    print(params)
    print("tags:", params["tag"])
    print("page:", get_one(params, "page"))
    print("debug:", get_bool(params, "debug"))
    print("empty:", repr(get_one(params, "empty")))
    print(build_multimap(params))
    try:
        get_one({"tag": ["a", "b"]}, "tag")
    except ValueError as error:
        print("rejected:", error)


if __name__ == "__main__":
    main()