用 Python 正确构造 URL 查询参数:中文、加号、重复键与本地验证
手工拼接 ?q= 时,搜索词里的 & 变成了新参数,C++ 到服务端却成了 C ,中文还可能触发 URL 编码错误。本文用 Python 标准库构造查询字符串,保留重复参数和空值,并通过本地 HTTP 请求检查接收端得到的实际数据。
教程面向初学者到中级读者,适合 Linux 自动化脚本和小型 API 客户端。示例只生成 URL;自检只访问本机,不需要账号或 API 密钥。
1. 方法:传原始值,统一编码一次
- 将参数保存为
(key, value)列表,允许同一个键出现多次。 - 拆分基础 URL,读取已有参数,再追加新参数。
- 用
urlencode()编码查询部分,最后组合 URL。
urlencode() 官方文档说明了查询编码和 doseq。默认空格编码为 +,字面加号编码为 %2B;下面的 CLI 使用字符串参数列表。若代码使用“字典中的列表”,需要这样展开:
from urllib.parse import urlencode
query = urlencode({"tag": ["python", "linux"], "q": "C++"}, doseq=True)
assert query == "tag=python&tag=linux&q=C%2B%2B"
新增参数始终传未编码的值。 想表达加号就传 +,传 %2B 表示字面文本 %2B,输出会包含 %252B。不要对整个 URL 调用编码函数,否则连结构分隔符也可能被编码。
2. 准备环境
需要 Python 3.10+ 和 curl,无需第三方 Python 包。示例已在 Linux、Python 3.11.4 上测试;命令适用于 Bash 和 zsh。
- 新建练习目录并下载两个脚本:
python3 --version
mkdir url-query-demo
cd url-query-demo
curl -fS https://www.bobobk.com/downloads/url-query/build_query.py -o build_query.py
curl -fS https://www.bobobk.com/downloads/url-query/check_query.py -o check_query.py
python3 build_query.py --help
build_query.py的完整实现如下,也可直接保存。自检脚本 check_query.py 和 URL 构造脚本 要放在同一目录。
3. 完整实现:保留已有参数再追加
"""Append raw string query parameters to a trusted HTTP URL. Python 3.10+."""
import argparse
import sys
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
def parameter(text):
key, found, value = text.partition("=")
if not found or not key:
raise argparse.ArgumentTypeError("use a nonempty KEY=VALUE; VALUE may be empty")
return key, value
def build_url(base, additions):
if not base.isascii() or any(ord(c) <= 32 or ord(c) == 127 for c in base):
raise ValueError("base must be an ASCII URL without whitespace or controls")
parts = urlsplit(base)
if parts.scheme not in {"http", "https"} or not parts.hostname:
raise ValueError("base must have an http(s) scheme and a host")
# Accessing port also rejects malformed or out-of-range port numbers.
_ = parts.port
pairs = parse_qsl(
parts.query, keep_blank_values=True, encoding="utf-8",
errors="strict", max_num_fields=1000, separator="&",
)
pairs.extend(additions)
if len(pairs) > 1000:
raise ValueError("at most 1000 query parameters are allowed")
query = urlencode(pairs, encoding="utf-8", errors="strict")
return urlunsplit(parts._replace(query=query))
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("base", help="trusted, already encoded base URL")
parser.add_argument(
"--param", action="append", type=parameter, default=[],
metavar="KEY=VALUE", help="raw string parameter; repeat to append more",
)
args = parser.parse_args()
try:
result = build_url(args.base, args.param)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
print(result)
return 0
if __name__ == "__main__":
raise SystemExit(main())
parse_qsl() 返回参数对列表;keep_blank_values=True 保留空值。本文策略是追加,不会覆盖已有同名键;限制最终参数数目为 1000。partition("=") 只切开第一个等号,因此值里可以继续包含 =。
基础 URL 应是可信、已经正确编码的 ASCII 地址。代码做基础格式检查;它不是完整 URL 校验器,也不编码路径或国际化域名。已有查询字符串会解码再编码,所以 %20 可能变成 +;带签名、依赖原始字节的 URL 不适合此流程。片段 #results 保留在生成结果中。
4. 运行并校验结果
- 传入中文、加号、重复参数和值内分隔符:
python3 build_query.py 'https://example.com/search?page=1&empty=&tag=old#results' \
--param 'q=C++ & Linux/中文' \
--param 'tag=python' --param 'tag=linux' \
--param 'note=a=b#c' > url.txt
cat url.txt
预期输出:
https://example.com/search?page=1&empty=&tag=old&q=C%2B%2B+%26+Linux%2F%E4%B8%AD%E6%96%87&tag=python&tag=linux¬e=a%3Db%23c#results
Shell 引号保护参数不被终端拆开,URL 编码保护参数不被服务器误解;两步都需要。此命令不请求 example.com。
- 还原参数并核对顺序和值:
python3 - <<'PY_CHECK'
from pathlib import Path
from urllib.parse import parse_qsl, urlsplit
parts = urlsplit(Path("url.txt").read_text(encoding="utf-8").strip())
assert parts.fragment == "results"
assert parse_qsl(parts.query, keep_blank_values=True) == [
("page", "1"), ("empty", ""), ("tag", "old"),
("q", "C++ & Linux/中文"), ("tag", "python"),
("tag", "linux"), ("note", "a=b#c"),
]
print("OK: query values and order")
PY_CHECK
应输出 OK: query values and order。tag=old 仍在,后面追加了两个 tag;empty= 仍然存在。若业务需要替换某个键,应先按键过滤旧参数,再追加新值;不要先转换为字典而丢失其他重复参数。
5. 用本地 HTTP 服务核对实际请求
执行下载的自检:
python3 check_query.py
预期输出:
OK: HTTP round trip, duplicate/blank values, fragment, and CLI exits
自检在 127.0.0.1 的临时端口启动服务,用实际 GET 请求回显参数并断言中文、加号、重复键、空值、=、# 和字面 %2B 均保留。它还核对生成 URL 包含片段,而服务端收到的请求不含片段,并在结束时停止服务、关闭端口。
服务基于 http.server,仅用于本地练习。测试客户端通过 ProxyHandler({})绕过代理,设置 3 秒阻塞操作超时;没有改动系统代理设置。
6. 验证失败退出
以下命令适用于交互终端及启用 set -e 的 Shell 脚本:
if python3 build_query.py 'file:///tmp/input'; then
printf 'UNEXPECTED success\n'
else
printf 'exit=%s\n' "$?"
fi
if python3 build_query.py 'https://example.com/' --param 'missing-equals'; then
printf 'UNEXPECTED success\n'
else
printf 'exit=%s\n' "$?"
fi
第一个例子将错误写到 stderr,输出 exit=1;第二个显示参数用法错误并输出 exit=2。成功返回 0,失败不会输出成功 URL。--param 'empty=' 合法;--param '=value' 因空键返回 2。
7. 常见问题与修复
| 现象 | 具体修复 |
|---|---|
服务端的 C++ 变成空格 |
把原始 C++ 交给 --param,不要直接拼进查询字符串。 |
出现 %252B |
检查是否提前编码;想传加号时用 +。若原值就是 %2B,该输出正常。 |
| 同名参数只剩一个 | 使用参数对列表,或对字典列表值使用 doseq=True。 |
| 空值消失 | 解码检查时指定 keep_blank_values=True。 |
| 基础 URL 有空格或中文路径 | 先准备正确编码的基础 URL;本工具只处理新增查询参数。 |
| 自检无法监听本机端口 | 换到允许回环套接字的本地 Python 环境;不要把测试服务暴露到公网。 |
继续实践:
- Python HTTP JSON 请求:超时与有限重试:将生成的 URL 交给客户端。
- 用 Python 可靠下载文件:处理带查询参数的下载入口。
- Linux 定时任务路径排查:在定时脚本中使用明确的解释器和文件路径。
总结
把原始参数作为字符串传入,保留重复键与空值,再统一编码查询部分。先还原参数做断言,再用本地 HTTP 回显确认接收值,避免只凭 URL 外观判断正确性。接入真实接口时,还应按接口约定处理参数替换、超时和请求失败。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-url-query-encoding.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。