☰
Python字符集和字符编码详情
2026/10/13 3:35:50 网站建设 项目流程

前言


「字符集」和「字符编码」这两个词被混用得太久了,久到很多人以为它们是一回事。实际上它们是两层:字符集管的是一张「字符到编号」的表,编码管的是「编号到字节」的转换规则。UTF-8 和 UTF-16 用的是同一个字符集,却产生完全不同的字节序列,就是因为它们处在第二层。


搞不清这两层,遇到乱码时就只能瞎试编码,试到对为止。搞清楚之后,你会知道乱码几乎是在某个环节用错了编码,而定位这个环节是有方法的。


本文讲清五件事:字符集与编码的分工、码位与字节序列的区别、ASCII / GBK / Unicode 三者的关系、UTF-8 为什么是变长的,以及 Python 3 里字符串与字节串的分界。文末特别给一张 Python 2 与 Python 3 的对照,因为编码是两代 Python 差异最大的地方。本机无 Python 解释器,代码为逐行推演。


一、字符集与编码:两层,别混



  • 字符集(character set):一个字符集合,加上「每个字符对应哪个编号」的映射。这个编号叫码位(code point)。

  • 字符编码(character encoding):把码位翻译成实际字节序列的规则。


用官方 Unicode HOWTO 的例子:字符「♞」(BLACK CHESS KNIGHT)的码位是 U+265E,也就是十进制 9822。这个 9822 是码位,不是字节。至于它落到文件里是几个字节、每个字节是什么,由编码决定——UTF-8 下是三个字节,UTF-16 下是另外两个字节。同一个码位,不同编码,不同字节。




概念回答的问题例子



字符集有哪些字符?各自编号多少?Unicode:U+265E 是「♞」

编码编号怎么写成一串字节?UTF-8:E2 99 9E

字形(glyph)屏幕上长什么样?由字体决定



注意最后一行:码位不等于字形。同一个码位在不同字体里长得可能完全不同,但它是同一个字符。反过来,有些字符看起来一样却不是同一个码位(后面的文章会讲这个陷阱)。


二、码位、码元与字节序列


这三个词经常连着出现,一层层往下:



  1. 码位(code point):字符的编号,Unicode 里范围是0到0x10FFFF(十进制 1,114,111)。

  2. 码元(code unit):编码把码位切成的基本单位。UTF-8 的码元是 8 位,UTF-16 的码元是 16 位,UTF-32 的码元是 32 位。

  3. 字节序列:码元最终写进文件/网络时的那串字节。


关键理解:「一个字符占几个字节」这个问题,离开编码就没法回答。同一个「中」字,UTF-8 里 3 字节,GBK 里 2 字节,UTF-32 里 4 字节。


Python 里可以用两个内置函数观察码位:


# 适用于 Python 3.8+
print(ord("A")) # 65 字符 -> 码位
print(chr(65)) # 'A' 码位 -> 字符
print(hex(ord("♞"))) # 0x265e 和 HOWTO 里的例子一致

ord()接受一个长度为 1 的字符串,返回它的码位整数;chr()反过来。这两个函数操作的是码位层面,完全不涉及字节。


三、ASCII、GBK、Unicode 的关系


它们不是同一层的替代品,历史关系是「后一层包含并取代前一层」:




名称类型覆盖范围与字节的关系



ASCII字符集 + 编码合一128 个字符1 字符 = 1 字节

Latin-1(ISO-8859-1)字符集 + 编码合一256 个字符1 字符 = 1 字节

GB2312 / GBK / GB18030中文编码简中字符,GBK 是 GB2312 的扩展,GB18030 再扩大变长(中文多 2 字节)

Unicode字符集全球字符不含字节规则

UTF-8 / UTF-16 / UTF-32Unicode 的编码全部 Unicode 码位1~4 字节 / 2~4 / 固定 4



几个要点:



  • ASCII 既是字符集又是编码,因为它的字符和字节一一对应,两层合一。Latin-1(latin-1)同样如此,官方文档指出latin-1、iso_8859_1、8859是同一编码的不同名字。

  • GBK 是中文编码,它是 GB2312 的扩展,字节层面和 Unicode 无关。GB18030 是更全的后继标准。一个 GBK 字节流用 UTF-8 去解,必然乱码或报错,反过来也一样。

  • Unicode 是字符集,不是编码。常说的「存成 Unicode」是含糊说法,实际必须指定 UTF-8 或 UTF-16 之类的具体编码。

  • 只要一个字节流全是 ASCII,它在 UTF-8 下也是合法的。这是 UTF-8 的一个便利性质:ASCII 文本天然就是合法的 UTF-8 文本。


四、为什么 UTF-8 是变长的


固定宽度(比如直接用 4 字节表示每个码位)的问题很直观:英语文本里绝大多数字符的码位都小于 128,用 4 字节存一个 ASCII 字符,浪费极大。官方 HOWTO 里明确算过这笔账:同样的字符串,用 32 位定长表示要 24 字节,而 ASCII 只要 6 字节——磁盘和带宽是 4 倍浪费。


UTF-8 的规则(来自官方 HOWTO):



  • 码位小于 128 时,用一个字节表示,值与码位相同。

  • 码位大于等于 128 时,用两、三或四个字节表示。


这套设计带来几个便利:



  1. 能表示任意 Unicode 码位。

  2. ASCII 部分是合法的 UTF-8,老的 ASCII 数据和 UTF-8 天然兼容。

  3. 不会出现意外的零字节(U+0000除外),所以能安全穿过 C 字符串函数。

  4. 字节序无关,不像 UTF-16 那样需要 BOM(字节序标记)来区分大小端。


代价是:UTF-8 是变长的,所以「字符串长度」和「字节数」不是一回事。Python 3 的len(s)数的是码位数,而len(s.encode("utf-8"))数的是字节数:


# 适用于 Python 3.8+
s = "中文abc"
print(len(s)) # 5 5 个字符
print(len(s.encode("utf-8"))) # 11 3+3+1+1+1 个字节

一个额外细节:Python 3 的len()数的是码位,也不等于用户眼中的「字符数」。带组合符号的字符(如「é」写成e加一个组合重音符)算两个码位,但看起来是一个字符。这个问题留给 Unicode 文本处理的文章展开。


五、Python 3 里字符串与字节串的分界


Python 3 最基本的一条分界:str是文本(码位序列),bytes是字节(8 位序列)。两者之间必须显式转换:




方向方法说明



str→bytess.encode("utf-8")编码

bytes→strb.decode("utf-8")解码

编码名"utf-8"、"gbk"、"ascii"等大小写不敏感,-和_等价



# 适用于 Python 3.8+
text = "中文"
data = text.encode("utf-8") # bytes
print(data) # b'\xe4\xb8\xad\xe6\x96\x87'
back = data.decode("utf-8") # str
print(back == text) # True

b"\xe4\xb8\xad..."里的\xe4是一个字节的十六进制写法。注意bytes对象的每个元素是 0~255 的整数,data[0]得到的是整数228,不是字符。


5.1 Python 2 的对照(必看)


Python 2.7 已于 2020 年 1 月 1 日停止维护(PEP 373 里写明了这个日期)。编码正是两代 Python 差异最大的地方:




项目Python 2Python 3



str是什么字节串文本(码位序列)

文本类型unicodestr

字节类型strbytes

u"..."前缀必需(否则是字节串)无意义(str已是文本)

默认编码ASCII,隐式ascii编解码UTF-8

源码默认编码ASCII,需# -*- coding: utf-8 -*-UTF-8(PEP 3120)



Python 2 里最坑的是隐式编解码:当字节串和unicode混用运算时,解释器会偷偷用 ASCII 去解码字节串,一旦遇到非 ASCII 字节就抛UnicodeDecodeError。Python 3 取消了这种隐式转换,逼你显式encode/decode,虽然啰嗦,但把乱码问题从「运行时随机炸」变成了「写代码时就得想清楚」。


常见坑点



  1. 把 Unicode 当成一种编码


❌ 「这个文件是 Unicode 格式的」,然后试图decode("unicode")。


✅ Unicode 是字符集,必须指明utf-8/utf-16之类的具体编码。



  1. 混用len()和字节数


❌ 用len(s)去算网络包该申请多大缓冲区,结果中文被截半。


✅ 字节相关问题一律用len(s.encode(...))。



  1. 用错编码硬解字节流


❌ GBK 的字节流拿去decode("utf-8"),报UnicodeDecodeError。


✅ 先确定字节流真实编码,再用对应编码解码。



  1. 默认编码不一致导致跨平台差异


❌open("f.txt")不写encoding,在 Windows 上按 GBK、在 Linux 上按 UTF-8,结果不同。


✅ 始终显式写open("f.txt", encoding="utf-8")。



  1. 以为 ASCII 能存中文


❌"中".encode("ascii")直接抛UnicodeEncodeError。


✅ 需要存中文就用utf-8,不要用ascii。



  1. 照抄 Python 2 的写法


❌ 写u"中文"或依赖隐式编解码。


✅ Python 3 里str已是文本,不需要u前缀;编解码必须显式。



  1. UTF-16 忽略字节序


❌ 用utf-16编出来的字节流,被另一端按固定大小端解读,出现「�」。


✅ 需要跨系统交换时用utf-16(带 BOM)或明确utf-16-le/utf-16-be。


总结




概念一句话



字符集字符到码位的表,如 Unicode

编码码位到字节的规则,如 UTF-8

码位0 ~ 0x10FFFF 的整数

UTF-8变长,ASCII 兼容,字节序无关

Python 3str是文本,bytes是字节,转换必须显式

Python 2str是字节串,unicode才是文本,已停止维护



字符集和编码分属两层,把它们分开看,乱码就不再玄学:不是「编码错了」,而是「在某个环节用了另一层该用的东西」。UTF-8 之所以成为事实标准,是因为它用变长换来了 ASCII 兼容和字节序无关;代价是长度概念要随时分清是码位还是字节。这两点想明白,后面的编码问题排查会顺很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询