关于本文
本文是通过利用生成式AI的自动化生成流程创建的。基于Python 3.13的官方urllib.parse文档,制作了一个不连接网络的虚拟URL查询检查示例。已在Linux / Python 3.13.5下确认了18个单元测试、语法检查以及正常和异常情况下的CLI输出。验证状态:🧪 在Linux / Python 3.13.5上本地运行,18个测试全部成功。未在Windows/macOS上验证。未运行GitHub Actions和Codex CLI。
支持环境、功能与限制事项
| 项目 | 内容 |
|---|---|
| 目标 | Python 3.10及以上(标准库) |
| 可实现的功能 | 计算虚拟查询中的重复键和空值 |
| 通信与费用 | 无网络通信,无额外费用 |
| 实测与限制 | Linux / Python 3.13.5。未在Windows/macOS上验证 |
“URL末尾出现了两次相同的项,究竟会使用哪一个呢?”在研究Web应用或API的行为时,我们常常会遇到这样的疑问。例如,在接下来的字符串中,tag出现了两次,empty出现了一次。
tag=one&tag=two&empty=
这里重要的是,发现重复与服务器采用哪个值是两码事。这次我们使用Python标准库的urllib.parse.parse_qsl()来进行计数,且不会丢失任何键值对。这是一个不与实际Web服务通信、不在屏幕上显示键名或值的简短测试程序。
在转换为字典之前检查键值对
parse_qsl()将查询字符串转换为“键值对列表”。另一方面,parse_qs()返回一个将每个键的值汇集为列表的字典。两者各有用途,但如果想确认最初的出现顺序和重复次数,parse_qsl()会更直观。
官方说明位于Python 3.13 urllib.parse文档中。keep_blank_values决定是否保留空值,而strict_parsing则是决定如何处理无法解析的项的参数。Python 3.10及更高版本可以明确指定separator='&',本教程也只使用&作为分隔符。
所需准备
需要 Python 3.10 或更高版本以及audit_query.py、test_audit_query.py、README。无需管理员权限或附加库。本次实际测试基于 Linux / Python 3.13.5,未单独验证 Windows 和 macOS 环境。
完整代码和测试位于文末的每日代码示例中。首先,请将 3 个文件放入一个新的工作文件夹中。在 Windows 中使用py -3,在 Linux/macOS 中使用python3等,根据您的实际环境选择 Python 启动命令。
注意:请勿将真实的验证 URL 或令牌传递给--query。 本示例的输出中不包含任何值,但命令行参数本身可能会保留在 shell 历史记录或操作系统进程列表中。以下全部为虚构字符串。
首先运行:包含重复键的示例
在示例目录中运行以下命令。
python3 audit_query.py --query 'tag=one&tag=two&empty='
在 Linux / Python 3.13.5 上实际验证的输出如下所示。退出代码为0。
STATUS=VALID FIELDS=3 UNIQUE_KEYS=2 DUPLICATE_KEYS=1 EXTRA_OCCURRENCES=1 BLANK_VALUES=1
分开理解这些数字的含义:FIELDS=3表示共有 3 个键值对,UNIQUE_KEYS=2表示有 2 种不同的键名。DUPLICATE_KEYS=1表示有 1 种“出现两次或多次的键名”。EXTRA_OCCURRENCES=1表示除了每个键的第一次出现之外,多余出现次数的总和。BLANK_VALUES=1表示有一个键值为空字符串的键值对。
例如tag=one&tag=two&tag=three这样,即使重复的键名只有一种,多出来的出现次数也有两次。将这两者区分开来,就不会把“有多少种键存在问题”与“多出来的项目总共有多少个”混淆。
工作原理:指定了什么来进行解析
完整版的核心部分如下。这只是用于说明的摘录,包含错误处理和输入限制的可执行完整版本以 GitHub 上的为准。
from urllib.parse import parse_qsl
pairs = parse_qsl(
'tag=one&tag=two&empty=',
keep_blank_values=True,
strict_parsing=True,
encoding='utf-8',
errors='strict',
max_num_fields=100,
separator='&',
)
print(len(pairs)) # 3
keep_blank_values=True如果没有它,empty=这样的项目在默认解析中可能会被排除。在业务的输入检查中,“没有该项目”与“有该项目但为空”可能具有不同的含义。因此,我们保留空值并将其计算在内。
errors='strict'用于指定:如果从百分号编码还原出的字节序列作为 UTF-8 是非法的,则报错而不是使用替代字符默默继续。此外,max_num_fields=100是为了防止解析过多项目而设置的教学用上限,并不是 URL 规范本身规定最多只能有 100 项。
改变一个条件:损坏的 % 编码
接下来是%后面没有跟两位十六进制数的例子。
python3 audit_query.py --query 'a=%ZZ'
实际输出如下,退出代码为2。
STATUS=INVALID ISSUE=INVALID_PERCENT_ESCAPE
这里是一个意想不到的陷阱。Python 的parse_qsl()并不是一个总会对损坏的百分号转义报错的验证器。因此在完整版中,我们在解析前会单独检查%后面是否紧跟两位十六进制数。%FF如果在形式上是两位数但作为 UTF-8 是非法的,例如这种情况,则会使用另一种INVALID_UTF8进行处理。
通过这种区分,“编码写法错误”和“无法将编码后的字节序列作为UTF-8读取”就可以区分开来。这并不是进行修复或自动重新发送,而是一份用于在更改输入之前确认原因的教材。
+、%2B,大小写会改变结果
在URL查询中,+被当作空格处理,而%2B则变成作为字符的加号。此外a=1&%61=2在解码后表现为同一个a键出现了两次。本代码会区分键名的大小写。也就是说Tag=1&tag=2会被作为两种不同的键来计算。
然而,实际的Web应用是否区分键名大小写,以及使用重复值的第一个、最后一个还是全部是另一个问题。请确认服务器、框架以及应用程序侧的规范。本程序不会推测这些采用规则。
失败时的故障排除
| 显示 | 主要调查内容 |
|---|---|
INVALID_PERCENT_ESCAPE | %后面是否跟着两位十六进制数 |
INVALID_UTF8 | %FF是否存在不合法的 UTF-8 字节序列(例如 |
INVALID_FIELD | a=1&b、&&等),格式是否遭到破坏(例如 |
EMPTY_KEY | =value),键名是否为空(例如 |
FIELD_LIMIT | 是否超过 100 项(教材特有限制) |
QUERY_TOO_LONG | 是否超过 4096 个字符(教材特有限制) |
若输入无效则返回退出码2,若有效则返回0。这样可以避免校验失败却显示成功并传递给下一步处理的情况。此外,--query若连自身都未指定等 CLI 参数错误,则作为argparse处理,这与相同的2程序本身的输入校验必须予以区分。
这 18 个测试验证了什么
执行的命令如下。
python3 -m unittest discover -p 'test_*.py' -v python3 -m py_compile audit_query.py test_audit_query.py
在 Linux / Python 3.13.5 下确认 18 项全部通过(PASS),语法检查通过(PASS)。测试了重复键、空值、%61与a、+与%2B、UTF-8 非法字符、字段数与字符数限制、CLI 的正常与异常情况。单元测试还确认了输出中不包含虚拟值。此环境中不存在 Codex CLI,未予执行。由于账单限制,也未执行 GitHub Actions。Windows/macOS 的结果未通过推测进行补充。
在实际业务中,切勿与“安全 URL”判定混淆
本教材仅检查查询的格式和出现次数。它不证明身份验证、授权、签名、URL 风险或服务器端的解释是否正确。查询字符串中可能包含个人信息或凭据。如果要将其扩展为检查实际业务数据的机制,则需要另外设计不将机密传递给命令行的输入路径、日志处理、访问权限以及数据保留期限。
此外,请参阅 Python 的关于 URL 解析的安全注意事项。切勿直接将标准库的解析结果认定为“安全输入”。本示例同样无法检测所有类型的恶意 URL。
总结:重复键的检查应从小规模开始尝试
parse_qsl(),在其中保存键值对,并保留空值进行计数。格式错误通过退出代码进行区分,且不显示输入值。仅凭这三点,也比单纯将 URL 作为字符串看待更容易整理原因。建议最初使用虚构的 3 个条目进行运行,tag然后每次更改一个条件(例如将次数增加到 3 次),以确认计数方式的差异。
