深入研究字符编码,告别Python乱码

  • 原创
  • Madman
  • /
  • 2018-04-18 09:18
  • /
  • 0
  • 331 次阅读

Python编码转换.png

Synopsis: 计算机只认识二进制0和1,任何输入输出都是字节流,应用程序(包括操作系统)按照某一编码格式将字符图形数据,编码成字节流进行存储或传输。如果编码和解码两个步骤所使用的编码格式不一致,就会出现乱码。不管是Python2还是Python3,建议源代码都保存为UTF-8格式,这样的话,Python3就不会出现乱码了,而Python2则需要注意str类型与unicode类型的区别

1. 什么是字符编码

理论知识比较枯燥,但建议从头至尾阅读,如果没耐心,也可以从1.5章节开始阅读!

在计算机内部,所有的信息最终都表示为二进制比特位序列,每个二进制比特位bit)表示01,因此八个二进制位就可以组合出2^8=256种状态,从0000 00001111 1111,这被称为一个字节(byte)。

1.1 ASCII/EASCII

(1) ASCII

维基百科

ASCII (American Standard Code for Information Interchange),美国信息交换标准代码,是美国(计算机最初由美国发明)于上世纪60年代制定的一套字符编码规则。使用一个字节来表示字符与二进制序列之间的对应关系,最高一个比特位用于奇偶校验位,所以ASCII的实际取值范围是000 0000111 1111共128个字符

缺点:

ASCII的局限在于只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,因此只能用于显示现代美国英语(而且在处理英语当中的外来词如naïve、café、élite等时,所有重音符号都不得不去掉,即使这样做会违反拼写规则)。

(2) EASCII

维基百科

EASCII (Extended ASCII),延伸美国标准信息交换码,是将ASCII码由7位扩充为8位而成,共256个字符。EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号。

  • Code Page 437
  • ISO/IEC 8859-1

缺点:

不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表示的符号是一样的,不一样的只是128--255的这一段。

1.2 GB 2312/GBK/GB 18030/Big5

拉丁语系国家使用扩展ASCII(8 bit)就能表示所有字符,但非拉丁语系使用的符号远不止256这么少,汉字就多达10万字符左右,一个字节只能表示256个字符,所以必须使用多个字节来表示。

(1) GB 2312

维基百科

GB 2312GB 2312–80 是中华人民共和国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB 2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB 2312。

GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但对于人名、古汉语等方面出现的罕用字和繁体字,GB 2312不能处理,因此后来GBK及GB 18030汉字字符集相继出现以解决这些问题。

每个汉字及符号以两个字节来表示。第一个字节称为"高位字节",第二个字节称为"低位字节"。"高位字节"使用了0xA1–0xF7(把01–87区的区号加上0xA0),"低位字节"使用了0xA1–0xFE(把01–94加上0xA0)。 由于一级汉字从16区起始,汉字区的"高位字节"的范围是0xB0–0xF7,"低位字节"的范围是0xA1–0xFE,占用的码位是72*94=6768。其中有5个空位是D7FA–D7FE。例如"啊"字在大多数程序中,会以两个字节,0xB0(第一个字节)0xA1(第二个字节)储存。(与区位码对比:0xB0=0xA0+16,0xA1=0xA0+1)。

(2) GBK

维基百科

GBK (Chinese Internal Code Extension Specification),汉字内码扩展规范,全名为《汉字内码扩展规范(GBK)》1.0版。 GBK共收录21886个汉字和图形符号,其中汉字(包括部首和构件)21003个,图形符号883个。GBK 只为“技术规范指导性文件”,不属于国家标准。

由于GB 2312-80只收录6763个汉字,有不少汉字,如部分在GB 2312-80推出以后才简化的汉字(如“啰”),部分人名用字(如中国前总理朱镕基的“镕”字),台湾及香港使用的繁体字,日语及朝鲜语汉字等,并未有收录在内。于是厂商微软利用GB 2312-80未使用的编码空间,收录GB 13000.1-93全部字符制定了GBK编码。

根据微软资料,GBK是对GB2312-80的扩展,也就是CP936字码表(Code Page 936)的扩展(之前CP936和GB 2312-80一模一样),最早实现于Windows 95简体中文版。虽然GBK收录GB 13000.1-93的全部字符,但GBK是一种编码方式并向下兼容GB2312;而GB 13000.1-93等同于Unicode 1.1是一种字符集,它的几种编码方式如UTF8、UTF16LE等,与GBK完全不兼容。

(3) GB 18030

维基百科

GB 18030,全称:“国家标准GB 18030-2005《信息技术 中文编码字符集》”,是中华人民共和国现时最新的变长度多字节字符集。对GB 2312-1980完全向后兼容,与GBK基本向后兼容;支持GB 13000(Unicode)的所有码位;共收录汉字70,244个。

GB 18030主要有以下特点:

  • 采用变长多字节编码,每个字可以由1个、2个或4个字节组成。
  • 编码空间庞大,最多可定义161万个字符。
  • 支持中国国内少数民族文字,不需要动用造字区。
  • 汉字收录范围包含繁体汉字以及日韩汉字。

(4) Big5

维基百科

Big5又称为大五码或五大码,是使用繁体中文(正体中文)社区中最常用的电脑汉字字符集标准,共收录13,060个汉字。中文码分为内码及交换码两类,Big5属中文内码,知名的中文交换码有CCCII、CNS11643。Big5虽普及于台湾、香港与澳门等繁体中文通行区,但长期以来并非当地的国家/地区标准或官方标准,而只是业界标准。倚天中文系统、Windows繁体中文版等主要系统的字符集都是以Big5为基准,但厂商又各自增加不同的造字与造字区,派生成多种不同版本。2003年,Big5被收录到CNS11643中文标准交换码的附录当中,获取了较正式的地位。这个最新版本被称为Big5-2003。

1.3 ANSI OEM/MBCS/DBCS/Code Page

1.4 Unicode/UTF-8/UTF-16/UTF-32

(1) Unicode

维基百科通用字符集

Unicode (Universal Code),统一码,是计算机科学领域里的一项业界标准。Unicode 的学名是“Universal Multiple-Octet Coded Character Set”,简称为UCS。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。

unicode.org

Unicode是为了解决传统的字符编码方案的局限而产生的,例如ISO 8859-1所定义的字符虽然在不同的国家中广泛地使用,可是在不同国家间却经常出现不兼容的情况。很多传统的编码方式都有一个共同的问题,即容许电脑处理双语环境(通常使用拉丁字母以及其本地语言),但却无法同时支持多语言环境(指可同时处理多种语言混合的情况)。

目前,几乎所有电脑系统都支持基本拉丁字母,并各自支持不同的其他编码方式。Unicode为了和它们相互兼容,其首256字符保留给ISO 8859-1所定义的字符,使既有的西欧语系文字的转换不需特别考量;并且把大量相同的字符重复编到不同的字符码中去,使得旧有纷杂的编码方式得以和Unicode编码间互相直接转换,而不会丢失任何信息。举例来说,全角格式区块包含了主要的拉丁字母的全角格式,在中文、日文、以及韩文字形当中,这些字符以全角的方式来呈现,而不以常见的半角形式显示,这对竖排文字和等宽排列文字有重要作用。

在表示一个Unicode的字符时,通常会用"U+"然后紧接着一组十六进制的数字来表示这一个字符。在基本多文种平面(英文:Basic Multilingual Plane,简写BMP。又称为“零号平面”、plane 0)里的所有字符,要用四个数字(即两个char,16bit ,例如U+4AE0,共支持六万多个字符);在零号平面以外的字符则需要使用五个或六个数字。

Unicode 有两种格式:UCS-2UCS-4。UCS-2 就是用两个字节编码,一共 16 个比特位,这样理论上最多可以表示 65536个字符,不过要表示全世界所有的字符显然 65536 个数字还远远不够,因为光汉字就有近 10 万个,因此 Unicode 4.0 规范定义了一组附加的字符编码,UCS-4 就是用 4 个字节(实际上只用了 31 位,最高位必须为 0)。

问题:

Unicode 理论上完全可以涵盖一切语言所用的符号。世界上任何一个字符都可以用一个 Unicode 编码来表示,一旦字符的 Unicode 编码确定下来后,就不会再改变了。但是 Unicode 有一定的局限性:

  • 第一个问题是,一个 Unicode 字符在网络上传输或者最终存储起来的时候,并不见得每个字符都需要两个字节,比如一字符“A“,用一个字节就可以表示的字符,偏偏还要用两个字节,显然太浪费空间了。
  • 第二个问题是,一个 Unicode 字符保存到计算机里面时就是一串 01 数字,那么计算机怎么知道一个 2 字节的 Unicode 字符是表示一个 2 字节的字符呢,还是表示两个 1 字节的字符呢,如果你不事先告诉计算机,那么计算机也会懵逼了。Unicode 只是规定如何编码,并没有规定如何传输、保存这个编码。例如“”的 Unicode 编码是6C49,我可以用 4 个 ASCII 数字来传输、保存这个编码;也可以用 UTF-8 编码的 3 个连续的字节 E6 B1 89来表示它。关键在于通信双方都要认可。因此 Unicode 编码有不同的实现方式,比如:UTF-8、UTF-16 等等。这里的 Unicode 就像英语一样,做为国与国之间交流世界通用的标准,每个国家有自己的语言,他们把标准的英文文档翻译成自己国家的文字,这是实现方式,就像 UTF-8。

注意:

  • ASCII、GB2312、GBK到GB18030的编码是向下兼容的,而Unicode只与ASCII兼容(更准确地是与ISO/IEC 8859-1兼容),与GB码不兼容。例如“汉”的Unicode编码是6C49,而GB码是BABA。
  • Unicode 只是规定如何编码,并没有规定如何传输、保存这个编码。

(2) UTF-8

维基百科

UTF-8 (8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,UTF-8是Unicode的实现方式之一。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。

  • 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
  • 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。
  • 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。
  • 其他极少使用的Unicode 辅助平面的字符使用四至六字节编码(Unicode范围由U+10000至U+1FFFFF使用四字节,Unicode范围由U+200000至U+3FFFFFF使用五字节,Unicode范围由U+4000000至U+7FFFFFFF使用六字节)。

对上述提及的第四种字符而言,UTF-8使用四至六个字节来编码似乎太耗费资源了。但UTF-8对所有常用的字符都可以用三个字节表示,而且它的另一种选择,UTF-16编码,对前述的第四种字符同样需要四个字节来编码,所以要决定UTF-8或UTF-16哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用Unicode标准压缩格式(SCSU)。

互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。

UTF-8的编码方式:

为了与以前的ASCII码兼容(ASCII为一个字节),因此UTF-8选择了使用可变长度字节来存储Unicode。

  • 在ASCII码的范围,用一个字节表示,超出ASCII码的范围就用字节表示,这就形成了我们上面看到的UTF-8的表示方法,这样的好处是当UNICODE文件中只有ASCII码时,存储的文件都为一个字节,所以就是普通的ASCII文件无异,读取的时候也是如此,所以能与以前的ASCII文件兼容。
  • 大于ASCII码的,就会由上面的第一字节的前几位表示该unicode字符的长度,比如110xxxxx前三位的二进制表示告诉我们这是个2BYTE的UNICODE字符;1110xxxx是个三位的UNICODE字符,依此类推;xxx的位置由字符编码数的二进制表示的位填入。
  • 只用最短的那个足够表达一个字符编码数的多字节串。
  • 注意在多字节串中,第一个字节的开头"1"的数目就是整个串中字节的数目。

ASCII字母继续使用1字节存储,重音文字、希腊字母或西里尔字母等使用2字节来存储,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。

以中文“”为例,它的Unicode是6C49,对应的区间是0000 0800--0000 FFFF,因此它用 UTF-8 表示时需要用 3 个字节来存储,6C49用二进制表示是0110 1100 0100 1001,从最后一个二进制位开始,依次从后向前填充到1110xxxx 10xxxxxx 10xxxxxx的x。得到11100110 10110001 1000 1001,转换成16进制为E6 B1 89,因此“”的UTF-8编码是“E6 B1 89”。

(3) UTF-16UTF-32

什么是BOM?

BOM (byte-order mark),字节顺序标记,是位于码点U+FEFF的统一码字符的名称。当以UTF-16或UTF-32来将UCS/统一码字符所组成的字符串编码时,这个字符被用来标示其字节序。它常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的记号。

为什么需要BOM?例如“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。

  • BE(big endian)大端序,或称大尾序
  • LE(little endian)小端序,或称小尾序

在UTF-8文件的开首,很多时都放置一个EF,BB,BF字符(它是U+FEFF转换成UTF-8后的编码),UTF-8不需要BOM来表明字节顺序,但可以凭此表明这个文本文件是以UTF-8编码。

1.5 实验

环境:

  • Windows 10 中文版
  • Notepad.exe "记事本"
  • UltraEdit 编辑器

步骤:

打开"记事本"程序Notepad.exe,新建一个文本文件,内容就是一个中文字符"",依次采用ANSIUnicodeUnicode big endianUTF-8编码方式保存。然后,用文本编辑软件UltraEdit中的"十六进制模式",观察该文件的内部编码方式。

1.ANSI: 将文本保存为操作系统默认编码格式,Windows简体中文版的Code Page是ANSI/OEM - 简体中文 GBK,Windows繁体中文版则是Big5

查看文本文件真正存储到硬盘上时,十六进制为BA BA,即""的GBK编码,说明GBK是两个字节

2.Unicode : 将文本保存为UCS-2编码方式,即两个字节的Unicode码,是Unicode little endianFF FE

查看文本文件真正存储到硬盘上时,十六进制为FF FE 49 6CFF FE表明是小端序方式存储,顺序是49 6C

3.Unicode big endian : 将文本保存为UCS-2编码方式,即两个字节的Unicode码,是Unicode big endianFE FF

查看文本文件真正存储到硬盘上时,十六进制为FE FF 6C 49FE FF表明是大端序方式存储,顺序是6C 49

4.UTF-8

编码是六个字节EF BB BF E6 B1 89,前三个字节EF BB BF表示这是UTF-8编码,后三个E6 B1 89就是""的UTF-8编码,它的存储顺序与编码顺序是一致的。

2. Python编码与转换

计算机只认识01,任何输入输出都是字节流,应用程序(包括操作系统)按照某一编码格式将字符图形数据,编码成字节流进行存储或传输。如果编码和解码两个步骤所使用的编码格式不一致,就会出现乱码。

例如,用Notepad.exe分别以gbkutf-8编码格式保存文本,如果用CMD去查看这两个文本内容,则gbk格式的正常显示中文,而utf-8格式文本会乱码,原因是CMD默认编码是cp936:

将这两个文本上传到CentOS,查看文本内容,则gbk格式乱码,而utf-8正常显示,因为CentOS默认编码为utf-8:

2.1 Python 2.x

(1) Terminal

通过CMD、Linux模拟终端或xshell等终端输入,由于用户实际输入的是人类可读的字符,终端程序会自动将字符按它的默认编码格式,转换为字节流送给Python 2.x解释器。

结论:

  • 终端输入,字节流由终端的编码格式决定,与sys.stdin.encoding无关
  • 终端输出,字节流由终端的编码格式决定,与sys.stdout.encoding无关
  • 如果输入输出的是unicode对象,输入的字节流由终端编码后,再由sys.stdin.encoding解码成unicode对象,在python内作其它运算处理,输出时,unicode对象先由sys.stdout.encoding编码成字节流,再由终端的编码格式显示给用户

str类型

如果定义的是str类型对象,Python直接将接收到的字节流存入内存。使用print关键字(Python 2.x是关键字,不能用help(print)查看帮助文档;Python 3.x是函数)输出对象时,直接将字节流输出给终端,终端再根据自己的默认编码解码显示为字符。

Windows中文版:

通过CMD运行Python 2.7解释器,默认编码是ANSI/OEM 简体中文 GBK,ASCII范围内的字符原样显示,超过这个范围(如中文字符)则用\x**十六进制表示

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation。保留所有权利。

C:\Users\wangy>C:\Python27\python.exe
Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:42:59) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> a1 = 'abc'
>>> a2 = '你好'
>>> a1
'abc'
>>> a2
'\xc4\xe3\xba\xc3'  # gbk编码
>>> type(a1)
<type 'str'>
>>> type(a2)
<type 'str'>
>>> len(a1)
3
>>> len(a2)
4
>>> print a1
abc
>>> print a2       # 终端按gbk编码格式,解码显示'\xc4\xe3\xba\xc3'
你好
>>> print '\xc4\xe3\xba\xc3'
你好

Linux:

通过xshell远程连接(设置默认编码与Linux终端一样为utf-8

[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> a1 = 'abc'
>>> a2 = '你好'
>>> a1
'abc'
>>> a2
'\xe4\xbd\xa0\xe5\xa5\xbd'  # utf-8编码
>>> type(a1)
<type 'str'>
>>> type(a2)
<type 'str'>
>>> len(a1)
3
>>> len(a2)
6
>>> print a1
abc
>>> print a2               # 终端按utf-8编码格式,解码显示'\xe4\xbd\xa0\xe5\xa5\xbd'
你好
>>> print '\xe4\xbd\xa0\xe5\xa5\xbd'
你好

>>> print a1
abc
>>> print a2               # xshell终端调成gbk编码格式,解码显示'\xe4\xbd\xa0\xe5\xa5\xbd'乱码
浣犲ソ

注意: 字节序列str对象使用len()函数,返回的结果是字节个数

unicode类型

如果定义的是unicode类型对象,Python需要先将接收到的字节流按照sys.stdin.encoding解码 decode转换为unicode,再存入内存。使用print关键字输出unicode类型对象时,先按照sys.stdout.encoding编码 encode转换为字节流,然后输出给终端,终端再根据自己的默认编码解码显示为字符。

说明1: 修改环境变量LANG会同时影响locale.getpreferredencoding()sys.stdin.encodingsys.stdout.encoding 的默认编码值

[root@CentOS ~]# echo $LANG
en_US.UTF-8
[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.getdefaultlocale()
('en_US', 'UTF-8')
>>> locale.getpreferredencoding()
'UTF-8'
>>> import sys
>>> sys.stdin.encoding
'UTF-8'
>>> sys.stdout.encoding
'UTF-8'
>>> sys.stderr.encoding
'UTF-8'
>>> sys.getdefaultencoding()
'ascii'
>>> sys.getfilesystemencoding()
'UTF-8'
>>>
[root@CentOS ~]# export LANG=zh_CN.GBK
[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.getdefaultlocale()
('zh_CN', 'gbk')
>>> locale.getpreferredencoding()
'GBK'
>>> import sys
>>> sys.stdin.encoding
'GBK'
>>> sys.stdout.encoding
'GBK'
>>> sys.stderr.encoding
'GBK'
>>> sys.getdefaultencoding()
'ascii'
>>> sys.getfilesystemencoding()
'GBK'
>>> 

说明2: 可以通过设置PYTHONIOENCODING只修改sys.stdin.encodingsys.stdout.encoding 的值

[root@CentOS ~]# export PYTHONIOENCODING='GBK'
[root@CentOS ~]# echo $LANG
en_US.UTF-8
[root@CentOS ~]# echo $PYTHONIOENCODING
GBK
[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.getdefaultlocale()
('en_US', 'UTF-8')
>>> locale.getpreferredencoding()
'UTF-8'
>>> import sys
>>> sys.stdin.encoding
'GBK'
>>> sys.stdout.encoding
'GBK'
>>> sys.stderr.encoding
'GBK'
>>> sys.getdefaultencoding()
'ascii'
>>> sys.getfilesystemencoding()
'UTF-8'
>>> 

说明3: 如果sys.stdin.encodingsys.stdout.encoding 值为None,unicode对象的解码和编码操作才会使用sys.getdefaultencoding(),比如管道或Sublime Text调试

[root@CentOS ~]# python -c "import sys; print sys.stdout.encoding; u = u'你好'; print u" |
 tee /tmp/stdout.txtTraceback (most recent call last):
  File "<string>", line 1, in <module>
None
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in 
range(128)[root@CentOS ~]# cat /tmp/stdout.txt
None

Windows中文版:

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation。保留所有权利。

C:\Users\wangy>C:\Python27\python.exe
Python 2.7.13 (v2.7.13:a06454b1afa1, Dec 17 2016, 20:42:59) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.stdin.encoding
'cp936'
>>> sys.stdout.encoding
'cp936'
>>> sys.stderr.encoding
'cp936'
>>> a1 = u'abc'
>>> a2 = u'你好'
>>> a1
u'abc'
>>> a2
u'\u4f60\u597d'  # '你好'先按cmd默认编码gbk变成字节流'\xc4\xe3\xba\xc3'输入,然后字节流再按gbk解码成unicode对象
>>> '你好'
'\xc4\xe3\xba\xc3'
>>> '\xc4\xe3\xba\xc3'.decode('gbk')
u'\u4f60\u597d'
>>> type(a1)
<type 'unicode'>
>>> type(a2)
<type 'unicode'>
>>> len(a1)
3
>>> len(a2)
2
>>> print a1
abc
>>> print a2  # u'\u4f60\u597d'先按gbk编码成字节流'\xc4\xe3\xba\xc3',再送给终端,终端按gbk编码格式解码显示
你好
>>> u'\u4f60\u597d'.encode('gbk')
'\xc4\xe3\xba\xc3'
>>> print '\xc4\xe3\xba\xc3'
你好

Linux:

[root@CentOS ~]# echo $LANG
en_US.UTF-8
[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.stdin.encoding
'UTF-8'
>>> sys.stdout.encoding
'UTF-8'
>>> sys.stderr.encoding
'UTF-8'
>>> a1 = u'abc'
>>> a2 = u'你好'
>>> a1
u'abc'
>>> a2
u'\u4f60\u597d'  # '你好'先按Linux默认编码utf-8变成字节流'\xe4\xbd\xa0\xe5\xa5\xbd'输入,然后字节流再按utf-8解码成unicode对象
>>> '你好'
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> '\xe4\xbd\xa0\xe5\xa5\xbd'.decode('utf-8')
u'\u4f60\u597d'
>>> type(a1)
<type 'unicode'>
>>> type(a2)
<type 'unicode'>
>>> len(a1)
3
>>> len(a2)
2
>>> print a1
abc
>>> print a2  # u'\u4f60\u597d'先按utf-8编码成字节流'\xe4\xbd\xa0\xe5\xa5\xbd',再送给终端,终端按utf-8编码格式解码显示
你好
>>> u'\u4f60\u597d'.encode('utf-8')
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> print '\xe4\xbd\xa0\xe5\xa5\xbd'
你好

>>> print a2  # xshell终端调成gbk编码格式,解码显示'\xe4\xbd\xa0\xe5\xa5\xbd'乱码
浣犲ソ
[root@CentOS ~]# export PYTHONIOENCODING='GBK'
[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.stdin.encoding
'GBK'
>>> sys.stdout.encoding
'GBK'
>>> sys.stderr.encoding
'GBK'
>>> a1 = u'abc'
>>> a2 = u'你好'
>>> a1
u'abc'
>>> a2
u'\u6d63\u72b2\u30bd'  # '你好'先按Linux默认编码utf-8变成字节流'\xe4\xbd\xa0\xe5\xa5\xbd'输入,然后字节流再按gbk解码成unicode对象
>>> '你好'
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> '\xe4\xbd\xa0\xe5\xa5\xbd'.decode('gbk')
u'\u6d63\u72b2\u30bd'
>>> type(a1)
<type 'unicode'>
>>> type(a2)
<type 'unicode'>
>>> len(a1)
3
>>> len(a2)
3
>>> print a1
abc
>>> print a2  # u'\u6d63\u72b2\u30bd'先按gbk编码成字节流'\xe4\xbd\xa0\xe5\xa5\xbd',再送给终端,终端按utf-8编码格式解码显示
你好
>>> u'\u6d63\u72b2\u30bd'.encode('gbk')
'\xe4\xbd\xa0\xe5\xa5\xbd'
>>> print '\xe4\xbd\xa0\xe5\xa5\xbd'
你好

>>> print a2  # xshell终端调成gbk编码格式,解码显示'\xe4\xbd\xa0\xe5\xa5\xbd'乱码
浣犲ソ

注意: unicode对象使用len()函数,返回的结果是字符个数

(2) File

结论:

  • 如果源代码中包含了非ASCII字符,需要指定编码格式。如果源代码中不使用unicode对象,指定的编码格式正不正确都没有影响,如果源代码中使用了unicode对象,必须指定正确的编码格式(文本文件保存时的编码格式)。
  • 源代码字节流输入后,如果要创建unicode对象,先用源代码中指定的编码格式解码字节流成unicode对象,输出时先用sys.stdout.encoding编码成字节流
  • 为保证同一源代码在各平台执行结果一致,建议文本文件保存时选择utf-8格式,同时所有的字符串定义为unicode类型,输入输出会自动解码编码,这样就不会出现Linux平台中文正常而Windows平台乱码的情况。

str类型

读取Python源代码文件时,首先在语法校验期间(compilation):

  • 如果源代码中没有指定编码格式,则使用Python 2.x默认编码格式ASCII去解码字节流。如果源代码中包含了非ASCII字符,则会报语法错误
[root@CentOS ~]# cat UTF-8.py  # 该文本文件保存时,编码格式为utf-8
a1 = 'abc'
a2 = '你好'

print repr(a1)
print a1

print repr(a2)
print a2

[root@CentOS ~]# cat GBK.py  # 该文本文件保存时,编码格式为gbk
a1 = u'abc'
a2 = u'ţºg

print repr(a1)
print a1

print repr(a2)
print a2

[root@CentOS ~]# python UTF-8.py 
File "UTF-8.py", line 2
SyntaxError: Non-ASCII character '\xe4' in file UTF-8.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

[root@CentOS ~]# python GBK.py
File "GBK.py", line 2
SyntaxError: Non-ASCII character '\xc4' in file GBK.py on line 2, but no encoding declared
; see http://www.python.org/peps/pep-0263.html for details
[root@CentOS ~]# python
Python 2.7.5 (default, Nov 20 2015, 02:00:19) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> 
  • 如果源代码中指定了编码格式,则使用指定的编码格式去解码字节流。输出字节流后,由执行python的终端按其默认编码显示字符。

说明: 参考pep-0263文档,# -*- coding: utf-8 -*-字符串中的-*-纯粹是为了格式美观而已

# 1. xshell为UTF-8编码时
[root@CentOS ~]# cat UTF-8.py 
# -*- coding: utf-8 -*-

a1 = 'abc'
a2 = '你好'

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# cat GBK.py 
# -*- coding: gbk -*-

a1 = 'abc'
a2 = 'ţºg

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# python UTF-8.py 
'abc'
abc
'\xe4\xbd\xa0\xe5\xa5\xbd'
你好
[root@CentOS ~]# python GBK.py 
'abc'
abc
'\xc4\xe3\xba\xc3'
ţº
[root@CentOS ~]# 

# 2. xshell为GBK编码时
[root@CentOS ~]# cat UTF-8.py 
# -*- coding: utf-8 -*-

a1 = 'abc'
a2 = '浣犲ソ'

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# cat GBK.py 
# -*- coding: gbk -*-

a1 = 'abc'
a2 = '你好'

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# python UTF-8.py 
'abc'
abc
'\xe4\xbd\xa0\xe5\xa5\xbd'
浣犲ソ
[root@CentOS ~]# python GBK.py 
'abc'
abc
'\xc4\xe3\xba\xc3'
你好
[root@CentOS ~]# 

语法检验通过后,str类型对象按原字节流输出给终端,终端再根据自己的默认编码解码显示为字符。

unicode类型

语法校验期间与上面一样,只是碰到unicode对象需要先将字节流按照源代码中指定的coding: xx编码格式解码 decode转换为unicode,再存入内存。使用print关键字输出unicode类型对象时,先按照sys.stdout.encoding编码 encode转换为字节流,然后输出给终端,终端再根据自己的默认编码解码显示为字符。

# 1. coding: xx指定的编码与文件保存时选定的编码一致时
[root@CentOS ~]# cat UTF-8.py
# -*- coding: utf-8 -*-

a1 = u'abc'
a2 = u'你好'

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# cat GBK.py
# -*- coding: gbk -*-

a1 = u'abc'
a2 = u'ţºg

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# python UTF-8.py
u'abc'
abc
u'\u4f60\u597d'
你好
[root@CentOS ~]# python GBK.py
u'abc'
abc
u'\u4f60\u597d'
你好

# 2. coding: xx指定的编码与文件保存时选定的编码不一致时
[root@CentOS ~]# cat UTF-8.py
# -*- coding: gbk -*-

a1 = u'abc'
a2 = u'你好'

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# cat GBK.py
# -*- coding: utf-8 -*-

a1 = u'abc'
a2 = u'ţºg

print repr(a1)
print a1

print repr(a2)
print a2
[root@CentOS ~]# python UTF-8.py
u'abc'
abc
u'\u6d63\u72b2\u30bd'
浣犲ソ
[root@CentOS ~]# python GBK.py
  File "GBK.py", line 4
    a2 = u'ţºg
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xc4 in position 0: invalid continuation byte

str类型与unicode类型执行连接操作%格式化输出时,str默认会先解码为unicode:

[root@CentOS ~]# cat UTF-8.py
# -*- coding: utf-8 -*-

a1 = '你好'
a2 = u'你好'

print repr(a1)
print a1

print repr(a2)
print a2

print a1 + a2
[root@CentOS ~]# python UTF-8.py
'\xe4\xbd\xa0\xe5\xa5\xbd'
你好
u'\u4f60\u597d'
你好
Traceback (most recent call last):
  File "UTF-8.py", line 12, in <module>
    print a1 + a2
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)

2.2 Python 3.x

Python 3.x中str类型(对应于Python 2.x的unicode类型)默认就是Unicode编码格式,由Python自动将字节流解码而成,字节流由bytes类型保存(对应于Python 2.x的str类型)。

str类型(Unicode)使用encode()方法编码成bytes字节流类型(以b'xxx'形式保存),bytes使用decode()方法解码成str

Python 3.x中默认编码格式是UTF-8,如果源文件保存时选择的编码不是UTF-8,也没有在源文件中指定对应的编码格式则会报错:

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation。保留所有权利。

C:\Users\wangy>type C:\Users\wangy\Desktop\GBK.py
a1 = 'abc'
a2 = '你好'

print(repr(a1))
print(a1)

print(repr(a2))
print(a2)

C:\Users\wangy>D:\python35\python.exe C:\Users\wangy\Desktop\GBK.py
  File "C:\Users\wangy\Desktop\GBK.py", line 2
SyntaxError: Non-UTF-8 code starting with '\xc4' in file C:\Users\wangy\Desktop\GBK.py on line 2, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

如果指定了错误的编码格式,由于str类型默认就是由字节流按照源文件中指定的编码格式decode 解码变成Unicode的,当源文件保存时的编码格式与coding: xx不一致时会报错:

C:\Users\wangy>type C:\Users\wangy\Desktop\GBK.py  # 保存格式为GBK
# -*- coding: utf-8 -*-

a1 = 'abc'
a2 = '你好'

print(repr(a1))
print(a1)

print(repr(a2))
print(a2)

C:\Users\wangy>D:\python35\python.exe C:\Users\wangy\Desktop\GBK.py
  File "C:\Users\wangy\Desktop\GBK.py", line 4
SyntaxError: (unicode error) 'utf-8' codec can't decode byte 0xc4 in position 0: invalid continuation byte

建议:源代码文件保存格式选择UTF-8(且要UTF-8 without BOM)

C:\Users\wangy>type C:\Users\wangy\Desktop\UTF-8.py
a1 = 'abc'
a2 = '浣犲ソ'

print(repr(a1))
print(a1)

print(repr(a2))
print(a2)

C:\Users\wangy>D:\python35\python.exe C:\Users\wangy\Desktop\UTF-8.py
'abc'
abc
'你好'
你好

上面用CMD查看UTF-8源代码内容时,中文会乱码,因为CMD默认编码是GBK。但是用python 3执行脚本后,正常输出中文字符,读取源代码时,str对象你好的字节流b'\xe4\xbd\xa0\xe5\xa5\xbd'sys.getdefaultencoding()值解码decode('utf-8')为Unicode值。输出时,str对象再由sys.stdout.encoding编码encode('cp936')b'\xc4\xe3\xba\xc3'

C:\Users\wangy>D:\python35\python.exe
Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 25 2016, 22:18:55) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.getdefaultlocale()
('zh_CN', 'cp936')
>>> locale.getpreferredencoding()
'GBK'
>>> import sys
>>> sys.stdin.encoding
'cp936'
>>> sys.stdout.encoding
'cp936'
>>> sys.stderr.encoding
'cp936'
>>> sys.getdefaultencoding()
'utf-8'
>>> sys.getfilesystemencoding()
'mbcs'
>>>

Python 3.x明确区分strbytes,不能混用,必须同类型才能执行像连接操作等:

C:\Users\wangy>type C:\Users\wangy\Desktop\UTF-8.py
# -*- coding: utf-8 -*-

a1 = b'abc'
a2 = '浣犲ソ'

print(repr(a1))
print(a1)

print(repr(a2))
print(a2)

print(a1 + a2)

C:\Users\wangy>D:\python35\python.exe C:\Users\wangy\Desktop\UTF-8.py
b'abc'
b'abc'
'你好'
你好
Traceback (most recent call last):
  File "C:\Users\wangy\Desktop\UTF-8.py", line 12, in <module>
    print(a1 + a2)
TypeError: can't concat bytes to str

参考:

未经允许不得转载: LIFE & SHARE - 王颜公子 » 深入研究字符编码,告别Python乱码

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录