跳到主要内容

2. 字符 (Characters)

本章定义 URI 中使用的字符集, 编码机制和处理规则.


字符编码基础 (Character Encoding Fundamentals)

URI 语法提供了一种将数据编码为字符序列的方法, 通常用于标识资源.

编码层次:

Resource → URI Characters → Octets → Transmission/Storage

字符集: URI 基于 US-ASCII 字符集, 该字符集由数字, 字母和少量图形符号组成.


2.1. 百分号编码 (Percent-Encoding)

目的

当某个数据八位字节在组件中对应的字符不属于允许集合, 或该字符正被用作定界符时, 使用百分号编码机制来表示该数据八位字节.

编码格式

pct-encoded = "%" HEXDIG HEXDIG

格式: 百分号编码由百分号字符 "%" 后跟两个十六进制数字组成, 这两个数字表示该八位字节的数值.

示例

字符二进制十六进制百分号编码
Space001000000x20%20
!001000010x21%21
#001000110x23%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 生成者:

  1. 生成 URI 时, 必须对不允许的字符进行百分号编码.
  2. 保留字符只有在用作定界符时才保持未编码.
  3. 非保留字符不应编码.

示例:

# 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 使用者:

  1. 解析 URI 之后, 按需要解码组件.
  2. 不要过早解码 (可能改变 URI 结构).
  3. 每个组件只解码一次.

危险示例:

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 生成:

  1. 使用 UTF-8 编码非 ASCII 字符.
  2. 对得到的八位字节进行百分号编码.
  3. 使用大写十六进制数字.
  4. 不编码非保留字符.

URI 使用:

  1. 按组件解析.
  2. 解码百分号编码.
  3. 使用 UTF-8 解释八位字节.
  4. 处理无效编码.

字符集快速参考

完整字符分类

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 的结构组件