Skip to main content

2. Characters (字符)

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


字符编码基础

URI语法提供了一种将数据(可能用于标识资源)编码为字符序列的方法。

编码层次:

资源 → URI字符 → 八位字节 → 传输/存储

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


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

目的

当八位字节对应的字符不在允许集中或被用作分隔符时,使用百分号编码机制来表示组件中的数据八位字节。

编码格式

pct-encoded = "%" HEXDIG HEXDIG

格式: 百分号字符 % 后跟表示该八位字节数值的两个十六进制数字

示例

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

  1. 生成URI时,必须对不允许的字符进行百分号编码
  2. 保留字符仅在作为分隔符时才不编码
  3. 非保留字符不应编码

示例:

# 编码路径
path = "/files/my document.pdf"
encoded = "/files/my%20document.pdf"

# 编码查询
query = "?name=John Doe&age=30"
encoded = "?name=John%20Doe&age=30"

解码时机

URI消费者:

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

危险示例:

原始: /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生产:

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

URI消费:

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

字符集快速参考

完整字符分类

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