
字节对象、字符编码解码
更新: 2025/2/24 字数: 0 字 时长: 0 分钟
本章节会涉及到处理字符编码相关的内容,因此在开始学习本节内容之前,最好先去看一下 Basic 部分的《如何显示字符》章节中的内容,再回来学习效果会比较好。
字节对象
比特 bit
计算机是由各种电子元件组成的,电子元件可以表现出高低不同的电压状态,从工程设计和元件制作方面来看,电子元件只表现出“高电平”或“低电平”两种稳定状态是最容易实现的。因此计算机内部加工、处理的数据和指令也就自然而然的选择了只有"0"或"1"两种数字的二进制码,每一个"0"或"1"的二进制码,我们称之为一个比特(bit)。
提醒
虽然电子元件以高电压和低电压来表示不同的状态,但这并不是唯一的方式。例如,存储器单元可以采用不同电荷的积累状态来表示不同的值,而不仅仅是电压。
字节 byte
在计算机中,虽然加工处理的数据和指令都是"0101"形式的二进制码,但在存储和传输方面二进制数据通常是以字节(byte)为基本单位进行的。例如,当我们将一个文件保存到硬盘或从网络上下载一个文件时,文件的大小通常是以字节为单位来衡量的,而数据的传输也是以字节流的形式进行的。一个字节由8个比特(bit)组成,它是计算机存储和处理数据的最小单元,也是最小的可寻址的存储单元。在 Python 中,二进制数据通常是以字节(byte)形式呈现的,为了表示字节类型数据就在最前面加一个字符 b
来表示这是一个字节对象。
- 用八进制表示一个包含三个字节的数据:每个
\
后面的三个字符表示一个字节,八进制的 141、142 和 143 分别对应十进制的 97、98 和 99,也就分别对应 ASCII 中的a
、b
和c
。
bin_data = b'\141\142\143'
print(type(bin_data)) # 输出:<class 'bytes'>。注释:这是一个字节对象。
print(bin_data) # 输出:b'abc'。注释:自动将八进制字节数据转换为字符字节数据。
- 用十六进制表示一个包含三个字节的数据:每个
\x
后面的两个字符表示一个字节,十六进制的 61、62 和 63 分别对应十进制的 97、98 和 99,也就分别对应 ASCII 中的a
、b
和c
。
bin_data = b'\x61\x62\x63'
print(type(bin_data)) # 输出:<class 'bytes'>。注释:这是一个字节对象。
print(bin_data) # 输出:b'abc'。注释:自动将十六进制字节数据转换为字符字节数据。
字符编码解码
Unicode
Unicode 编号
Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符集,它为世界上大部分书写系统中的字符分配了唯一的数字编号。通过下面两种方法我们可以在字符和数字编号之间相互转换:
ord(单个字符)
返回单个字符在 Unicode 字符集中对应的数字编号。
n = ord('a')
print(n) # 输出:97
n = ord('b')
print(n) # 输出:98
n = ord('c')
print(n) # 输出:99
chr(整型Unicode编号)
返回 Unicode 字符集中数字编号对应的单个字符。
s = chr(97)
print(s) # 输出:a
s = chr(98)
print(s) # 输出:b
s = chr(99)
print(s) # 输出:c
Unicode 使用 17 个平面编码空间来容纳世界上所有文字和符号,每个平面编码空间的大小为 256 行 256 列,总计可以容纳下 17 X 256 X 256 = 1114112 个字符,对应的数字编号范围就是 0 ~ 1114111,对应的十六进制数字编号范围就是 0 ~ 0x10ffff。下面是 chr
内置函数注释:
截至 2023 年,Unicode 的最新版本是 Unicode 15.0,它包含了超过 150000 个字符,这些字符覆盖了多种语言、符号、表情符号(如 Emoji)等。具体看下面案例:
# Emoji表情符号
smiley = '😄'
print(ord(smiley)) # 输出:128516
# number数字编号
number = 128516
print(chr(number)) # 输出:😄
Unicode 字符
在 Python 中表示 Unicode 字符通常是以 \u(小写字母)
开头加四位十六进制的数字编号组成。例如,字符 a
在 Unicode 字符集的数字编号是 97,那么它对应的 Unicode 字符就是 \u0061
。
# Python会自动将Unicode字符转换为字符
print('\u0061', ord('\u0061')) # 输出:a 97
print('\u0062', ord('\u0062')) # 输出:b 98
print('\u0063', ord('\u0063')) # 输出:c 99
print('\u0061\u0062\u0063') # 输出:abc
在 Unicode 字符集中,常用汉字(并非所有的汉字)对应的数字编号范围是 19968 ~ 40959,转换为 Unicode 字符就是 \u4e00 ~ \u9fff
,其中排位第一的常用汉字为“一”,它的 Unicode 字符就是 \u4e00
,而排位倒数第一的常用汉字为“鿿”,它的 Unicode 字符就是 \u9fff
。代码案例如下:
print('\u4e00') # 输出:一。注释:汉字'一'的Unicode字符是\u4e00,是常用汉字在Unicode码排位第一的汉字。
print('\u9fff') # 输出:鿿。注释:汉字'鿿'的Unicode字符是\u9fff,是常用汉字在Unicode码排位倒第一的汉字。
for i in range(ord('\u4e00'), ord('\u9fff') + 1):
print(chr(i), end='') # 输出:一丁丂七丄...。注释:输出Unicode字符集中常用的汉字字符。
重要
在 Python 中,Unicode 字符和其对应的字符是等价的。例如,print('\u0063' == 'c')
返回的结果是 True
,print('\u4e00' == '一')
返回的结果也是 True
。
介绍到这里,有小伙伴会发现一个问题,Unicode 字符中只有四位十六进制的数字编号,也就是说它能表示的范围是 0 ~ 0xffff,对应数字范围就是 0 ~ 65535,而上面的 😄 Emoji 表情符号的数字编号为 128516,不在 Unicode 字符对应的数字范围内,是否就无法展示?回答这个问题之前,我们先回顾上面的内容。上面我们提到 Unicode 使用了 17 个平面编码空间来容纳全世界所有的文字和符号,而以 \u(小写字母)
开头加四位十六进制的数字编号组成的 Unicode 字符所表示的范围刚好就是 1 个平面编码空间的大小,具体表示的就是基本多语言平面(BMP, Basic Multilingual Plane),这个平面是 Unicode 的第一个平面,它里面包含了多个语种的基本字符,是我们最常使用的平面,而且仅用两个字节表示,所以倍受青睐。然而 😄 Emoji 表情符号并不在这个平面中,而是在补充多语言平面(SMP, Supplementary Multilingual Plane)中,因此我们无法使用 \u(小写字母)
开头加四位十六进制的数字编号组成的 Unicode 字符来展示它,但是可以使用我们 \U(大写字母)
开头加八位十六进制的数字编号组成的 Unicode 字符来展示它。上面的 😄 Emoji 表情符号就可以进行如下展示:
# 😄Emoji表情符号的数字编号为128516,十六进制为1F604
print("\U0001F604") # 输出:😄
在写 Unicode 字符的时候需要注意,下面这种写法只会输出原始的字符串,无法输出 😄 Emoji 表情符号。这是因为当你使用双反斜杠 \\
时,第一个反斜杠 \
是一个转义字符,用于表示接下来的字符应该被当作普通字符而不是转义字符。因此,\\U
在字符串中实际上会被解释为一个普通的 \
字符后跟一个 U
字符,而不是一个 Unicode 转义序列的开始。简单说,就是你不能直接在字符串字面量中使用 \U
后跟一个变量来得到 Unicode 字符,因为 Python 解释器在解析字符串时不会动态地将 \U
和后面的字符串解释为 Unicode 转义序列。
number = 128516
char = f'\\U{number:08X}'
print(char) # 输出:\U0001F604
总结一下,在 Python 字符串中,要表示一个 Unicode 字符,你需要使用 \u
(对于 BMP 中的字符)或 \U
(对于整个 Unicode 范围)后跟恰好四个或八个十六进制数字(不需要前缀的双反斜杠)。
提醒
额外补充,Python 中的 str
字符串使用的是 Unicode 编码,并且采用一种灵活的字符串表示,存储的字符长度取决于字符串中最大的 Unicode 码点。若字符串中全部是 ASCII 字符,则每个字符占用 1 字节;如果有字符超出了 ASCII 范围,但全部在基本多语言平面(BMP)内,则每个字符占用 2 字节;如果有超出 BMP 的字符,则每个字符占用 4 字节。
编码解码
Unicode 只是对世界上的所有字符进行了编号,并未指定了每个字符编号该如何映射为某串二进制码。而确定映射为某串二进制码的规则就是字符编码了,二进制码在 Python 中是以字节类型进行展示的。
内置方法
在 Python 中有四个用于编码解码的内置函数,它们可以将字符型数据与字节型数据进行转换:
字符类型.encode('编码')
将字符类型数据编码为字节类型数据(默认编码是UTF-8
,常用)。bytes(字符类型, encoding='编码')
将字符类型数据指定编码为字节类型数据。
# 经过UTF-8编码
print('123'.encode()) # 输出:b'123'。注释:数字字符串经过UTF-8编码后变为了数字字节类型。
print('aBc'.encode()) # 输出:b'aBc'。注释:大小写字母字符串经过UTF-8编码后变为了大小写字母字节类型。
print('我'.encode()) # 输出:b'\xe6\x88\x91'。注释:汉字字符串经过UTF-8编码后变为了三个十六进制字节类型。
print('😄'.encode()) # 输出:b'\xf0\x9f\x98\x84'。注释:Emoji表情符号经过UTF-8编码后变为了四个十六进制字节类型。
# 经过GBK编码
print('123'.encode('GBK')) # 输出:b'123'。注释:数字字符串经过GBK编码后变为了数字字节类型。
print('aBc'.encode('GBK')) # 输出:b'abc'。注释:大小写字母字符串经过GBK编码后变为了大小写字母字节类型。
print('我'.encode('GBK')) # 输出:b'\xce\xd2'。注释:汉字字符串经过GBK编码后变为了两个十六进制字节类型。
print('😄'.encode('GBK')) # 报错:GBK字符集中不包含Emoji表情符号,因此也就无法将其转换为GBK字节编码。
# 转换为Unicode字符
print('123'.encode('unicode_escape')) # 输出:b'123'。注释:数字字符串经过Unicode转换后变为了数字字节类型。
print('aBc'.encode('unicode_escape')) # 输出:b'aBc'。注释:大小写字母字符串经过Unicode转换后变为了大小写字母字节类型。
print('我'.encode('unicode_escape')) # 输出:b'\\u6211'。注释:汉字字符串经过Unicode转换后变为了Unicode字符。
print('😄'.encode('unicode_escape')) # 输出:b'\\U0001f604'。注释:Emoji表情符号经过Unicode转换后变为了Unicode字符。
字节类型.decode(解码)
将字节类型数据指定解码为字符类型数据(默认解码是UTF-8
,常用)。str(字节类型, encoding='编码')
将字节类型数据指定解码为字符类型数据。
# 经过UTF-8解码
print(b'123'.decode()) # 输出:123。注释:数字字节类型经过UTF-8解码后变为了数字字符串。
print(b'aBc'.decode()) # 输出:aBc。注释:大小写字母字节类型经过UTF-8解码后变为了大小写字母字符串。
print(b'\xe6\x88\x91'.decode()) # 输出:我。注释:十六进制字节类型经过UTF-8解码后变为了汉字字符串。
print(b'\xf0\x9f\x98\x84'.decode()) # 输出:😄。注释:十六进制字节类型经过UTF-8解码后变为了Emoji表情符号。
# 经过GBK解码
print(b'123'.decode('GBK')) # 输出:123。注释:数字字节类型经过GBK解码后变为了数字字符串。
print(b'aBc'.decode('GBK')) # 输出:aBc。注释:大小写字母字节类型经过GBK解码后变为了大小写字母字符串。
print(b'\xce\xd2'.decode('GBK')) # 输出:我。注释:十六进制字节类型经过GBK解码后变为了汉字字符串。
# 转换为字符类型
print(b'123'.decode('unicode_escape')) # 输出:123。注释:数字字节类型经过Unicode转换后变为了数字字符串。
print(b'aBc'.decode('unicode_escape')) # 输出:aBc。注释:大小写字母字节类型经过Unicode转换后变为了大小写字母字符串。
print(b'\\u6211'.decode('unicode_escape')) # 输出:我。注释:Unicode字符经过Unicode转换后变为了汉字字符串。
print(b'\\U0001f604'.decode('unicode_escape')) # 输出:😄。注释:Unicode字符经过Unicode转换后变为了Emoji表情符号。
重要
在上面的代码案例中,字符串中属于 ASCII 码中的字符(例如,数字、大小写字母)在经过 UTF-8 编码、GBK 编码、Unicode 字符转后均可以正常显示,不会被编码为 \编码数字
的形式。
字符转换
有些时候,我们会遇到内容是字节编码的字符类型数据,这里我们通过下面两步进行转换:
- 使用
latin-1
编码将字节编码的字符类型数据转换为字节类型数据。 - 选择对应的字符编码,对字节类型数据进行解码。
# UTF-8编码字符串
utf8_str = '\xe6\x88\x91'
utf8_bytes = utf8_str.encode('latin-1')
print(utf8_bytes) # 输出:b'\xe6\x88\x91'。注释:内容不变,类型改变。
utf8_con = utf8_bytes.decode()
print(utf8_con) # 输出:我。注释:选择字节类型数据对应的字符编码解码。
# GBK编码字符串
gbk_str = '\xce\xd2'
gbk_bytes = gbk_str.encode('latin-1')
print(gbk_bytes) # 输出:b'\xce\xd2'。注释:内容不变,类型改变。
gbk_con = gbk_bytes.decode('GBK')
print(gbk_con) # 输出:我。注释:选择字节类型数据对应的字符编码来解码。
# Unicode字符串
uni_str = '\\u6211'
uni_bytes = uni_str.encode('latin-1')
print(uni_bytes) # 输出:b'\\u6211'。注释:内容不变,类型改变。
uni_con = uni_bytes.decode('unicode_escape')
print(uni_con) # 输出:我。注释:选择字节类型数据对应的字符编码来解码。
建议
latin-1
编码又叫做”西欧语言“,它可以处理英文字符,但不能处理中文字符。这里之所以使用 latin-1
编码字符串,是因为通过该编码转换后的数据和原数据之间的内容不会发生改变,只是数据类型由字符类型转换为字节类型。
编码逆向
字节逆向
上面进行编码解码的时候,会发现Unicode 编码是很好区分的,因为它是以 \u
或 \U
为开头的,而 UTF-8 编码、GBK 编码就不那么好区分了,因为它们都是以 \x
为开头的。这时就需要用到 Python 中一个用于编码识别的 chardet
第三方库了,安装命令如下:
pip install chardet
现在我们给出一段内容为字节编码的字符串,通过 chardet
进行识别和解码,案例如下:
import chardet
# 未知编码
unknown_encode = '\xe5\xa5\xbd\xe5\xa5\xbd\xe5\x8a\xaa\xe5\x8a\x9b\xef\xbc\x81'
# 转为为byte类型
byte_data = unknown_encode.encode('latin-1')
# chardet 接受bytes类型,返回一个字典,返回内容为页面编码类型.
result = chardet.detect(byte_data)
print(result) # 输出:{'encoding': 'utf-8', 'confidence': 0.99, 'language': ''}。注释:编码utf-8的概率为99%。
# 获取编码类型
encode_type = result.get('encoding')
# 解码字符的byte类型
reduction = byte_data.decode(encode_type)
print(reduction) # 输出:好好努力!
二进制逆向
某公厕分别用中文,英文,日语和韩语写了下面一段提示:向前一小步,文明一大步。这种提示在男厕所基本上是见怪不怪了,但关键是下面还有一段用 0 和 1 组成的数字,作为一个程序员能敏锐的感觉到这应该就是上面中文的二进制表示方式。
汉字在计算机中存储常见的字符集编码有 GBK,UTF-8 等,我们先假定是 UTF-8 编码,但 UTF-8 是一种可变长字符编码,怎么确定一个字符占几个字节呢?这就和二进制的表示有关,如果是一个字节,那么最高位就是 0(就是上面讲 ASCII 所介绍的扩充比特),剩下的 7 个比特可以表示 128 种状态,这些字符对应 ASCII 的 128 个字符。如果是两个字节会以 110 开头,三个字节是 1110 开头,四个字节是 11110 开头……。上面的二进制很多地方出现了连续的 3 个 1,大胆猜测应该使用的是 UTF-8 编码。
字节 | 格式 | 实际编码位 | 码点范围 |
---|---|---|---|
1字节 | 0xxxxxxx | 7 | 0 ~ 127 |
2字节 | 110xxxxx 10xxxxxx | 11 | 128 ~ 2047 |
3字节 | 1110xxxx 10xxxxxx 10xxxxxx | 16 | 2048 ~ 65535 |
4字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 21 | 65536 ~ 2097151 |
现在我们使用 Python 进行验证,具体代码如下:
bits = "11100101100100001001000111100101100010011000110111100100101110" \
"00100000001110010110110000100011111110011010101101101001010010" \
"00001110011010010110100001111110011010011000100011101110010010" \
"11100010000000111001011010010010100111111001101010110110100101"
# 确保二进制字符串长度是8的倍数
length = len(bits)
if length % 8 != 0:
raise ValueError("二进制字符长度不是8的倍数")
# 将二进制字符串分割为每8位一组,然后二进制字符串转换为十进制整型,最后转化为字节序列
bytes_seq = bytes([int(bits[i:i + 8], 2) for i in range(0, length, 8)])
# 尝试解码字节序列(如果它们实际上是文本的话)
try:
text = bytes_seq.decode('utf-8')
print(f"输出:{text}") # 输出:向前一小步 文明一大步
except UnicodeDecodeError:
print("可能不是有效的UTF-8编码")
总结拓展
最后我们总结和拓展一下上面学习的知识点,具体如下:
- ASCII 字符集中每个字符的 ASCII 编码都是 1 个字节。
- GBK(扩展国标码)是一个用于简体中文字符的编码标准,它不仅兼容 ASCII 编码,而且还是中文版 Windows 的默认编码。在 GBK 编码中, ASCII 字符集中的字符仍然保持其原有的单字节编码,对于其他中文字符和扩展字符,则使用双字节或更多字节的编码。
- Unicode 字符集中每个字符都有一个唯一的 Unicode 编号,如果把编号映射为固定等长的字节,则需要把小编码字符的高字节位全部填为 0,这在存储和传输上十分不划算。本着节约的精神,出现了**“UTF 可变长编码方式”**,最常用的就是 UTF-8 编码。在 UTF-8 编码中,ASCII 字符集中的字符仍然保持 1 个字节,欧洲文字通常是 2 个字节,汉字通常是 3 个字节,只有很生僻的字符、Emoji 字符才会被编码成 4 ~ 6 个字节。
- 如果你要传输的文本包含大量英文字符,用 UTF-8 编码或 GBK 编码最节省空间,但如果你要传输的文本包含大量中文字符,GBK 编码通常比 UTF-8 编码更节省空间,因为 GBK 是为中国字符设计的,它对常用汉字进行了优化编码,很多中文字符在 GBK 中使用双字节表示,而 UTF-8 中可能需要更多字节(通常是三个字节,但对于某些生僻字可能需要四个字节)。如果你的文本需要跨平台或跨语言处理,UTF-8 编码可能是一个更好的选择,因为它是一个国际化的标准编码,几乎所有的现代系统和应用都支持 UTF-8。而 GBK 主要在中国境内使用,并且在处理非中文字符时可能不如 UTF-8 方便。此外,如果你在处理包含混合字符(如中英文混合)的文本时,UTF-8 的优势可能更加明显,因为它可以在不引入额外开销的情况下同时处理多种语言的字符。
- 一定要注意编码和解码的一致性,例如一个字符串经过 GBK 编码后,再使用 UTF-8 进行解码,就可能会导致乱码或报错,因此不同编码之间不能直接进行转化。