2. 字符 (Characters)
本章定义 URI 中使用的字符集, 编码机制和处理规则.
字符编码基础 (Character Encoding Fundamentals)
URI 语法提供了一种将数据编码为字符序列的方法, 通常用于标识资源.
编码层次:
Resource → URI Characters → Octets → Transmission/Storage
字符集: URI 基于 US-ASCII 字符集, 该字符集由数字, 字母和少量图形符号组成.
2.1. 百分号编码 (Percent-Encoding)
目的
当某个数据八位字节在组件中对应的字符不属于允许集合, 或该字符正被用作定界符时, 使用百分号编码机制来表示该数据八位字节.
编码格式
pct-encoded = "%" HEXDIG HEXDIG
格式: 百分号编码由百分号字符 "%" 后跟两个十六进制数字组成, 这两个数字表示该八位字节的数值.
示例
| 字符 | 二进制 | 十六进制 | 百分号编码 |
|---|---|---|---|
| Space | 00100000 | 0x20 | %20 |
| ! | 00100001 | 0x21 | %21 |
| # | 00100011 | 0x23 | %23 |
| 中 (Chinese) | - | 0xE4B8AD | %E4%B8%AD |
大小写规则
等价性: 大写十六进制数字 'A' 到 'F' 与小写数字 'a' 到 'f' 等价.
规范化: 如果两个 URI 只在百分号编码八位字节所用十六进制数字的大小写上不同, 则它们等价.
建议: URI 生成者和规范化器应当对所有百分号编码使用大写十六进制数字.
Recommended: %2F %3A %5B
Not recommended: %2f %3a %5b
2.2. 保留字符 (Reserved Characters)
定义
URI 包含由 "reserved" 集合中的字符分隔的组件和子组件. 这些字符称为"保留", 因为它们可以 (也可以不) 被定义为定界符.
保留字符集
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 containing "?" character:
Original: /path/file?.txt
Encoded: /path/file%3F.txt
Query containing "&" character:
Original: ?name=Tom&Jerry
Correct: ?name=Tom%26Jerry (if & is not a delimiter)
Or: ?name=Tom&name=Jerry (if & is a delimiter)
等价性
重要: 如果两个 URI 的差异在于某个保留字符被替换为相应的百分号编码八位字节, 则它们不等价.
http://example.com/path?key=value
http://example.com/path%3Fkey=value
These two URIs are NOT equivalent
2.3. 非保留字符 (Unreserved Characters)
定义
允许出现在 URI 中但没有保留用途的字符称为非保留字符.
非保留字符集
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
包括:
- ALPHA: 大写和小写字母 (A-Z, a-z)
- DIGIT: 十进制数字 (0-9)
- -: 连字符
- .: 句点
- _: 下划线
- ~: 波浪号
编码规则
等价性: 如果两个 URI 的差异在于某个非保留字符被替换为相应的百分号编码 US-ASCII 八位字节, 则它们等价.
规范化: 对应非保留字符的百分号编码八位字节应当被解码.
Equivalent URIs:
http://example.com/~user
http://example.com/%7Euser
Normalized to:
http://example.com/~user
百分号编码范围
不应创建:
- ALPHA:
%41-%5A(A-Z),%61-%7A(a-z) - DIGIT:
%30-%39(0-9) - Hyphen:
%2D - Period:
%2E - Underscore:
%5F - Tilde:
%7E
应当解码: 当在 URI 中发现这些编码时, 规范化器应当将它们解码为对应的非保留字符.
2.4. 何时编码或解码 (When to Encode or Decode)
何时编码
URI 生成者:
- 生成 URI 时, 必须对不允许的字符进行百分号编码.
- 保留字符只有在用作定界符时才保持未编码.
- 非保留字符不应编码.
示例:
# Encoding path
path = "/files/my document.pdf"
encoded = "/files/my%20document.pdf"
# Encoding query
query = "?name=John Doe&age=30"
encoded = "?name=John%20Doe&age=30"
何时解码
URI 使用者:
- 解析 URI 之后, 按需要解码组件.
- 不要过早解码 (可能改变 URI 结构).
- 每个组件只解码一次.
危险示例:
Original: /path%2Fto%2Ffile
Premature decode: /path/to/file (changed path structure!)
Correct: Parse first, then decode each segment
Segment 1: "path%2Fto%2Ffile" → Decode → "path/to/file"
双重编码问题
Original data: "100%"
First encoding: "100%25"
Wrong second encoding: "100%2525"
When decoding:
"100%2525" → "100%25" → "100%"
2.5. 标识数据 (Identifying Data)
字符集和编码
字符与八位字节:
- URI 是字符序列.
- 字符会编码为八位字节以便传输/存储.
- UTF-8 是推荐的字符编码.
国际化资源标识符 (Internationalized Resource Identifiers, IRI)
IRI 扩展: RFC 3987 定义了 IRI, 它允许使用 Unicode 字符.
转换:
IRI: http://例え.jp/引き出し
↓ Encode to UTF-8 and percent-encode
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 Characters
├── Unreserved (unreserved)
│ ├── ALPHA: A-Z, a-z
│ ├── DIGIT: 0-9
│ └── Symbols: - . _ ~
│
├── Reserved (reserved)
│ ├── General Delimiters (gen-delims): : / ? # [ ] @
│ └── Sub-Delimiters (sub-delims): ! $ & ' ( ) * + , ; =
│
└── Percent-Encoded (pct-encoded): %HEXDIG HEXDIG
编码决策树
Character needs to appear in URI?
├─ Is unreserved character? → Use directly
├─ Is reserved character?
│ ├─ Used as delimiter? → Use directly
│ └─ Used as data? → Percent-encode
└─ Other character? → Percent-encode
常见字符编码表
| 字符 | 用途 | 编码 |
|---|---|---|
| Space | 分隔 | %20 或 + (在 query 中) |
| ! | 子定界符 | %21 (需要编码时) |
| " | 引号 | %22 |
| # | 片段定界符 | %23 (作为数据时) |
| $ | 子定界符 | %24 (需要编码时) |
| % | 编码标记 | %25 |
| & | 参数分隔符 | %26 (作为数据时) |
| ' | 子定界符 | %27 (需要编码时) |
| ( ) | 子定界符 | %28 %29 |
| + | 空格/子定界符 | %2B (作为数据时) |
| , | 列表分隔符 | %2C (需要编码时) |
| / | 路径分隔符 | %2F (作为数据时) |
| : | 方案分隔符 | %3A (作为数据时) |
| ; | 参数分隔符 | %3B (需要编码时) |
| = | 键值分隔符 | %3D (需要编码时) |
| ? | 查询分隔符 | %3F (作为数据时) |
| @ | 用户信息分隔符 | %40 (作为数据时) |
| [ ] | IPv6 边界 | %5B %5D |
实现建议 (Implementation Recommendations)
编码实现
def percent_encode(text, safe=''):
"""Percent-encode text"""
result = []
for char in text:
if char in safe or is_unreserved(char):
result.append(char)
else:
# UTF-8 encode and percent-encode
for byte in char.encode('utf-8'):
result.append(f'%\{byte:02X}')
return ''.join(result)
def is_unreserved(char):
"""Check if character is unreserved"""
return (char.isalnum() or
char in '-._~')
解码实现
def percent_decode(text):
"""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 的结构组件