Logo
Published on

2.3.编码

Authors
  • avatar
    Name
    xiaobai
    Twitter

1.计算机如何表示文字?

计算机底层只能处理和存储二进制数(0和1)。那么,我们看到的字母、汉字、表情符号等,是如何被计算机存储和显示的呢?

答案就是:编码。 编码就是一套映射规则,它规定了每个字符对应哪个二进制数字。

2.一个简单的例子:数字编码字母

为了更好地理解,我们可以用"数字编码字母"来举例:

假设我们规定:

  • 字母 A 用数字 1 表示
  • 字母 B 用数字 2 表示
  • 字母 C 用数字 3 表示

在这种规则下,1 就是 A 的"编码"。只要发送方和接收方都知道这套规则(即"编码表"),看到数字 1 就能明白它代表字母 A,从而顺利交流信息。

计算机编码的本质与此完全相同。

3.编码的发展历程

3.1.阶段一:ASCII码 - 英文字符的专属编码

早期计算机主要在英语国家使用,所以诞生了ASCII(美国信息交换标准代码)

text = "Hello"
encoded = text.encode('ascii')
print(encoded)  # b'Hello'
print(encoded.hex())  # 48656c6c6f
print(''.join(f'\\x{b:02x}' for b in encoded))  # \x48\x65\x6c\x6c\x6f
print(encoded.decode('ascii')) # Hello

字节串中ASCII字符直接显示为字符

格式说明{b:02x} 可以分解为:

  • b - 要格式化的变量
  • : - 格式说明符开始
  • 0 - 填充字符
  • 2 - 最小宽度
  • x - 格式类型(十六进制小写)

ASCII的特点

  • 只有128个字符(英文字母、数字、标点)
  • 每个字符1个字节
  • 使用7位二进制数(后来扩展为8位),共可以表示128(或256)个字符
  • 包含英文字母(大小写)、数字、标点符号以及一些控制字符(如换行、响铃)
  • 局限性:无法表示中文、日文、阿拉伯文等成千上万的字符
字符ASCII码(十进制)ASCII码(二进制)
A6501000001
a9701100001
04800110000

3.2.阶段二:本地化编码 - 各自为政的时代

为了表示各自的语言,各个国家和地区制定了不同的编码标准。

# 中文GB2312/GBK编码示例
text = "中国"
gbk_encoded = text.encode('gbk')
print(gbk_encoded)  # b'\xd6\xd0\xb9\xfa'

各国编码标准

  • 中国:GB2312 → GBK → GB18030
  • 台湾:Big5
  • 日本:Shift-JIS
  • 韩国:EUC-KR

问题来了:"乱码"的根源 如果你用一个编码(比如GBK)去保存文件,却用另一个编码(比如BIG5)去打开它,就会显示一堆乱七八糟的字符,这就是"乱码"。因为同一串二进制数字在不同编码规则下对应着不同的字符。

结果:乱码问题严重,各国之间无法正常交流文本信息!

3.3.阶段三:Unicode - 世界大同的字符集

为了解决混乱,Unicode应运而生。它的目标很宏大:为世界上所有语言的所有字符,都赋予一个全球唯一的编号(这个编号叫做"码点")。

Unicode字符集演示

# Unicode 字符集包含全球所有字符
characters = [
    'A',        # 拉丁字母
    '中',       # 中文
    '🍕',       # Emoji
    'α',        # 希腊字母
    'あ'        # 日文
]

for char in characters:
    print(f"字符: {char}, Unicode码点: U+{ord(char):04X}")

输出:

字符: A, Unicode码点: U+0041
字符:, Unicode码点: U+4E2D
字符: 🍕, Unicode码点: U+1F355
字符: α, Unicode码点: U+03B1
字符:, Unicode码点: U+3042

格式说明{ord(char):04X} 可以分解为:

  • ord(char) - 获取字符的Unicode码点
  • : - 格式说明符开始
  • 0 - 填充字符
  • 4 - 最小宽度
  • X - 格式类型(十六进制大写)

Unicode的特点

  • 只定义字符和编号的对应关系,不关心这个编号在计算机中如何存储
  • 例如:
    • 字符 A 的Unicode码点是 U+0041(和ASCII的65对应)
    • 汉字 的Unicode码点是 U+4F60
    • 笑脸 😊 的Unicode码点是 U+1F60A

3.4.阶段四:Unicode编码方案 —— 如何将Unicode存储为二进制

Unicode 只负责为每个字符分配一个唯一的“码点”(可以理解为身份证号),但并不规定这些码点在计算机中具体如何存储。要让字符真正变成可以存储和传输的二进制数据,还需要编码方案(如 UTF-8、UTF-16 等)来规定“码点”如何转换为字节序列。

简而言之:Unicode 解决了“每个字符的编号是多少”,而编码方案解决了“如何把编号变成计算机能存储的字节”。

在 Unicode 出现之前,通常是直接将编号存入计算机,但随着字符数量的激增,这种方式会极大浪费存储空间。因此,Unicode 设计了多种编码方案,以便在不同场景下高效地存储和传输字符。

3.4.1.UTF-8(最常用)

text = "Hello 世界 🍕"
utf8_encoded = text.encode('utf-8')
print(f"UTF-8编码: {utf8_encoded}")
print(f"长度: {len(utf8_encoded)} 字节")

# 分析每个字符的UTF-8编码
for char in text:
    encoded_char = char.encode('utf-8')
    print(f"'{char}' -> {encoded_char} ({list(encoded_char)})")

3.4.2.UTF-16

text = "Hello 世界 🍕"
utf16_encoded = text.encode('utf-16')
print(f"UTF-16编码: {utf16_encoded}")
print(f"长度: {len(utf16_encoded)} 字节")

3.4.3.UTF-32

text = "Hello 世界 🍕"
utf32_encoded = text.encode('utf-32')
print(f"UTF-32编码: {utf32_encoded}")
print(f"长度: {len(utf32_encoded)} 字节")

UTF-8的特点

  1. 可变长度:一个字符可以用1到4个字节表示
  2. 兼容ASCII:所有ASCII字符的UTF-8编码和其ASCII编码完全相同,这意味着一个纯ASCII文件也是一个合法的UTF-8文件
  3. 高效:对于英文网站,UTF-8比固定使用2或4字节的UTF-16/UTF-32更节省空间

正因为这些优点,UTF-8成为了互联网和现代软件的默认编码标准。

4.编码方案比较

编码方案英文字母中文字符Emoji特点
UTF-81字节3字节4字节兼容ASCII,网络标准
UTF-162字节2字节4字节Java、Windows内部使用
UTF-324字节4字节4字节定长,简单但浪费空间

所以,一个字符的完整生命周期是: 字符 → Unicode码点(抽象编号) → UTF-8编码(具体字节)

以汉字"你"为例:

  1. 字符是 在Unicode花名册里,它的码点U+4F60通过UTF-8编码规则,这个码点被转换成3个字节的二进制数据:\xE4\xBD\xA0(这是十六进制表示)

5.编程中的实践

在编程时,你会遇到两种基本类型:

  • str(字符串):在内存中表示的文本,可以看作是Unicode字符的序列。它是人类可读的。

    s = "Hello 世界"
    
  • bytes(字节串)二进制数据,是字符经过编码后的实际存储形式。它以 b'...' 的形式显示。

    b = b'Hello \xe4\xb8\x96\xe7\x95\x8c'
    

它们之间的转换就是编码解码

  • 编码:strbytes 将人类可读的文本转换为用于存储/传输的二进制数据。

    print("你".encode('utf-8')) # 得到 b'\xe4\xbd\xa0'
    
  • 解码:bytesstr 将接收到的二进制数据转换回人类可读的文本。

    print(b'\xe4\xbd\xa0'.decode('utf-8')) # 得到 "你"
    

关键:解码时使用的编码必须与编码时使用的编码一致,否则就会产生乱码

6.总结

6.1.大纲

  1. 字符集:定义"有什么字符"以及它们的码点是多少(Unicode是现代标准)
  2. 编码:定义"如何存储字符"(UTF-8是最佳实践)
  3. 关系:一个字符集可以有多种编码方案
  4. 黄金法则:用什么编码存储,就用什么编码读取

6.2.现代编程建议

在现代编程中,推荐始终使用:

  • 字符集:Unicode
  • 编码:UTF-8

这样可以避免大多数字符编码问题!

6.3.记忆口诀

记住这个简单的流程,你就能驾驭绝大多数编码问题: 输入字节流 --(解码)--> 程序内文本 --(编码)--> 输出字节流

img