Py学习  »  Python

字符编码(二)|python3中的字符编码问题

dwzb • 6 年前 • 507 次点击  

网上绝大多数写编码问题的博客都是基于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’无论在ASCII、UTF-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()

原因在于

  • astr类型,对应Unicode编码,只能encode不能decode
  • bbytes类型,对应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中支持的字体限制,有些字体无法正常显示一些字符,让人看起来像乱码

专栏信息

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/FPqsBQxIQu
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/8701
 
507 次点击