概述
在使用 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 表, 这里有 https://baike.baidu.com/item/ASCII/309296
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 库来检查字符串编码.
总结
在本文中你已经了解了编码的详细原理, 相信你在以后的编程过程中, 再遇到编码错误, 相信你能比较从容的解决了.
来源: https://segmentfault.com/a/1190000019289565