专栏名称: dwzb
目录
相关文章推荐
51好读  ›  专栏  ›  dwzb

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

dwzb  · 掘金  ·  · 2018-03-15 02:17

正文

网上绝大多数写编码问题的博客都是基于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=256 8位的二进制数可以表示的数。也就是说一个 \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进制值







请到「今天看啥」查看全文