编码过程与允许集
百分号编码分两步:把字符按字符编码(现代 Web 事实标准是 UTF-8)转成字节,再把每个不属于当前组件允许集的字节写成 %加两位十六进制。允许集由 RFC 3986 定义:未保留字符(字母、数字与 - . _ ~)永不编码;保留字符(: / ? # [ ] @ 与 ! $ & ' ( ) * + , ; =)有语法含义,用作分隔符时保持原样、用作数据时必须编码;其余字符一律编码。理解这一点就能解释「为什么同一个参数在路径与查询串里编码结果不同」。
基本信息
| 形式 | % 加两位十六进制,如 %E4%B8%AD |
|---|---|
| 字符编码来源 | 通常按 UTF-8 编码后再逐字节转义 |
| 未保留字符 | 字母、数字、- . _ ~,永远不编码 |
| 保留字符 | 有语法含义,作数据时必须编码 |
| 组件差异 | 路径、查询、片段允许的字符集不同 |
| 常用实现 | encodeURIComponent、urlencode、quote 等 |
典型使用场景
- 拼接口参数:对参数值编码后再拼接,避免分隔符被误解析
- 生成分享链接:中文标题与特殊符号必须编码才能安全传递
- 签名与校验:签名前的规范化要求固定的编码规则,选错会导致签名不一致
- 日志分析:识别被编码过的内容,避免把 %E4%B8%AD 当成普通字符串处理
- 爬虫与抓包:还原编码内容以查看真实参数
编码示例
编码示例
python -c "import urllib.parse as u; print(u.quote('中文 值'))" # 路径风格:空格为 %20,中文为 UTF-8 字节
python -c "import urllib.parse as u; print(u.quote_plus('中文 值'))" # 表单风格:空格为 +
python -c "import urllib.parse as u; print(u.quote('中', encoding='gbk'))" # 不用 GBK 生成 URL
# 按参数逐个编码再拼接,不要编码整条 URL
python -c "import urllib.parse as u; print('https://e.com/s?' + u.urlencode({'q':'中文 & 值'}))"
# 解码还原
python -c "import urllib.parse as u; print(u.unquote('%E4%B8%AD%E6%96%87'))" # 中文字符类别与处理
| 类别 | 示例 | 处理方式 |
|---|---|---|
| 未保留字符 | A z 0 9 - . _ ~ | 永不编码 |
| 保留字符(作分隔符) | : / ? # & = | 保持原样以维持结构 |
| 保留字符(作数据) | 参数值中的 & = + | 必须编码为 %XX |
| 百分号自身 | % | 作为数据时编码为 %25 |
| 非 ASCII 字符 | 中文、emoji | 先按 UTF-8 编成字节再逐字节转义 |
| 控制字符 | 换行、制表 | 必须编码,禁止出现在 URL 中 |
编码发生在字节层:同一个字符在不同源编码下转义结果不同。历史上用 GBK 生成 URL 编码会得到 %D6%D0 之类的序列,现代系统按 UTF-8 解码就会失败或乱码,因此不要用非 UTF-8 编码生成 URL 内容。
编码与安全直接相关:参数值里的 & 与 = 若未编码,会被解析成新的参数(参数注入);路径里的 .. 与编码变体可能绕过路径校验。服务端应解码后再校验,并对已解码路径做规范化。
签名场景要求编码规则完全一致:OAuth 等规范对哪些字符编码、空格用哪种形式都有明确规定,实现时不能凭直觉替换成「等价的 %20 或 +」,否则签名校验会以「部分请求失败」的形式间歇报错。