2. Characters (字符)
本章定义URI中使用的字符集、编码机制和处理规则。
字符编码基础
URI语法提供了一种将数据(可能用于标识资源)编码为字符序列的方法。
编码层次:
资源 → URI字符 → 八位字节 → 传输/存储
字符集: URI基于US-ASCII字符集,由数字、字母和少数图形符号组成
2.1. Percent-Encoding (百分号编码)
目的
当八位字节对应的字符不在允许集中或被用作分隔符时,使用百分号编码机制来表示组件中的数据八位字节。
编码格式
pct-encoded = "%" HEXDIG HEXDIG
格式: 百分号字符 % 后跟表示该八位字节数值的两个十六进制数字
示例
| 字符 | 二进制 | 十六进制 | 百分号编码 |
|---|---|---|---|
| 空格 | 00100000 | 0x20 | %20 |
| ! | 00100001 | 0x21 | %21 |
| # | 00100011 | 0x23 | %23 |
| 中文"你" | - | 0xE4BDA0 | %E4%BD%A0 |
大小写规则
等价性: 大写十六进制数字'A'到'F'等同于小写数字'a'到'f'
规范化: 仅在十六进制数字大小写上不同的两个URI是等价的
建议: URI生产者和规范化器应对所有百分号编码使用大写十六进制数字
推荐: %2F %3A %5B
不推荐: %2f %3a %5b
2.2. Reserved Characters (保留字符)
定义
URI包含由"保留"集中的字符分隔的组件和子组件。这些字符被称为"保留",因为它们可能(或可能不)被定义为分隔符。
保留字符集
reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
分类
通用分隔符 (gen-delims)
| 字符 | 用途 | 示例 |
|---|---|---|
| : | 分隔方案和授权 | http: |
| / | 路径分隔符 | /path/to/resource |
| ? | 查询分隔符 | ?key=value |
| # | 片段分隔符 | #section |
| [ ] | IPv6地址 | [2001:db8::1] |
| @ | 用户信息分隔符 | user@host |
子分隔符 (sub-delims)
| 字符 | 常见用途 |
|---|---|
| ! $ ' ( ) * | 路径或查询中的子组件分隔 |
| + | 空格的替代表示 |
| , | 列表分隔 |
| ; | 参数分隔 |
| = | 键值对分隔 |
| & | 查询参数分隔 |
编码规则
冲突处理: 如果URI组件的数据与保留字符作为分隔符的目的冲突,则必须在形成URI之前对冲突数据进行百分号编码
示例:
路径包含"?"字符:
原始: /path/file?.txt
编码: /path/file%3F.txt
查询包含"&"字符:
原始: ?name=Tom&Jerry
正确: ?name=Tom%26Jerry (如果&不是分隔符)
或: ?name=Tom&name=Jerry (如果&是分隔符)
等价性
重要: 在替换保留字符与其对应的百分号编码八位字节方面不同的URI是不等价的
http://example.com/path?key=value
http://example.com/path%3Fkey=value
这两个URI不等价
2.3. Unreserved Characters (非保留字符)
定义
URI中允许但没有保留目的的字符称为非保留字符。
非保留字符集
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
包括:
- ALPHA: 大写和小写字母 (A-Z, a-z)
- DIGIT: 十进制数字 (0-9)
- -: 连字符
- .: 句点
- _: 下划线
- ~: 波浪号
编码规则
等价性: 在替换非保留字符与其对应的百分号编码US-ASCII八位字节方面不同的URI是等价的
规范化: 百分号编码的非保留字符应该被解码
等价的URI:
http://example.com/~user
http://example.com/%7Euser
规范化为:
http://example.com/~user
百分号编码范围
不应创建:
- ALPHA:
%41-%5A(A-Z),%61-%7A(a-z) - DIGIT:
%30-%39(0-9) - 连字符:
%2D - 句点:
%2E - 下划线:
%5F - 波浪号:
%7E
应该解码: 当在URI中发现这些编码时,规范化器应将其解码为相应的非保留字符
2.4. When to Encode or Decode (何时编码或解码)
编码时机
URI生产者:
- 生成URI时,必须对不允许的字符进行百分号编码
- 保留字符仅在作为分隔符时才不编码
- 非保留字符不应编码
示例:
# 编码路径
path = "/files/my document.pdf"
encoded = "/files/my%20document.pdf"
# 编码查询
query = "?name=John Doe&age=30"
encoded = "?name=John%20Doe&age=30"
解码时机
URI消费者:
- 解析URI后,根据需要解码组件
- 不要过早解码(可能改变URI结构)
- 每个组件只解码一次
危险示例:
原始: /path%2Fto%2Ffile
过早解码: /path/to/file (改变了路径结构!)
正确: 解析后再解码每个段
段1: "path%2Fto%2Ffile" → 解码 → "path/to/file"
双重编码问题
原始数据: "100%"
第一次编码: "100%25"
错误的第二次编码: "100%2525"
解码时:
"100%2525" → "100%25" → "100%"
2.5. Identifying Data (识别数据)
字符集与编码
字符 vs 八位字节:
- URI是字符序列
- 字符编码为八位字节用于传输/存储
- UTF-8是推荐的字符编码
国际化资源标识符 (IRI)
IRI扩展: RFC 3987定义了IRI,允许使用Unicode字符
转换:
IRI: http://例え.jp/引き出し
↓ 编码为UTF-8并百分号编码
URI: http://xn--r8jz45g.jp/%E5%BC%95%E3%81%8D%E5%87%BA%E3%81%97
最佳实践
URI生产:
- 使用UTF-8编码非ASCII字符
- 对编码后的八位字节进行百分号编码
- 使用大写十六进制数字
- 不要编码非保留字符
URI消费:
- 按组件解析
- 解码百分号编码
- 使用UTF-8解释八位字节
- 处理无效编码
字符集快速参考
完整字符分类
URI字符
├── 非保留字符 (unreserved)
│ ├── ALPHA: A-Z, a-z
│ ├── DIGIT: 0-9
│ └── 符号: - . _ ~
│
├── 保留字符 (reserved)
│ ├── 通用分隔符 (gen-delims): : / ? # [ ] @
│ └── 子分隔符 (sub-delims): ! $ & ' ( ) * + , ; =
│
└── 百分号编码 (pct-encoded): %HEXDIG
HEXDIG
编码决策树
字符需要出现在URI中?
├─ 是非保留字符? → 直接使用
├─ 是保留字符?
│ ├─ 用作分隔符? → 直接使用
│ └─ 用作数据? → 百分号编码
└─ 其他字符? → 百分号编码
常见字符编码表
| 字符 | 用途 | 编码 |
|---|---|---|
| 空格 | 分隔 | %20 或 + (查询中) |
| ! | 子分隔符 | %21 (如需编码) |
| " | 引号 | %22 |
| # | 片段分隔符 | %23 (数据中) |
| $ | 子分隔符 | %24 (如需编码) |
| % | 编码标记 | %25 |
| & | 参数分隔 | %26 (数据中) |
| ' | 子分隔符 | %27 (如需编码) |
| ( ) | 子分隔符 | %28 %29 |
| + | 空格/子分隔符 | %2B (数据中) |
| , | 列表分隔 | %2C (如需编码) |
| / | 路径分隔符 | %2F (数据中) |
| : | 方案分隔 | %3A (数据中) |
| ; | 参数分隔 | %3B (如需编码) |
| = | 键值分隔 | %3D (如需编码) |
| ? | 查询分隔符 | %3F (数据中) |
| @ | 用户信息分隔 | %40 (数据中) |
| [ ] | IPv6边界 | %5B %5D |
实现建议
编码实现
def percent_encode(text, safe=''):
"""百分号编码文本"""
result = []
for char in text:
if char in safe or is_unreserved(char):
result.append(char)
else:
# UTF-8编码并百分号编码
for byte in char.encode('utf-8'):
result.append(f'%{byte:02X}')
return ''.join(result)
def is_unreserved(char):
"""检查是否为非保留字符"""
return (char.isalnum() or
char in '-._~')
解码实现
def percent_decode(text):
"""百分号解码文本"""
result = bytearray()
i = 0
while i < len(text):
if text[i] == '%' and i + 2 < len(text):
try:
byte = int(text[i+1:i+3], 16)
result.append(byte)
i += 3
except ValueError:
result.extend(text[i].encode('utf-8'))
i += 1
else:
result.extend(text[i].encode('utf-8'))
i += 1
return result.decode('utf-8', errors='replace')
下一章: 3. Syntax Components (语法组件) - URI的结构组件