- 为什么需要 Unicode? (ASCII 的局限性)
- Unicode 是什么? (核心概念)
- Python 3 中的 Unicode 实现:
str和bytes - 编码与解码:
encode()和decode() - 常见问题与最佳实践
- 实战案例
为什么需要 Unicode?(ASCII 的局限性)
在 Unicode 出现之前,计算机世界充满了各种字符编码,ASCII、ISO-8859-1 (Latin-1)、GBK、Big5 等。

- ASCII (American Standard Code for Information Interchange): 使用 1 个字节(8位)来表示字符,只能表示 128 个字符(英文字母、数字、标点符号),扩展的 ASCII 使用 7 位,也只能表示 128 个。
- 问题: 当你试图用 ASCII 显示中文 "你好" 时,会发现它无法表示,如果用 GBK 编码保存的文本文件,用 ASCII 编码器去读,就会得到一堆乱码。
核心痛点:不同的国家和地区制定了各自的编码标准,导致同一个字符在不同编码下可能有不同的二进制表示,这使得跨平台、跨语言的文本交换变得极其困难和混乱,在简体中文系统上正常显示的文件,拿到日文系统上就可能变成乱码。
Unicode 的诞生就是为了解决这个问题,它旨在为世界上所有的字符(无论是什么语言、什么符号)都分配一个唯一的数字,即码点。
Unicode 是什么?
Unicode 的核心思想是建立一个字符集,它是一个字符到码点的巨大映射表。
-
码点: 每个字符在 Unicode 表中都有一个唯一的、无歧义的数字标识,这个数字就是码点。
(图片来源网络,侵删)- 字母
A的码点是U+0041。 - 汉字 "中" 的码点是
U+4E2D。 - 表情符号 "😂" 的码点是
U+1F602。 U+表示这是一个 Unicode 码点,后面的十六进制数就是它的编号。
- 字母
-
Unicode 编码格式: 仅仅定义了码点还不够,计算机需要一种方式将这些码点转换成二进制数据进行存储和传输,这就引出了具体的编码格式,最常见的 Unicode 编码格式有:
-
UTF-8 (8-bit Unicode Transformation Format):
- 最流行、最通用的编码格式。
- 变长编码:它使用 1 到 4 个字节来表示一个字符。
- ASCII 兼容:对于 ASCII 字符(码点 < 128),UTF-8 使用 1 个字节表示,且与 ASCII 完全相同,这使得 UTF-8 成为处理英文文本时非常高效的选择。
- 无 BOM (Byte Order Mark):通常不需要 BOM,使得它在处理纯文本文件时更简洁。
-
UTF-16 (16-bit Unicode Transformation Format):
- 使用 2 或 4 个字节来表示一个字符。
- 在 Windows 内核、Java、.NET 等平台内部广泛使用。
- 对于许多亚洲语言字符(如中文、日文),UTF-16 通常比 UTF-8 更紧凑(都使用 2 个字节)。
-
UTF-32 (32-bit Unicode Transformation Format):
(图片来源网络,侵删)- 定长编码:每个字符都严格使用 4 个字节。
- 优点是计算和随机访问字符非常方便。
- 缺点是空间浪费严重(对于英文和拉丁文来说,UTF-8 比 UTF-32 节省 75% 的空间),因此很少用于存储和传输。
-
Unicode 是标准(字符表),UTF-8/UTF-16 是实现这个标准的编码方式(如何将字符表中的数字存进计算机)。
Python 3 中的 Unicode 实现:str 和 bytes
Python 3 在设计上对 Unicode 做了非常清晰和优雅的区分,这是它与 Python 2 最大的不同之一。
-
str类型:- Unicode 字符串:这是 Python 3 中表示文本的默认类型。
- 它是抽象的,存储的是字符的码点,而不是具体的字节序列。
- 你可以在代码中直接使用 Unicode 字符,
s = "你好,世界!😊",Python 解释器会正确地识别这些字符。 len()函数作用于str对象时,返回的是字符的数量,而不是字节数。s = "Hello 你好 😊" print(len(s)) # 输出 8 (H, e, l, l, o, 你, 好, 😊)
-
bytes类型:-
字节序列:这是一个只包含原始字节(0-255 的整数)的序列。
-
它是具体的,是计算机实际存储和传输数据的形式。
-
bytes对象是不可变的,它的行为很像一个只包含 ASCII 字符的str。 -
len()函数作用于bytes对象时,返回的是字节的数量。b = b'Hello' # b 前缀表示这是一个 bytes 对象 print(len(b)) # 输出 5 # 错误示范:bytes 不能直接包含非 ASCII 字符 # b = b'你好' # 会引发 SyntaxError
-
核心思想:在 Python 3 中,str 是“道”,是抽象的文本;bytes 是“器”,是具体的二进制数据,两者之间的转换就是编码和解码的过程。
编码与解码:encode() 和 decode()
这是处理文本和二进制数据时最关键的两个操作。
-
编码: 将
str(Unicode 字符串) 转换为bytes(字节序列)。- 使用
str.encode(encoding)方法。 - 你需要指定一个编码格式,最常用的是
'utf-8'。
- 使用
-
解码: 将
bytes(字节序列) 转换为str(Unicode 字符串)。- 使用
bytes.decode(encoding)方法。 - 同样需要指定正确的编码格式,否则就会得到乱码。
- 使用
图示:
str (Unicode) <--(decode)-- bytes (二进制数据) --(encode)--> str (Unicode)
示例:
# 1. 定义一个 Unicode 字符串
my_string = "你好,Python! 😊"
print(f"原始字符串 (str): {my_string}")
print(f"类型: {type(my_string)}")
print(f"字符长度: {len(my_string)}") # 8 个字符
# 2. 编码:将 str 转换为 bytes
# 使用最常用的 UTF-8 编码
my_bytes_utf8 = my_string.encode('utf-8')
print(f"\n编码后的字节序列 (bytes): {my_bytes_utf8}")
print(f"类型: {type(my_bytes_utf8)}")
print(f"字节长度: {len(my_bytes_utf8)}") # 15 个字节 (中文字符通常占 3 字节,emoji 占 4 字节)
# 3. 解码:将 bytes 转换回 str
# 必须使用和编码时相同的编码格式
decoded_string = my_bytes_utf8.decode('utf-8')
print(f"\n解码后的字符串: {decoded_string}")
print(f"类型: {type(decoded_string)}")
print(f"解码前后是否相等: {my_string == decoded_string}")
# 4. 错误示范:使用错误的编码解码
# 假设我们错误地用 'latin-1' (ISO-8859-1) 去解码一个 UTF-8 编码的字符串
try:
wrong_decoded = my_bytes_utf8.decode('latin-1')
print(f"\n错误解码结果: {wrong_decoded}") # 你会得到一堆奇怪的字符
except UnicodeDecodeError as e:
print(f"\n使用错误的编码解码会引发错误: {e}")
# 5. 另一个常见错误:对已经是 bytes 的对象进行编码
try:
my_bytes_utf8.encode('utf-8')
except AttributeError as e:
print(f"\n对 bytes 对象编码会引发错误: {e}")
常见问题与最佳实践
问题 1:UnicodeEncodeError 和 UnicodeDecodeError
这是最常遇到的两个错误。
-
UnicodeEncodeError:当你尝试将一个str编码成bytes时,如果字符串中包含目标编码格式无法表示的字符,就会报这个错。- 经典场景:尝试用
ASCII编码一个中文字符串。chinese_str = "中文" chinese_str.encode('ascii') # 会引发 UnicodeEncodeError: 'ascii' codec can't encode characters... # 解决方案:使用一个能表示中文的编码,如 'utf-8' chinese_str.encode('utf-8') # 正常执行
- 经典场景:尝试用
-
UnicodeDecodeError:当你尝试将一个bytes解码成str时,如果字节序列不符合指定的编码格式,就会报这个错。- 经典场景:用错误的编码解码文件,比如一个文件是 UTF-8 编码的,但你用
gbk去读。# 假设 my_bytes_utf8 是上面例子中 "你好" 的 UTF-8 编码结果 my_bytes_utf8.decode('gbk') # 可能会引发 UnicodeDecodeError
- 经典场景:用错误的编码解码文件,比如一个文件是 UTF-8 编码的,但你用
问题 2:文件读写 (open 函数)
在 Python 3 中,open() 函数默认使用系统平台的默认编码来读写文本文件,这在不同操作系统上可能导致不一致的行为。
- 最佳实践:始终显式地指定
encoding参数,强烈推荐使用'utf-8'。
# 写入文件
my_data = "这是一个测试文件。"
# 错误做法:依赖系统默认编码
# with open('test.txt', 'w') as f:
# f.write(my_data)
# 正确做法:显式指定 UTF-8 编码
with open('test.txt', 'w', encoding='utf-8') as f:
f.write(my_data)
# 读取文件
# 错误做法:依赖系统默认编码
# with open('test.txt', 'r') as f:
# content = f.read()
# 正确做法:显式指定 UTF-8 编码
with open('test.txt', 'r', encoding='utf-8') as f:
content = f.read()
print(f"读取到的内容: {content}")
问题 3:网络请求
当你从网络(如 API)获取数据时,收到的通常是 bytes 类型,你需要根据响应头中的 Content-Type 信息,使用正确的编码将其解码为 str。
import requests
response = requests.get('https://example.com/api/data')
# response.content 是 bytes 类型
# response.text 是 str 类型 (requests 会尝试自动解码)
# response.encoding 可以查看或设置编码
# 手动处理更可靠
if response.encoding is None:
# 尝试从内容中猜测编码,或使用一个安全的默认值
response.encoding = 'utf-8' # 或者更智能的 chardet 库
data_str = response.text # 这已经是解码后的字符串了
实战案例:处理一个乱码的 CSV 文件
假设你有一个 data.csv 文件,内容是用 GBK 编码保存的,但你用错误的编码(UTF-8)去读,得到了乱码。
data.csv (GBK 编码):
姓名,年龄
张三,25
李四,30
错误的读取方式:
# 会引发 UnicodeDecodeError 或得到乱码
with open('data.csv', 'r', encoding='utf-8') as f:
print(f.read())
输出 (可能是乱码或报错):
����,����
����,25
����,30
正确的读取方式:
# 使用正确的编码 'gbk' (或 'gb2312')
with open('data.csv', 'r', encoding='gbk') as f:
content = f.read()
print(content)
输出:
姓名,年龄
张三,25
李四,30
| 概念 | Python 3 中的实现 | 核心思想 |
|---|---|---|
| Unicode 字符串 | str 类型 |
抽象的文本,由字符(码点)组成,是程序内部处理文本的形式。 |
| 字节序列 | bytes 类型 |
具体的二进制数据,是计算机存储和传输数据的形式。 |
| 编码 | str.encode(encoding) |
将抽象的 str 转换为具体的 bytes,用于写入文件或网络传输。 |
| 解码 | bytes.decode(encoding) |
将具体的 bytes 转换为抽象的 str,用于读取文件或网络数据。 |
| 文件操作 | open(..., encoding='utf-8') |
最佳实践:始终为文本文件显式指定编码,推荐 utf-8。 |
核心心法:
- 在 Python 程序内部,统一使用
str类型来处理所有文本。 - 只在与外部世界交互时(读写文件、网络请求、数据库操作),才需要将
str编码为bytes或将bytes解码为str。 - 在编码和解码时,务必确保使用正确的、一致的编码格式(首选
utf-8)。
掌握了这些概念和操作,你就能在 Python 中游刃有余地处理各种文本和二进制数据了。
