要说在整个编程领域中最难的问题有哪些的话,字符编码的问题,也就是乱码问题,绝对算得上很多程序员写代码时的一个“噩梦”。以至于在IT界有个著名的笑话,“手持一把锟斤拷,口中直呼烫烫烫”,如果你笑了,那么你肯定是做IT的,哈哈哈。而在python这门语言中,因为python2和python3本身编码机制完全不一样,所以这个问题又尤其突出。包括我本人在内,也被这个编码问题困扰了很久,一直没有完全搞明白。后来,为了彻底解决编码问题,专门查询了很多的书籍和资料,终于搞清楚了关于编码问题的来龙去脉以及各种情况下存在的问题以及解决方式,今天这篇文章就来做一个总结,相信大家只要认真看了之后,妈妈再也不会担心你的编码问题了。

    要彻底弄清楚乱码是怎么来的,有两个大的关键因素必须要了解:一个是究竟有哪些编码类型,各种类型有哪些不同的特点,这些必须烂熟于心。二是你的代码运行环境是什么。比如是在命令行运行?还是在编辑器中运行?在python2中还是在python3中?在linux系统里面?还是在windows系统里面?弄清楚这两个问题,乱码问题便会迎刃而解。接下来我们来一一解决这些问题。

    一、编码类型
    很多人想不明白为什么计算机中有这么多乱七八糟各种各样的编码,比如什么ASCII啊,GBK,GB2312,UNICODE,UTF8,这些都是什么鬼?为什么要有这么多不同的编码格式?要想搞清楚这些编码问题,必须先了解一下关于字符编码的历史,这些都是祖上留下来的“孽债”。

    1. 什么是字符编码
      首先我们来了解下究竟什么是字符编码,为什么要有字符编码这个东西出现?原因很简单,计算机从本质上来说只认识二进制中的0和1,可以说任何数据在计算机中实际的物理表现形式也就是0和1,如果你将硬盘拆开,你是看不到所谓的数字0和1的,你能看到的只是一块光滑闪亮的磁盘,如果你用足够大的放大镜你就能看到磁盘的表面有着无数的凹凸不平的元件,凹下去的代表0,突出的代表1,我们用bit(位)来表示每个这种二进制的数,这就是计算机用来表现二进制的方式。而我们在处理数据时,一般并不是按位来进行处理,而是按照字节(byte)来进行处理的,一个字节byte=8bit。那现在我们面临了第一个问题:如何让人类语言能够被计算机正确理解呢?我们以英文为例(因为计算机是美国佬发明的,所以最开始当然只考虑英文的情况),英文中有英文字母(大小写)、标点符号、特殊符号。如果我们将这些字母与符号给予固定的编号,然后将这些编号转变为二进制用字节来表示,那么计算机明显就能够正确读取这些符号,同时通过这些编号,计算机也能够将二进制转化为编号对应的字符再显示给人类去阅读。所以,基于这种思想,便产生了ASCII码。
    2. ASCII编码
      ASCII码是人类计算机历史上最早发明的字符集,大家都知道 ,计算机是美国佬发明的,他们只用英文,所以可以说ASCII码是专门为表示英文、数字以及英文标点符号而生。由于英文本身比较简单,就是由26个字母组成,加上0-9十个数字以及一些英文的标点符号。而在计算机中,1byte=8bit,也就是说有从0000000-11111111共2的8次方共256种不同的组合,这些组合已经足够存储所有的这些英文字母、数字以及标点了,所以早期的编码只有ASCII编码。
    3. GB2312以及其他编码
      如果全世界的人都使用英文的话,今天我们就不必这么费神来研究编码问题了。正因为全世界的语言太多,大家都想使用自己熟悉的语言来使用计算机,比如中国人用计算机当然使用中文了。那么问题来了,在中文中光常用的汉字就已经达到了6000多个了,很明显之前的ASCII码已经完全无法满足汉字存储的需求了。怎么办?既然使用ASCII码这样一个字节无法搞定,那么我们自然想到能不能多用1个字节是不能就能搞定了呢?所以,为了满足国内在计算机中使用汉字的需要,中国国家标准总局发布了一系列的汉字字符集国家标准编码,统称为GB码,或国标码。其中最有影响的是于1980年发布的《信息交换用汉字编码字符集 基本集》,标准号为GB 2312-1980,因其使用非常普遍,也常被通称为国标码。GB2312编码通行于我国内地;新加坡等地也采用此编码。几乎所有的中文系统和国际化的软件都支持GB 2312。所以,大家可以理解为,GB系列的编码是为了适应复杂的中文编码而对ASCII码的一种扩充。
    4. UNICODE标准编码
      既然咱们中国人能够对ASCII码进行扩充,以便于显示更复杂的中文,那么其他国家呢?比如日本、韩国,其实也面临着同样的问题。所以,他们自然也会对ASCII码扩展出自己的一套编码。假设每种语言都自己搞一套,工作量上去了不说,还为不同编码之间的转换和显示造成了巨大的困难,这也行不通啊。所以,为了简化不同编码之间的显示和转换问题,很有必要搞一套统一的编码格式出来。基于这种情况一种新的编码诞生了:Unicode。Unicode又被称为统一码、万国码;它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode支持欧洲、非洲、中东、亚洲(包括统一标准的东亚象形汉字和韩国表音文字)。这样不管你使用的是英文或者中文,日语或者韩语,在Unicode编码中都有收录,且对应唯一的二进制编码。这样大家都开心了,只要大家都用Unicode编码,那就不存在这些转码的问题了,什么样的字符都能够解析了。
    5. UTF-8编码
      看完上面的UNICODE编码,大家是不是想编码问题已经解决了呢?既然UNICODE能够兼容所有已知的语言和文字,那就全部按照UNICODE来编码就行了呗。如果你这样想的话,就too young too native了。由于UNICODE实际上是使用更多的字节来保存除英文外的其他国家的复杂语言文字,所以对于中文字符这样的文字是非常合适的。比如,中文汉字的“中”字,用UNICODE编码两个字节就可以这样表示:01001110 00101101,这样一点问题都没有。但如果是英文字母呢?本来英文字母只需要一个字节就可以表示,比如大写字母A,用二进制表示为0100 0001,而用UNICODE的话,就必须用0来补足多出来的一个字节,即表示为00000000 01000001。大家看出问题所在了吗?对了,对于英文来说,UNICODE编码太浪费空间了,足足大了一倍的空间。特别是在网络上进行传输时,这种浪费就极其明显,会大大降低我们的传输效率。为了解决这个问题,就出现了一些中间格式的字符集,他们被称为通用转换格式,即UTF(Unicode Transformation Format)。而我们最常用的UTF-8就是这些转换格式中的一种。UTF-8编码其实是一种可“变长”的编码格式,即把英文变长为1个字节,而汉字用3个字节表示,特别生僻的还会变成4-6字节。所以如果是传输或存储大量英文的话,UTF编码格式优势非常明显。
    6. 不同编码格式和UNICODE之间的转换
      为了在不同的编码格式之间进行转换,我们必须对字符进行编码和解码的工作。任何非UNICODE格式的字符(串),我们都可以使用decode方法将其解码为UNICODE编码的字符(串),这种转换过程叫“解码”。同样道理,UNICODE格式的字符(串),也可以通过encode()方法将其编码为其他编码格式的字符(串),这个过程叫“编码”。后面我们会频繁使用到编码和解码的操作,大家都应该明白什么时候应该使用编码,什么时候应该解码。

    image.png
    到此为此,大家应该对编码类型有一定了解了,总结一下就是:
    1.为了处理英文字符,产生了ASCII码。
    2.为了处理中文字符,产生了GB2312。
    3.为了处理各国字符,产生了Unicode。
    4.为了提高Unicode存储和传输性能,产生了UTF-8,它是Unicode的一种实现形式。

    二、运行环境的影响
    搞清楚了上面介绍的各种编码格式之后,接下来我们就开始详细讲解为什么会出现乱码了。关于乱码,大家记住两个要点:
    (1)所谓乱码的本质是字符的编码格式与显示字符的环境编码格式不一致引起的。这句话告诉我们要解决乱码问题,我们需要知道两个信息,一个是字符本身是什么编码,另一个就是显示字符的环境编码是什么,两者必须一致,才能显示出正确的内容。
    (2)由于Unicode编码是标准编码格式,也可以看做是没有任何特定编码格式的“无编码”模式。所以,对于任何Unicode类型编码的字符,打印时python会自动根据环境编码转为特定编码后再显示。

    上面两个要点大家一定要记住,接下来,我们来看看字符在python代码中是怎么被编码的。在不同的python版本中,字符编码的方式也不一样。先来说说比较麻烦的py2版本。如果你用py2来写脚本的话,因为默认py2是用ascii来编码脚本的,所以如果你的脚本中出现了中文,就必须在脚本的开始位置注明支持中文的编码格式,否则会报错。所有支持中文的编码格式都是可以的,比如声明为#coding:utf8或#coding:gbk都是可以的。注明以后,我们就可以在脚本中随意使用中文了。例如下面这个例子:
    image.png
    声明编码格式#coding:utf8或#coding:gbk以后可以正常工作。如下:
    image.png
    在py2中,所有字符串的编码方式默认是用ascii来进行编码的,如果通过coding:xxx的方式声明了脚本的编码方式,则字符串会按照声明的字符编码格式来进行编码,而字符串变量类型是为str类型的。这里大家要记住py2中str一定是有特定编码的,不是Unicode格式(这里为什么要讲这一句,因为待会介绍的py3字符串默认是Unicode编码的,待会我们会细讲)。比如上面的a变量中保存的“中国”这两个中文字符的编码就是gbk格式了。那么当我们打印这个a变量的时候,会出现什么情况呢?我们现在IDE中打印来看看,比如pycharm,打印出来结果如下。纳尼?居然出现了乱码,这是为什么呢?
    image.png
    如果记住了我之前说的关于乱码的那两个要点的同学,应该很容易明白这里为什么会出现乱码。原因很简单,这里a变量的编码是gbk的,而我们运行脚本的编辑器pycharm设置的环境编码却是utf8,两者编码方式并不一致,所以必定会出现乱码。那么怎么解决呢?解决方式有几种,一种是修改#coding:gbk为#coding:utf8,二是可以在’中国’前面加一个u,即a=u’中国’。在前面加u是将“中国”强制转换为unicode编码,即“无编码”,此时变量的type将会变为unicode。前面已经说过,对于unicode编码的字符,python将自动根据环境编码进行显示,所以也就是会自动帮我们编码为utf8进行显示。还有一种方式是通过encode和decode函数,比如像下面:
    image.png
    使用decode方法可以将字符串进行解码,解码后格式就是Unicode了,所以a.decode(‘gbk’)这句跟u”中国”效果是等价的,打印出来当然是没问题的。当然,我们也可以明确写出要编码的类型,比如a.decode(‘gbk’).encode(‘utf8’),这样将Unicode明确地编码为utf8,也是一样的效果。这里大家要注意一点,我们对所有非Unicode类型的字符只能进行decode操作,不能进行encode操作。对Unicode类型的只能是encode而不能decode,这个大家要注意。

    搞明白了pycharm里面的行为后,我们再看看如果这个脚本不是在pycharm里面运行,而是直接在命令行里面运行,又会发生什么问题呢?就将就上面这个文件,我们在命令行里面运行,结果如下:
    image.png
    果不其然,b1正常显示了,b2却出现了乱码。这次出现乱码的原因又是什么呢?这里大家要知道,命令行里面的环境编码是gbk格式,由于b1是Unicode编码,Unicode编码的字符会自动随着环境编码来输出,所以不管在什么环境下,b1都能正常显示输出。而b2由于被encode成了utf8格式,所以它只能在环境编码为utf8的环境中才能正常显示,在命令行这种环境下就会出现由于编码不一致而导致的乱码。大家可以试试直接print a,由于文件是coding:gbk的,所以a是可以直接正常显示的。比如代码如下:
    image.png
    在pycharm中无法正常显示a的值,但在命令行中却可以,如下图:
    image.png
    如果是py3的脚本的话,则要简单得多。因为py3中,脚本的默认编码格式就是utf-8并以该格式保存,所以我们无须再在脚本开头声明coding为gbk或utf8即可在脚本中正常使用中文字符。然后,在运行脚本时,py3解释器又会将utf8全部转换为unicode格式,所以你可以认为在py3中的字符串默认就是unicode格式来编码的,无论在windows或linux中运行,总是能得到正确的显示结果,是不是感觉很爽?所以py3中出现的字符编码问题比py2中要少得多,就算你对编码方面的知识一无所知,也不用太担心字符编码问题,因为一切都已经给你解决。

    上面是通过脚本来运行的情况,那么如果是直接在命令行中写脚本,又会出现什么问题呢?其实不管在哪里运行,上面说的两个原则始终不变,大家永远记住无非我们就是要弄清楚字符本身的编码和环境编码,只要这两者一致了,那一定不会出现乱码。在python shell(即命令行)中直接写代码运行时,大家只需注意在windows下,命令行的默认编码是gbk的,而在Linux环境下,命令行的默认编码是utf8的,其他没什么区别。所以我们接下来分别来看看。

    在windows环境下,我们在命令行中写一段代码来看看,运行效果如下:
    image.png
    大家注意,第一行我们在定义a=”中国”时,并不会报错,因为在命令行中默认是gbk编码,所以此时其实a的编码已经是gbk了,支持中文没有任何问题。直接显示a变量时,打印出来的不是乱码,而是该字符串的字节码表示方式,大家可以理解成给计算机看的,不是给人看的,只有print出来的内容才是给人看的。print a也不会报错,因为按照gbk方式编码并且在gbk环境中运行,不可能会出问题。下面直接将a进行decode解码时,解码方式必须跟编码方式是一致的,所以gbk方式编码的内容不能解码为utf8格式,只能decode为gbk。decode之后,字符串会变为Unicode,也可以正常显示。最后,我们将Unicode编码为utf8时,字符的编码格式又跟环境编码不一致了,所以再次出现了乱码。py3同理,就不再赘述了,如果掌握了之前说的原则,应该完全不会出现问题。如果是在Linux下面的命令行中运行,道理也是一样,只是需要注意linux下命令行默认的编码格式是utf8的就可以了。

    看完上面的内容,我相信大家应该已经掌握了字符编码的所有秘密。不管编码格式是什么,在什么地方执行,大家始终记住那两个关于乱码的原则,问题一定会迎刃而解。接下来,我们再看看更多实际的例子。

    比如,我们在使用爬虫爬取网页时,也会经常遇到乱码,如果结合上面讲的原则,大家是否能够知道问题出在哪,并且解决这些问题呢?我们以网易和百度这两个网站为例,给大家看看会有什么样的问题。首先来看看网易的首页,打开源码,我们可以看到,网页首页的编码格式是gbk的,
    image.png
    编码格式gbk意味着,如果我们需要对抓取的网页内容进行解码的话,必须指定解码方式为gbk才能正常解码为Unicode类型的字符串。假定我们使用的是py3,如果使用默认的decode()方法,将默认解码为utf8,肯定是会报错的。比如下面的代码,我们先抓取163的首页内容,并用正则取出页面的title,代码如下:
    image.png
    这里为什么会报错呢?因为resp.content实际上是抓取的网页的原始字符串,是以gbk编码的二进制内容,所以我们需要知道这个字符串的编码方式才能正确地进行解码。从网页中我们可以知道,该网页的编码为gbk编码方式,所以我们decode时必须指定gbk作为解码的方式(如果decode中不指定解码方式的话,默认以utf8来解码),所以我们应该改为下面这样就可以正确拿到我们的结果:
    image.png
    而对于百度首页,其网页编码方式是utf8的,所以我们在解码时就不用再专门指定utf8格式了,直接decode即可,大家可以自己试试。

    本文到这里终于可以结束了,内容确实不少,因为要搞明白编码的问题,我们需要知道很多东西,这是我们必须要掌握的。另外,很多资料和书籍上都会写到,在py2的脚本中指定编码方式时,必须在脚本开头的位置写coding:utf8,想必大家读完此文应该知道这种说法是对的还是错的了。这就是学习的价值,为什么我们要抠原理、抓本质,就是让我们有足够的能力和底气去判断和质疑一个问题的对和错,只有这样,你的技术才能真正进步,让你去解决更多的问题。最后,希望大家以后再也不会受到乱码问题的困扰了。

    参考资料:
    https://blog.csdn.net/apache0554/article/details/53889253
    https://www.cnblogs.com/yyxayz/p/4044528.html