概述
在使用 Python 或者其他的编程语言,都会多多少少遇到编码错误,处理起来非常痛苦。在 Stack Overflow 和其他的编程问答网站上,UnicodeDecodeError 和 UnicodeEncodeError 也经常被提及。本篇教程希望能帮你认识 Python 编码,并能够从容的处理编码问题。
本教程提到的编码知识并不限定在 Python,其他语言也大同小异,但我们依然会以 Python 为主,来演示和讲解编码知识。
通过该教程,你将学习到如下的知识:
- 获取有关字符编码和数字系统的概念
- 理解编码如何使用 Python 的 str 和 bytes
- 通过 int 函数了解 Python 对数字系统的支持
- 熟悉 Python 字符编码和数字系统相关的内置函数
什么是字符编码
现在的编码规则已经有好多了,最简单、最基本是的 ASCII 编码,只要是你学过计算机相关的课程,你就应该多少了解一点 ASCII 编码,他是最小也是最适合了解字符编码原理的编码规则。具体如下:
- 小写英文字符:a-z
- 大写英文字符:A-Z
- 符号: 比如 $ 和!
- 空白符:回车、换行、空格等
- 一些不可打印的字符: 比如 b 等
那么,字符编码的定义到底是什么了?它是一种将字符(如字母,标点符号,符号,空格和控制字符)转换为整数并最终转换为 bit 进行存储的方法。每个字符都可以编码为唯一的 bit 序列。如果你对 bit 的概念不了解,请不要担心,我们后面会介绍。
ASCII 码的字符被分为如下几组:
ASCII 表一共包括 128 个字符,如果你想了解整个 ASCII 表,这里有
大家在学 python 的时候肯定会遇到很多难题,以及对于新技术的追求,这里推荐一下我们的 Python 学习扣 qun:784,758,214,这里是 python 学习者聚集地
Python string 模块
string 模块是 python 里处理字符串很方便的模块,它包括了整个 ASCII 字符,让我们来看看部分 string 模块源码:
# From lib/python3.7/string.py
whitespace = '\t\n\r\v\f'
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_letters = ascii_lowercase + ascii_uppercase
digits = '0123456789'
hexdigits = digits + 'abcdef' + 'ABCDEF'
octdigits = '01234567'
punctuation = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
printable = digits + ascii_letters + punctuation + whitespace
你可以在 Python 中这样使用 string 模块:
>>> import string
>>> s = "What's wrong with ASCII?!?!?">>> s.rstrip(string.punctuation)'What's wrong with ASCII'
什么是 bit
学过计算机相关课程的同学,应该都知道,bit 是计算机内部存储单位,只有 0 和 1 两个状态 (二进制),我们上面所说的 ASCII 表,都是一个 10 进制的数字表示一个字符,而这个 10 进制数字,最终会转换成 0 和 1,存储在计算机内部。例如 (第一列是 10 进制数字,第二列是二进制,第三列是计算机内部存储结果):
这是一种在 Python 中将 ASCII 字符串表示为位序列的方便方法。ASCII 字符串中的每个字符都被伪编码为 8 位,8 位序列之间有空格,每个字符代表一个字符:
>>> def make_bitseq(s: str) -> str:
... if not s.isascii():
... raise ValueError("ASCII only allowed")
... return "".join(f"{ord(i):08b}" for i in s)
>>> make_bitseq("bits")
'01100010 01101001 01110100 01110011'
>>> make_bitseq("CAPS")
'01000011 01000001 01010000 01010011'
>>> make_bitseq("$25.43")
'00100100 00110010 00110101 00101110 00110100 00110011'
>>> make_bitseq("~5")
'01111110 00110101'
我们也可以是用 python 的 f -string 来格式化,比如 f ”{ord(i):08b}”:
- 冒号的左侧是 ord(i),它是实际的对象,其值将被格式化并插入到输出中。使用 ord()为单个 str 字符提供了 base-10 代码点。
- 冒号的右侧是格式说明符。08 表示宽度为 8,0 填充,b 用作在基数 2(二进制)中输出结果数的符号。
ASCII 编码不够用了
ASCII 采用的是 8bit 来存储字符 (只使用 7 位,剩下的 1 位二进制为 0),所以,ASCII 最多存储 128 个字符,这有个简单的公式,计算存储字符的 bit 数量与存储字符总数的关系:2 的 n 次方,n 表示 bit 数量。例如:
- 1bit 存储 2 个字符
- 8bit 存储 256 个字符
- 64bit 存储 2 的 64 次方 == 18,446,744,073,709,551,616
我们可以写个简单的代码,来计算一下,指定字符数量,至少需要多少 bit 来存储:
>>> from math import ceil, log
>>> def n_bits_required(nvalues: int) -> int:
... return ceil(log(nvalues) / log(2))
>>> n_bits_required(256)
8
数字系统
在上面的 ASCII 讨论中,您看到每个字符映射到 0 到 127 范围内的整数。但在 CPython 中还有其他的数字系统,通过其他方式是表示数字。除了十进制外,python 还支持以下几个方式:
- Binary: 2 进制
- Octal: 8 进制
- Hexadecimal (hex): 16 进制
你可能要问,为什么有了十进制,还要支持这么多其他进制的数字了?这个取决你的业务场景和操作系统,在 Python 里,把 str 转换成 int,默认是 10 进制的。
>>> int('11')
11
>>> int('11', base=10) # 10 is already default
11
>>> int('11', base=2) # Binary
3
>>> int('11', base=8) # Octal
9
>>> int('11', base=16) # Hex
17
你可以在赋值时,直接告诉解释器数字的类型,不同进制标表示方法如下:
类型 | 前缀 | 示例 |
---|---|---|
n/a | n/a | 11 |
二进制 | 0b 或者 0B | 0b11 |
八进制 | 0o 或者 0O | 0o11 |
十六进制 | 0x 或者 0X | 0x11 |
>>> 11
11
>>> 0b11 # 二进制
3
>>> 0o11 # 八进制
9
>>> 0x11 # 16 进制
17
深入 Unicode
正如您所看到的,ASCII 的问题在于它不是一个足够大的字符集来容纳世界上的语言,方言,符号和字形。(这对于英语来说甚至都不够大。)Unicode 从根本上起到与 ASCII 相同的作用,但是 Unicode 拥有更大的存储空间,具有 1,114,112 个可能的字符,能够完全包含世界上所有的语言。事实上,ASCII 是 Unicode 的完美子集。Unicode 表中的前 128 个字符与您合理期望的 ASCII 字符完全对应。
Unicode 本身不是编码,但是有很多遵循 Unicode 编码规范编码,后面讲到的 UTF- 8 就是其中一个。
Unicode vs UTF-8
Unicode 是一种抽象编码标准,而不是编码。这就是 UTF- 8 和其他编码方案发挥作用的地方。Unicode 标准(字符到代码点的映射)从其单个字符集定义了几种不同的编码。UTF- 8 及其较少使用的表兄弟 UTF-16 和 UTF-32 是用于将 Unicode 字符表示为每个字符一个或多个字节的二进制数据的编码格式。我们稍后将讨论 UTF-16 和 UTF-32,但到目前为止,UTF- 8 占据了最大份额。
Python 3 里的编码与解码
Python 3 的 str 类型用于表示人类可读的文本,可以包含任何 Unicode 字符。
相反,字节类型表示二进制数据或原始字节序列,它们本质上没有附加编码。
编码和解码是从一个到另一个的过程:
decode 和 encode 函数,默认编码是 utf-8:
>>> "résumé".encode("utf-8")
b'r\xc3\xa9sum\xc3\xa9'
>>> "El Niño".encode("utf-8")
b'El Ni\xc3\xb1o'
>>> b"r\xc3\xa9sum\xc3\xa9".decode("utf-8")
'résumé'
>>> b"El Ni\xc3\xb1o".decode("utf-8")
'El Niño'
str.encode()的结果是一个 bytes 对象,bytes 对象只允许 ASCII 字符。这就是为什么在调用“ElNiño”.encode(“utf-8”)时,允许 ASCII 兼容的“El”按原样表示,但带有波浪号的 n 被转义为“xc3 xb1”。这个看起来很乱的序列代表两个字节,十六进制为 0xc3 和 0xb1:
>>> "".join(f"{i:08b}"for i in (0xc3, 0xb1))'11000011 10110001'
Python3 一切字符皆 Unicode
- 默认情况下,Python 3 源代码假定为 UTF-8。这意味着您不需要# – – 编码:UTF-8 – – 位于 Python 3 中.py 文件的顶部。
- 默认情况下,所有文本(str)都是 Unicode。编码的 Unicode 文本表示为二进制数据(字节)。str 类型可以包含任何文字 Unicode 字符,例如“Δv/Δt”,所有这些字符都将存储为 Unicode。
- Unicode 字符集中的任何内容都是标识符中的犹太符号,这意味着 résumé=“〜/ Documents / resume.pdf”是有效的,虽然这看起来很花哨。
- Python 的 re 模块默认为 re.UNICODE 标志而不是 re.ASCII。这意味着,例如,r“w”匹配 Unicode 字符,而不仅仅是 ASCII 字母。
- str.encode()和 bytes.decode()中的默认编码是 UTF-8。
还有一个更细微的属性,即内置的 open()的默认编码是依赖于平台的,并且取决于 locale.getpreferredencoding()的值:
>>> # Mac OS X High Sierra
>>> import locale
>>> locale.getpreferredencoding()
'UTF-8'
>>> # Windows Server 2012; other Windows builds may use UTF-16
>>> import locale
>>> locale.getpreferredencoding()
'cp1252'
一个关键特性是 UTF- 8 是一种可变长度编码。回想一下关于 ASCII 的部分。扩展 ASCII-land 中的所有内容最多需要一个字节的空间。您可以使用以下生成器表达式快速证明这一点:
>>> all(len(chr(i).encode("ascii")) == 1 for i in range(128))
True
UTF- 8 完全不同。给定的 Unicode 字符可以占用 1 到 4 个字节。以下是占用四个字节的单个 Unicode 字符的示例:
>>> ibrow = "????"
>>> len(ibrow)
1
>>> ibrow.encode("utf-8")
b'\xf0\x9f\xa4\xa8'
>>> len(ibrow.encode("utf-8"))
4
>>> # Calling list() on a bytes object gives you
>>> # the decimal value for each byte
>>> list(b'\xf0\x9f\xa4\xa8')
[240, 159, 164, 168]
这是 len()的一个微妙但重要的特性:
- 作为 Python str 的单个 Unicode 字符的长度始终为 1,无论它占用多少字节。
- 编码为字节的相同字符的长度将介于 1 和 4 之间。
UTF-16 和 UTF-32
我们来聊聊 UTF-16 和 UTF-32,在实际的编程实践中,它们和 UTF- 8 区别还是很重要的,下面的通过实例我们来看看具体区别:
>>> letters = "αβγδ"
>>> rawdata = letters.encode("utf-8")
>>> rawdata.decode("utf-8")
'αβγδ'
>>> rawdata.decode("utf-16") #
'뇎닎돎듎'
在这种情况下,使用 UTF- 8 编码四个希腊字母然后解码回 UTF-16 中的文本将产生一个完全不同语言(韩语)的文本 str。
此表汇总了 UTF-8,UTF-16 和 UTF-32 下的字节范围或字节数:
编码 | 长度 (字节) | 是否可变 |
---|---|---|
UTF-8 | 1~4 | 是 |
UTF-16 | 2~4 | 是 |
UTF-32 | 4 | 否 |
UTF 系列编码另外一个需要注意的地方是,UTF- 8 编码占用存储空间不一定比 UTF-16 少,因为他们都不是固定长度的。例如
>>> text = "記者 鄭啟源 羅智堅"
>>> len(text.encode("utf-8"))
26
>>> len(text.encode("utf-16"))
22
原因是 U + 0800 到 U + FFFF(十进制的 2048 到 65535)范围内的代码点占用了 UTF- 8 中的三个字节,而 UTF-16 中仅占用了两个字节。
正常情况下,最好不用使用 UTF-16,除非特殊要求,不然 UTF- 8 更加通用。
Python 内建函数
Python 内置了很多与编码相关的函数:
- ascii()
- bin()
- bytes()
- chr()
- hex()
- int()
- oct()
- ord()
- str()
可以分成以下几组:
- ascii(),bin(),hex() 和 oct(), 第一个是 ascii(),它生成一个仅对象的 ASCII 表示,其中非 ASCII 字符被转义。其余三个分别给出整数的二进制,十六进制和八进制表示。
- bytes(),str() 和 int() 是各自类型,bytes,str 和 int 的类构造函数。它们各自提供了将输入强制转换为所需类型的方法。例如,如前所述,虽然 int(11.0) 可能更常见,但您可能也会看到 int(‘11’,base = 16)。
- ord() 和 chr(),ord() 将 str 字符转换为 10 进制,而 chr() 执行相反的操作。
Python 中的其他编码
目前,我们讲了 4 中编码:
- ASCII
- UTF-8
- UTF-16
- UTF-32
还有其他很多编码,比如 Latin-1(也称作 ISO-8859-1),这是 HTTP 默认的编码,然而 windows 是 Latin- 1 变体,称作 cp1252。
完整的已接受编码列表隐藏在编解码器模块的文档中,该模块是 Python 标准库的一部分。
还有一个有用的公认编码需要注意,即“unicode-escape”。如果您有一个已解码的 str 并希望快速获得其转义的 Unicode 文字的表示,那么您可以在.encode()中指定此编码:
>>> alef = chr(1575) # Or "\u0627"
>>> alef_hamza = chr(1571) # Or "\u0623"
>>> alef, alef_hamza
('ا', 'أ')
>>> alef.encode("unicode-escape")
b'\\u0627'
>>> alef_hamza.encode("unicode-escape")
b'\\u0623'
注意外部数据编码
虽然 Python 代码默认使用了 UTF- 8 作为编码,但并不意味着外部输入的数据也是 UTF- 8 编码的,如果这些外部的数据没有指定编码,那么在处理他们是,你就要格外小心了。比如你调用 API 获取数据,正常是用 UTF- 8 去解码,没有问题,但如果突然 API 给你返回这样的数据:
>>> data = b"\xbc cup of flour"
>>> data.decode("utf-8")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbc in position 0: invalid start byte
这地方抛出了 UnicodeDecodeError 错误,仔细检查,发现其实数据的编码是 Latin-1。
>>> data.decode("latin-1")
'¼ cup of flour'
如果你对字符串的编码不确定,可以使用 chardet 库来检查字符串编码。
总结
在本文中你已经了解了编码的详细原理,相信你在以后的编程过程中,再遇到编码错误,相信你能比较从容的解决了。