网上绝大多数写编码问题的博客都是基于 python2 来写的, 他们指出的是 python2 中一些不好的设计导致了更多莫名其妙的编码问题, 但是我们不能忽略 python3 中的一些正常的编码问题
可以这么说, python3 中的编码问题在 python2 中都有, 这部分问题是编码中比较基础的问题, python3 在编码上的设计已经非常好了, 如果我们使用过程中还是有编码方面的报错, 就是我们不懂编码解码原理而造成的而 python2 中有另外一些编码方面的问题, 这是 python2 设计上的不足, 我们即使了解了编码解码的原理, 仍然会对它的报错一头雾水, 因为有时它会偷偷调用一些编码解码程序, 还是错误的程序, 然后抛出异常给我们, 所以在使用 python2 时还要去额外了解它们如何偷偷调用编码解码程序的
所以我认为要弄懂 python2 中编码的错误, 是分两步走: 一是理解编码解码原理, 二是理解 python2 那些不够好的设计前者基础打好了, 看后者就会非常轻松前者是在任何软件中都无法避免的, 不是一句弃 2 改 3 可以解决的
本文借 python3 来讲解第一部分的内容, 分为两个部分, 一个理论, 一个应用
python3 中的编码与解码原理
python3 中的报错或乱码
本文基于 python3.5,windows10 64 位操作系统
python3 中的编码与解码原理
我们先看下面代码>>> a = '中文'>>> a
'中文'>>> print(a)
中文>>> b = 'English'>>> b
- 'English'>>> print(b)
- English
在 python3 中无论是中文还是英文都可以正常 print 出来下面展示编码与解码过程>>> aa = a.encode('utf-8')>>> aa
- b'\xe4\xb8\xad\xe6\x96\x87'>>> a.encode('gbk')
- b'\xd6\xd0\xce\xc4'>>> aa.decode('utf-8')
- '中文'>>> type(a)
- <class 'str'>>>> type(aa)
- <class 'bytes'>>>> '\u4e2d\u6587'
- '中文'>>> print('\u4e2d\u6587')
中文>>> b.encode('ascii')
- b'English'>>> b.encode('utf-8')
- b'English'>>> b.encode('gbk')
- b'English'
接下来就上面的输出结果进行详细解读(这里是本文最重要的地方, 每一句话都很重要)
1. 编码与解码
首先要知道 python3 中涉及到编码与解码的主要只有两个方法: 编码 encode 和解码 decode
编码 (encode) 过程是将 Unicode 形式转化为 utf-8 等其他形式
解码 (decode) 过程是将 utf-8 等其他形式转化为 Unicode 形式
这里一定一定要注意, 要把 Unicode 和 utf-8 等其他形式区分来看待, Unicode 自己是一类, 其他形式合在一起是一类
Unicode 形式的字符串的 type 是 str,utf-8 等其他形式的字符串的 type 是 bytes
可以理解成 Uincode 就是我们看到的字符本身, utf-8 等其他形式是存储进文件时的格式
Unicode 形式的字符串用 print 打印出来就是我们看到的字符, 其他格式 print 都是一些 16 进制数
在 python3 中不涉及与文件网页交互时, 不涉及到编码解码, 也不会涉及到乱码之类的问题, 上面展示的只供学习使用(而 python2 是涉及的, 因此很多人会说弃用 py2 改用 py3 就没有编码问题了, 说的就是这里)
python3 中 a = '中文'这样赋值默认 a 的编码方式是 Unicode,encode 之后得到的 aa 是二进制格式(二进制和 16 进制本质上是一样的)
编码和解码过程是这样的: 比如一串字符, 最初以 GBK 编码格式存在文件中, 我们想将其变成 UTF-8 编码需要先用 GBK 编码将原始的二进制数翻译成字符, 即由 GBK 编码向 Unicode 编码进行转换, 这是解码过程; 得到字符之后再去找这些字符在 UTF-8 编码下对应什么二进制数, 这些二进制数就是我们要的结果, 这是编码过程, 由 Unicode 向 UTF-8 编码的转换所以 Unicode 相当于一个中介, 所有编码的相互转化都要经过它
2. 编码的形式解读
首先要熟悉 python 中出现的编码形式, 有时可以根据它的形式来判断这是什么编码
'\u4e2d\u6587'就是中文二字对应的 Unicode 编码
b'\xe4\xb8\xad\xe6\x96\x87'
就是中文二字对应的 utf-8 编码
b'\xd6\xd0\xce\xc4'
就是中文二字对应的 gbk 编码
其中 \ u 和 \ x 都是转义字符, 和 \ n 换行符类似
\x 表示十六进制数, 每个 \ x 后面跟两位, 每一位都是 0-9abcdef 这 16 个中的一个两位共可以表示 16*16=256 个数, 即可以表示 2^8=2568 位的二进制数可以表示的数也就是说一个 \ x 可以代表一个字节
\u 表示 Unicode 编码, 一个 \ u 后面接 4 位的 16 进制数, 每一位也是 0-9abcdef 这 16 个中的一个, 4 位可以表示 16 位二进制数可以表示的数, 所以说一个 \ u 可以代表两个字节
从字节的角度我们再来看一下这个输出, 中文两个字
在 Unicode 编码中占 4 个字节
在 utf-8 编码中占 6 个字节
在 gbk 编码中占 4 个字节
这个结果和我们之前所说的一个中文字符在各个编码中占字节数相符
再注意到'\u4e2d\u6587'直接输出和 print 都会出现中文二字, 进一步说明 python3 中我们通常说的字符其实就是 Unicode, 将他们看成完全一样的就好
输出'\u4e2d\u6587'这种转义字符时, 是识别了 \ u, 自动通过对照表将后面的那串字节显示成了中文
对于 b''这种前面有个 b 的, type 都变了, 不是 str 而是 bytes, 这种在 print 时会原样输出
3. 各种进制之间相互转化
- int('0x17', 16) # 16 进制转化为 10 进制 23
- int('101010',2) # 二进制转化为 10 进制
- bin(42) # 十进制转化为 2 进制 '0b101010'
- oct(10) # 十进制转化为 8 进制'0o12'
- hex(23) # 十进制转 16 进制 '0x17'
我们可以看到, 转化为 2/8/16 进制都有自己专门的函数, 他们都支持将 10 进制转化为各自进制数; 而 10 进制使用 int 加参数指定从多少进制转化而来这样以 10 进制为中间变量就可以实现各个进制数之间的转化
如果我们想看各种编码对应的二进制数是多少, 十进制数是多少呢, 要对 b \u \x 这样的东西进行处理
首先, 看 unicode 的 16/10/2 进制对应数值
- s = "中文"
- "".join("{:02x}".format(ord(c)) for c in s) # 16 进制'4e2d 6587'"".join("{:d}".format(ord(c)) for c in s) # 10 进制 '20013 25991'
- "".join("{:b}".format(ord(c)) for c in s) # 二进制'100111000101101 110010110000111'
上面代码的原理是
循环中的每个 c 对应'\u4e2d\u6587'中的 \ u4e2d 和 \ u6587
ord 是可以将 \ u 前缀的 16 进制数识别出来并转化为 8 进制数
用 format 再将八进制数指定格式输出成 16 进制数, 这两步相当于去掉了 \ u
join 将得到的 16 进制值用空格拼在一起
下面转化为 10 进制二进制只是把 format 输出格式换了
另外, 反向过程也是可以的, 对于单个字符的正逆过程如下
- ord('中') # 20013
- chr(20013) # '中'
其次, 看 utf-8 的 16/10/2 进制对应数值
utf-8 中的 bytes 是三个 \ x 表示一个文字, 英文则是一个 \ x, 所以没办法通过循环获知每一个字符的各进制数, 只能得到整串字符的各个进制数(其实也是可以对每个字符分别解码再看的)
- a = "中文"
- b = a.encode('utf-8')
- b.hex() # 'e4b8ade69687'
- bin(int(b.hex(), 16)) # '0b111001001011100010101101111001101001011010000111'
因为两个十进制数拼起来和一起算结果不同, 所以放在一起结果有误, 只能分开看十进制结果
- a = "中文"
- for i in a:
- j = i.encode('utf-8')
- print(i,':')
- print(j.hex())
- print(int(j.hex(), 16))
- print(bin(int(j.hex(), 16)))
- # 输出
- # 中 :
- # e4b8ad
- # 14989485
- # 0b111001001011100010101101
- # 文 :
- # e69687
- # 15111815
- # 0b111001101001011010000111
这是 UTF-8 的, 因为 GBK 结果类似, 所以用相同的代码就可以
4. 英文编码
我们继续来看最开始那些编码和解码的代码, 可以发现一个比较奇怪的现象, 即中文转化为 UTF-8 的编码是用 16 进制数表示的, 而英文竟然直接用英文字母表示这是因为英文在各个编码方式中对应的 16 进制数都是一样的(因为众多编码都兼容 ASCII 编码), 比如 A 无论在 ASCIIUTF-8 还是 GBK 中对应的数字都是 65, 转化为 16 进制数也是一样, 所以在 Python 中干脆将这些 ASCII 码对应的 16 进制值以 ASCII 码对应符号命名我们可以用上一部分的方法探究英文字母的真实 16 进制值
- a = "AB"
- for i in a:
- j = i.encode('utf-8')
- print(i,':')
- print(j.hex())
- print(int(j.hex(), 16))
- print(bin(int(j.hex(), 16)))
- # 输出
- # A :
- # 41
- # 65
- # 0b1000001
- # B :
- # 42
- # 66
- # 0b1000010
我们发现 A 的 16 进制值是 41,B 是 42, 用 bytes 形式在 Python 中输出如下>>> m = b'\x41\x42'>>> m
b'AB'
确实这些 16 进制都被字母代替了
python3 中的报错或乱码
本节分为如下部分
没分清 str 和 bytes
字符集没有包含当前字符造成编码错误
二进制数据编码方式和解码方式不统一造成报错
二进制数据编码方式和解码方式不统一造成乱码
输出 unicode 编码本身的所谓乱码
windows 命令行中产生的额外错误
1. 没分清 str 和 bytes
我们有时会看到这样两种报错
- AttributeError: 'str' object has no attribute 'decode'
- AttributeError: 'bytes' object has no attribute 'encode'
上面的报错可以又下面的代码导致
- a = '中文'
- b = a.encode('GBK')
- a.decode()
- b.encode()
原因在于
a 是 str 类型, 对应 Unicode 编码, 只能 encode 不能 decode
b 是 bytes 类型, 对应 UTF-8 编码, 只能 decode 不能 encode
所以涉及编码问题时, 需要编码与解码, 第一步一定是检查变量类型, 看它是 str 还是 btyes, 再决定使用 encode 还是 decode 方法
- type(a) # str
- type(b) # bytes
这点还涉及到一种乱码, 就是 print 时得到类似这样的内容
b'\n<div class="textarea-con">\n <div class="txt-reply">\n \xe4\xbd\xa0\xe8\xbf\x98\xe6\xb2\xa1\xe6\x9c\x89\xe7\x99\xbb\xe5\xbd\x95\xef\xbc\x8c\xe8\xaf\xb7\xe5\x85\x88<span class="lgn js-lgn">\xe7\x99\xbb\xe5\xbd\x95</span>\xe6\x88\x96<span class="regst js-rgr">\xe6\xb3\xa8\xe5\x86\x8c</span>\xe6\x85\x95\xe8\xaf\xbe\xe7\xbd\x91\xe5\xb8\x90\xe5\x8f\xb7\n </div>\n</div>\n'
其实这不是乱码, 只是打印的东西不对, 我们从来不需要打印 UTF-8 等编码后的内容即 bytes, 我们要打印的永远都是 str, 所以不要把 encode 后的结果打印出来, 没有任何意义
2. 字符集没有包含当前字符造成编码错误
其实我们之前提到的 UTF-8/GBK 等编码, 都对应字符集, 即这种编码方式支持对哪些字符编码, UTF-8 就支持所有字符, GBK 对韩文就不支持, ASCII 就不支持中文
我们在 python 中输入各国文字都可以, 因为 Unicode 是支持任意字符的当把字符转化为一些不支持它的编码时, 就会报错, 示例如下
ASCII 中文>>> a = '中文'>>> a.encode('ascii')
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
GBK 韩文>>> a = '-o bba'>>> a.encode('GBK')
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- UnicodeEncodeError: 'gbk' codec can't encode character'\uc624' in position 0: illegal multibyte sequence
注意上面是 can't encode 错误
3. 二进制数据编码方式和解码方式不统一造成报错
对于一个字符集来说, 每一个字符都对应着二进制数据, 但是不能说这么多位的二进制数每个都对应一个字符所以一个字符通过 A 编码变成的二进制数, 可能在 B 编码中并不对应一个字符, 此时用 B 来解码就会报错, 因为找不到这样的字符
例子如下>>> a = '中文'>>> b = a.encode('utf-8')>>> b.decode('gbk')
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- UnicodeDecodeError: 'gbk' codec can't decode byte 0xad in position 2: illegal multibyte sequence
4. 二进制数据编码方式和解码方式不统一造成乱码
和上一点相似, A 编码变成的二进制数, 可能在 B 编码中正好对应着字符, 但是这个字符肯定和 A 中对应的字符不一样了, 很有可能是一些我们平时没见到过的, 被我们称之为乱码, 例子如下>>> a = '方法'>>> b = a.encode('utf-8')>>> b.decode('gbk')
'鏂规硶'
5.windows 命令行中产生的额外错误
有时候会用 windows 下使用交互模式的 python, 有时会编写 py 文件在命令行中调用这两种情况下代码运行结果都会直接打印在命令行窗口中打印在命令行窗口的结果可能会和在 jupyter 中结果不同
在网上 copy 了一些奇怪的文字(这是奥里亚语), 比如在 jupyter 中使用是可以正常返回结果的, 在 IDLE 交互模式下也是可以返回正常结果的
- a = ' '
- print(a)
- #
如果在 1.py 文件中输入上面两行代码, 在 cmd 中输入 python 1.py, 会得到如下结果
- Traceback (most recent call last):
- File "1.py", line 2, in <module>
- print(a)
- UnicodeEncodeError: 'gbk' codec can't encode character'\u0b06' in position 0: illegal multibyte sequence
如果是在 cmd 的 python 交互模式下使用是这样的(其中那个赋值的文字还显示的是方框)>>> a = ' '>>> print(a)
??? ????? ??????
两种情况, 我们一种一种来看
第一种, 从报错内容上来看, 和第二条一样, 可以看出这里相当于执行了 a.encode('GBK'), 但是实际上我们的程序只是打印了, 根本没涉及到编码解码的问题这是因为默认情况下, cmd 显示字符是通过 GBK 来显示的, 也就是会自动把字符先转化为 GBK 的二进制数, 再用这些二进制数对应字符显示在屏幕上, 所以这个屏幕只支持 GBK 字符集收录的字符, 有不属于这个字符集的字符想显示就会报这个错误
解决方法: 先在 cmd 中输入 chcp 65001, 这样会 clear 之前所有命令, 好像一个新的界面一样, 然后再 python 1.py, 理论上就可以正常输出了, 不过还是没完全正常
输出的是一堆方框, 但是将这些方框复制下来, 粘贴到 jupyter 里显示的是正确的字符说明其实打印出来的是正确的, 只是在 cmd 中无法显示而已, 这和 cmd 的字体有关点击左上角图标 - 属性, 修改字体, 不过这个语言实在太偏, 没有一个字体可以将其正常显示
类似地, 拿希伯来文עברית做实验, 在 consolas 字体下也显示不出来, 复制到 jupyter 时也是正常的, 这个情况就和刚才完全一样了, 字体换成 courier new 就发现可以正常显示了, 说明显示方框确实是字体问题
回过头来, 讲一下 chcp 命令 chcp 指代码页, 具体其实不用懂, 默认是 chcp936, 对应编码方式是中文简体 GBK, 而 chcp65001 对应的是 UTF-8, 更多代码页见这个链接
第二种情况
上面 (离这里最近的那个代码块) 展示的代码是直接从 cmd 中复制过来的, 从复制结果来看, 原来显示方框的在这里是正常显示的, 在 cmd 中无法显示是因为字体无法显示; 而下面? 是真的没编码正确其实问题和第一种情况是一样的
在默认的 chcp936 下, 字符需要先编码为 GBK 才能 print 在屏幕上, 而这一步编码是失败的, 所以显示的是问号, 这里没有报错是和第一种的一种点差别, 但是本质上是一样的
对于方框显示问题, 因为没有合适的字体, 可以再拿希伯来文עברית做实验来加深理解我们是不是直接将字体换了就可以正常显示了呢, 其实也不是因为在 chcp936 的情况下, 是没有刚才的字体的, 只有切换到 chcp65001 下才有可以显示的字体
总结
其实在 windows 中出现额外的问题就两种
一种是由于 chcp 代码页的显示, 会有隐藏着的编码过程, 这个过程可能引起编码错误
一种是 cmd 中支持的字体限制, 有些字体无法正常显示一些字符, 让人看起来像乱码
来源: https://juejin.im/entry/5aa73688f265da238a301ac9