用 Python 标准库解析 URL 查询参数:重复值、空值与 ticket 脱敏
处理搜索链接、回调地址或日志时,查询参数看起来只是 key=value,却很容易在重复字段、空值和编码上丢失信息。Python 标准库的 urllib.parse 已经提供了合适的工具,关键是选对返回结构和参数。
例如下面这条教学链接。域名和 ticket 都是虚构示例:
https://example.com/search?tag=python&tag=cloud&empty=&q=hello+world&symbol=%2B&ticket=demo-secret
tag 出现了两次,empty 的值为空,q 中的 + 表示空格,而 %2B 才表示字面意义的加号。不要先对整条链接解码再手工切割:值里的 %26 如果提前变成 &,就可能被误当成参数分隔符。
可以先用 urlsplit 提取查询字符串,再交给 parse_qsl。它返回键值对列表,能保留重复参数及其顺序。
import re
from urllib.parse import parse_qsl, urlencode, urlsplit
BAD_PERCENT_ESCAPE = re.compile(r"%(?![0-9A-Fa-f]{2})")
SENSITIVE_KEYS = {"ticket", "token", "access_token", "api_key", "password"}
def parse_query(url: str, *, max_fields: int = 100) -> list[tuple[str, str]]:
query = urlsplit(url).query
if BAD_PERCENT_ESCAPE.search(query):
raise ValueError("查询参数包含无效的百分号编码")
return parse_qsl(
query,
keep_blank_values=True,
encoding="utf-8",
errors="strict",
max_num_fields=max_fields,
separator="&",
)
def redacted_query(url: str) -> str:
pairs = parse_query(url)
return urlencode([
(key, "REDACTED" if key.casefold() in SENSITIVE_KEYS else value)
for key, value in pairs
])
这里的几个设置各有用途:
keep_blank_values=True保留empty=。默认行为会丢弃空值;本例中不带等号的flag也会解析为("flag", ""),两种原始写法不再区分。errors="strict"让无效 UTF-8 编码抛出异常,避免悄悄替换成其他字符。但它不会拒绝%ZZ这类不完整的百分号编码,因此示例额外检查了%后是否有两位十六进制数字。max_num_fields=100限制字段数量,重复字段也会计数。它不限制单个值或整条 URL 的长度,长度上限应另外设置。separator="&"明确参数分隔符,避免把分隔规则留给猜测。
运行下面的示例,可以观察解析和脱敏结果:
demo_url = (
"https://example.com/search?tag=python&tag=cloud&empty="
"&q=hello+world&symbol=%2B&ticket=demo-secret"
)
pairs = parse_query(demo_url)
# 仅为展示虚构示例;真实日志不要输出原始敏感参数。
print(pairs)
print([value for key, value in pairs if key == "tag"])
print(redacted_query(demo_url))
输出为:
[('tag', 'python'), ('tag', 'cloud'), ('empty', ''), ('q', 'hello world'), ('symbol', '+'), ('ticket', 'demo-secret')]
['python', 'cloud']
tag=python&tag=cloud&empty=&q=hello+world&symbol=%2B&ticket=REDACTED
不要直接执行 dict(pairs) 来处理允许重复的字段,否则前面的 tag=python 会被后面的值覆盖。需要按名称取出多值时,可以像示例一样筛选,或使用返回“键到值列表”映射的 parse_qs。如果某个字段按业务规则只能出现一次,应显式检查数量;不能让不同组件各自决定取第一个还是最后一个值。
脱敏时也要遍历全部键值对,不能只处理第一个 ticket。示例先完成参数名解码,再用 casefold() 匹配,所以重复的 ticket、大写的 TICKET 和编码后的 %74icket 都会被遮盖。这里的大小写不敏感是日志脱敏策略,不是在改变业务参数的匹配规则。
这个函数只生成脱敏后的查询字符串,不负责完整 URL 的安全校验,也不会处理 URL 用户信息或片段中的秘密。敏感字段集合仍可能遗漏业务自定义字段;生产日志更适合只记录预先允许的参数,并避免输出原始 URL。
重新生成查询字符串时,对键值对列表使用 urlencode(pairs) 就能保留重复项;若输入是 {"tag": ["python", "cloud"]} 这样的字典,则要使用 urlencode(data, doseq=True)。重新编码可以保留参数值的含义,却不保证原始文本完全一致,例如空格可能被写成 +。涉及 URL 签名时,应按协议保留或规范化原始内容,不能随意解析后重写。
以上代码已在 Python 3.9.6 上运行验证。11 项测试覆盖重复字段、空值、加号与中文编码、值内分隔符、非法编码、字段数量限制、重复敏感字段脱敏、编码往返及片段边界。
参考:Python 官方标准库文档中的 urllib.parse 章节。
- 点赞
- 收藏
- 关注作者
评论(0)