Python 编码错误的本质缘由

不论你是有着多年经验的 Python 老司机仍是刚入门 Python 不久的新贵,你必定遇到过UnicodeEncodeError、UnicodeDecodeError 错误,每当遇到错误咱们就拿着 encode、decode 函数翻来覆去的转换,有时试着试着问题就解决了,有时候怎么试都没辙,只有借用 Google 大神帮忙,但彷佛不多去关心问题的本质是什么,下次遇到相似的问题重蹈覆辙,那么你有没有想过一次性完全把 Python 字符编码给搞懂呢?javascript

彻底理解 Python 字符编码前,咱们有必要把一些基础概念弄清楚,虽然有些概念咱们天天都在接触甚至在使用它,但并不必定真正理解它。好比:字节、字符、字符集、字符码、字符编码。html

字节

字节(Byte)是计算机中存储数据的单元,一个字节等于一个8位的比特,计算机中的全部数据,不管是磁盘文件上的仍是网络上传输的数据(文字、图片、视频、音频文件)都是由字节组成的。java

字符

你正在阅读的这篇文章就是由不少个字符(Character)构成的,字符一个信息单位,它是各类文字和符号的统称,好比一个英文字母是一个字符,一个汉字是一个字符,一个标点符号也是一个字符。python

字符集

字符集(Character Set)就是某个范围内字符的集合,不一样的字符集规定了字符的个数,好比 ASCII 字符集总共有128个字符,包含了英文字母、阿拉伯数字、标点符号和控制符。而 GB2312 字符集定义了7445个字符,包含了绝大部分汉字字符。程序员

字符码

字符码(Code Point)指的是字符集中每一个字符的数字编号,例如 ASCII 字符集用 0-127 这连续的128个数字分别表示128个字符,"A" 的编号就是65。网络

字符编码

字符编码(Character Encoding)是将字符集中的字符码映射为字节流的一种具体实现方案,常见的字符编码有 ASCII 编码、UTF-8 编码、GBK 编码等。某种意义上来讲,字符集与字符编码有种对应关系,例如 ASCII 字符集对应 有 ASCII 编码。ASCII 字符编码规定使用单字节中低位的7个比特去编码全部的字符。例如"A" 的编号是65,用单字节表示就是0×41,所以写入存储设备的时候就是b'01000001'。函数

编码、解码

编码的过程是将字符转换成字节流,解码的过程是将字节流解析为字符。编码


理解了这些基本的术语概念后,咱们就能够开始讨论计算机的字符编码的演进过程了。spa

从 ASCII 码提及

说到字符编码,要从计算机的诞生开始提及,计算机发明于美国,在英语世界里,经常使用字符的个数很是有限,26个字母(大小写)、10个数字、标点符号、控制符,这在计算机中用一个字节的存储空间来表示一个字符绰绰有余,由于一个字节至关于8个比特位,8个比特位能够表示256个符号。因而美国国家标准协会ANSI制定了一套字符编码的标准叫 ASCII(American Standard Code for Information Interchange),每一个字符都对应惟一的一个数字,好比字符 "A" 对应数字是65,"B" 对应 66,以此类推。最先 ASCII 只定义了128个字符编码,包括96个文字和32个控制符号,一共128个字符只须要一个字节的7位就能表示全部的字符,所以 ASCII 只使用了一个字节的后7位,剩下最高位1比特被用做一些通信系统的奇偶校验。.net

ascii

扩展的 ASCII:EASCII(ISO/8859-1)

然而计算机慢慢地普及到其余西欧地区时,发现还有不少西欧字符是 ASCII 字符集中没有的,显然 ASCII 已经无法知足人们的需求了,好在 ASCII 字符只用了字节的前7位 0×00~0x7F 共128个字符,因而他们在 ASCII 的基础上把原来的7位扩充到8位,把0×80-0xFF这后面的128个数字利用起来,叫 EASCII ,它彻底兼容ASCII,扩展出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号。然而 EASCII 时代是一个混乱的时代,各个厂家都有本身的想法,你们没有统一标准,他们各自把最高位按照本身的标准实现了本身的一套字符编码标准,比较著名的就有 CP437, CP437 是 始祖IBM PC、MS-DOS使用的字符编码,以下图:

cp437

众多的 ASCII 扩充字符集之间互不兼容,这样致使人们没法正常交流,例如200在CP437字符集表示的字符是 È ,在 ISO/8859-1 字符集里面显示的就是 ╚,因而国际标准化组织制定的一系列8位字符集标准 ISO/8859-1(Latin-1),它继承了 CP437 字符编码的128-159之间的字符,因此它是从160开始定义的,ISO-8859-1在 CP437 的基础上从新定义了 160~255之间的字符。

iso8859-1

多字节字符编码 GBK

ASCII 字符编码是单字节编码,计算机进入中国后面临的一个问题是如何处理汉字,对于拉丁语系国家来讲经过扩展最高位,单字节表示全部的字符已经绰绰有余,可是对于亚洲国家来讲一个字节就显得捉襟见肘了。因而中国人本身弄了一套叫 GB2312 的双字节字符编码,又称GB0,1981 由中国国家标准总局发布。GB2312 编码共收录了6763个汉字,同时他还兼容 ASCII,GB 2312的出现,基本知足了汉字的计算机处理须要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率,不过 GB2312 仍是不能100%知足中国汉字的需求,对一些罕见的字和繁体字 GB2312 无法处理,后来就在GB2312的基础上建立了一种叫 GBK 的编码,GBK 不只收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。一样 GBK 也是兼容 ASCII 编码的,对于英文字符用1个字节来表示,汉字用两个字节来标识。

Unicode 的问世

GBK仅仅只是解决了咱们本身的问题,可是计算机不止是美国人和中国人用啊,还有欧洲、亚洲其余国家的文字诸如日文、韩文全世界各地的文字加起来估计也有好几十万,这已经大大超出了ASCII 码甚至GBK 所能表示的范围了,虽然各个国家能够制定本身的编码方案,可是数据在不一样国家传输就会出现各类各样的乱码问题。若是只用一种字符编码就能表示地球甚至火星上任何一个字符时,问题就迎刃而解了。是它,是它,就是它,咱们的小英雄,统一联盟国际组织提出了Unicode 编码,Unicode 的学名是”Universal Multiple-Octet Coded Character Set”,简称为UCS。它为世界上每一种语言的每个字符定义了一个惟一的字符码,Unicode 标准使用十六进制数字表示,数字前面加上前缀 U+,好比字母『A』的Unicode编码是 U+0041,汉字『中』的Unicode 编码是U+4E2D

Unicode有两种格式:UCS-2和UCS-4。UCS-2就是用两个字节编码,一共16个比特位,这样理论上最多能够表示65536个字符,不过要表示全世界全部的字符显示65536个数字还远远不过,由于光汉字就有近10万个,所以Unicode4.0规范定义了一组附加的字符编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)。理论上彻底能够涵盖一切语言所用的符号。

Unicode 的局限

可是 Unicode 有必定的局限性,一个 Unicode 字符在网络上传输或者最终存储起来的时候,并不见得每一个字符都须要两个字节,好比字符“A“,用一个字节就能够表示的字符,恰恰还要用两个字节,显然太浪费空间了。

第二问题是,一个 Unicode 字符保存到计算机里面时就是一串01数字,那么计算机怎么知道一个2字节的Unicode字符是表示一个2字节的字符呢,例如“汉”字的 Unicode 编码是 U+6C49,我能够用4个ascii数字来传输、保存这个字符;也能够用utf-8编码的3个连续的字节E6 B1 89来表示它。关键在于通讯双方都要承认。所以Unicode编码有不一样的实现方式,好比:UTF-八、UTF-16等等。Unicode就像英语同样,作为国与国之间交流世界通用的标准,每一个国家有本身的语言,他们把标准的英文文档翻译成本身国家的文字,这是实现方式,就像utf-8。

UTF-8

UTF-8(Unicode Transformation Format)做为 Unicode 的一种实现方式,普遍应用于互联网,它是一种变长的字符编码,能够根据具体状况用1-4个字节来表示一个字符。好比英文字符这些本来就能够用 ASCII 码表示的字符用UTF-8表示时就只须要一个字节的空间,和 ASCII 是同样的。对于多字节(n个字节)的字符,第一个字节的前n为都设为1,第n+1位设为0,后面字节的前两位都设为10。剩下的二进制位所有用该字符的unicode码填充。

code

以『好』为例,『好』对应的 Unicode 是597D,对应的区间是 0000 0800—0000 FFFF,所以它用 UTF-8 表示时须要用3个字节来存储,597D用二进制表示是: 0101100101111101,填充到 1110xxxx 10xxxxxx 10xxxxxx 获得 11100101 10100101 10111101,转换成16进制是 e5a5bd,所以『好』的 Unicode 码 U+597D 对应的 UTF-8 编码是 “E5A5BD”。你能够用 Python 代码来验证:

>>> a = u"好"
>>> a
u'\u597d'
>>> b = a.encode('utf-8')
>>> len(b)
3
>>> b
'\xe5\xa5\xbd'复制代码

如今总算把理论说完了。再来讲说 Python 中的编码问题。Python 的诞生时间比 Unicode 要早不少,Python2 的默认编码是ASCII,Python3 的默认编码是 UTF-8

>>> import sys
>>> sys.getdefaultencoding()
'ascii'复制代码

因此在 Python2 中,源代码文件必须显示地指定编码类型,不然但凡代码中出现有中文就会报语法错误

# coding=utf-8
或者是:
# -*- coding: utf-8 -*-复制代码

Python2 字符类型

在 python2 中和字符串相关的数据类型有 str 和 unicode 两种类型,它们继承自 basestring,而 str 类型的字符串的编码格式能够是 ascii、utf-八、gbk等任何一种类型。


图片来源: funhacks.net/2016/11/25/character_encoding

对于汉字『好』,用 str 表示时,它对应的 utf-8 编码 是’\xe5\xa5\xbd’,对应的 gbk 编码是 ‘\xba\xc3’,而用 unicode 表示时,他对应的符号就是u’\u597d’,与u”好” 是等同的。

str 与 unicode 的转换

在 Python 中 str 和 unicode 之间是如何转换的呢?这两种类型的字符串之间的转换就是靠decode 和 encode 这两个函数。encode 负责将unicode 编码成指定的字符编码,用于存储到磁盘或传输到网络中。而 decode 方法是根据指定的编码方式解码后在应用程序中使用。

#从unicode转换到str用 encode

>>> b  = u'好'
>>> c = b.encode('utf-8')
>>> type(c)
<type 'str'>
>>> c
'\xe5\xa5\xbd'

#从str类型转换到unicode用decode

>>> d = c.decode('utf-8')
>>> type(d)
<type 'unicode'>
>>> d
u'\u597d'复制代码

UnicodeXXXError 错误的缘由

在字符编码转换操做时,遇到最多的问题就是 UnicodeEncodeError 和 UnicodeDecodeError 错误了,这些错误的根本缘由在于 Python2 默认是使用 ascii 编码进行 decode 或者 encode 操做的,例如:

case 1

>>> s = '你好'
>>> s.decode()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)复制代码

当把 s 转换成 unicode 类型的字符串时,decode 方法默认使用 ascii 编码进行解码,而 ascii 字符集中根本就没有中文字符『你好』,因此就出现了 UnicodeDecodeError,正确的方式是显示地指定 UTF-8 字符编码。

>>> s.decode('utf-8')
u'\u4f60\u597d'复制代码

一样地道理,对于 encode 操做,把 unicode字符串转换成 str类型的字符串时,默认也是使用 ascii 编码进行编码转换的,而 ascii 字符集找不到中文字符『你好』,因而就出现了UnicodeEncodeError 错误。

>>> a = u'你好'
>>> a.encode()
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)复制代码

case 2

str 类型与 unicode 类型的字符串混合使用时,str 类型的字符串会隐式地将 str 转换成 unicode字符串,若是 str字符串是中文字符,那么就会出现UnicodeDecodeError 错误,由于 python2 默认会使用 ascii 编码来进行 decode 操做。

>>> s = '你好'  # str类型
>>> y = u'python'  # unicode类型
>>> s + y    # 隐式转换,即 s.decode('ascii') + u
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)复制代码

正确地方式是显示地指定 UTF-8 字符编码进行解码

>>> s.decode('utf-8') +y
u'\u4f60\u597dpython'复制代码

乱码

全部出现乱码的缘由均可以归结为字符通过不一样编码解码在编码的过程当中使用的编码格式不一致,好比:

# encoding: utf-8

>>> a='好'
>>> a
'\xe5\xa5\xbd'
>>> b=a.decode("utf-8")
>>> b
u'\u597d'
>>> c=b.encode("gbk")
>>> c
'\xba\xc3'
>>> print c
��复制代码

utf-8编码的字符‘好’占用3个字节,解码成Unicode后,若是再用gbk来解码后,只有2个字节的长度了,最后出现了乱码的问题,所以防止乱码的最好方式就是始终坚持使用同一种编码格式对字符进行编码和解码操做。

decode-encode

本文首发于公众号『一个程序员的微站』(id:VTtalk),分享 Python 干货的有温度的内容
博客地址:foofish.net/python-unic…

一个程序员的微站
相关文章
相关标签/搜索