python语言中的编码问题
在编程的过程当中,常常会遇到莫名其妙的乱码问题。很多人选择出了问题直接在网上找答案,把别人的例子照搬过来,这是快速解决问题的一个好办法。然而,作为一个严谨求实的开发者,如果不从源头上彻底理解乱码产生的机制,并由此寻求解决问题的根本路径,那么永远不能从码农的阴影中摆脱出来。下面就来一起了解一下计算机编码问题的来龙去脉。
ASCII
众所周知,计算机中的所有数据,不论是文字、图片、视频、还是音频文件,本质上最终都是按照类似 01010101 的二进制形式存储的。然而,计算机中的字符,并不能完全以这种方式来表示。由于计算机最初是由美国人发明的,因而最初的计算机编码使用的也是美国人的标准,即ASCII( American Standard Code for Information Interchange,美国信息交换标准代码)。ASCII码一共规定了128个字符的编码,比如大写的字母A是65(二进制01000001),符号@的编码是64(二进制01000000)。这128个符号中, 0~31及127(共33个)是控制字符或通信字符,32–126 分配给了能在键盘上找到并且能打印出来的字符。所有ASCII编码表示的内容,只占用了一个字节的后面7位,最高位统一规定为0。
后来为了能够表示欧洲地区除了英文字母以外的其它字母,出现了扩展的ASCII编码。 扩展的ASCII包含原有的128个字符,又增加了128个字符,总共是256个。编码时最高位为1,这样就可以与ASCII码完全兼容。可以表示诸如音标æ(编码145,二进制10010001)以及法语中的字母é(编码为130,二进制10000010)等字符。
这个编码能表示音标和欧洲大多数非英语系字母,但是它并不是国际标准,在不同的国家, 128 到 255对应的字符并不完全相同,这就产生了各种不同的扩展ASCII编码。比如 ISO8859-1 字符集,也就是 Latin-1,加入了西欧常用字符,包括德法两国的字母。ISO8859-2 字符集,也称为 Latin-2,收集了东欧字符。 ISO8859-3 字符集,也称为 Latin-3,收集了南欧字符,等等。
这样的编码方式够吗?显然不够,比如汉字,就无法用ASCII表示。扩展的ASCII 也远远不够。
GBK
中国人为了能够正常使用计算机这一伟大方明,做出了多方面的努力。GB2312就是这一努力的成果, 该标准于1980年发布,1981年5月1日开始实施。它标志着我国在使用电子计算机方面迈出了重要的一步。GB2312 编码共收录了6763个汉字,同时还兼容 ASCII。这一字符编码基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率,对一些古汉语和繁体字 GB2312 没法处理。后来就在GB2312的基础上创建了一种叫 GBK 的编码,于1995年正式发布。GBK 不仅收录了GB 2312 中的全部汉字、非汉字符号,同时还收录了日韩语中出现的汉字,如韩国著名围棋手李世乭中的乭 GBK编码是0x8168(0x表示16进制)。这里可以查询汉字对应的GBK编码。
GBK编码一般用两个字节表示一个字符,如果是英文字母,则使用一个字符,与ASCII编码相同,因此,GBK 也是兼容 ASCII 编码的,但并不与任何扩展的ASCII编码兼容。这可以从它的编码序列看出来。
GBK 采用双字节表示,总体编码范围为 0x8140-0xFEFE(1000000101000000-1111111011111110),首字节在 0x81-0xFE 之间,尾字节在 0x40-0xFE之间。可以看出首字节最高位都为1,这样一来,如果尾字节后的字节最高位为0,那么就可以解析为一个ASCII编码字符,否则就是一个连续的二字节字符。
Unicode
世界上存在着多种语言,有没有一种编码方式能够囊括所有语言中的字符呢?答案是有。Unicode编码正是为了满足这种需求制定的。Unicode是一个很大的集合,目前的规模可以容纳100多万个符号。每个符号的编码都不一样,这么多的字符,想要以二进制形式表示,就需要比较多的字节才能够一一对应。标准的Unicode采用4个字节表示一个字符串。这个四字节的二进制代码,称为这个字符的码点。比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+ 4E6D表示汉字"乭 "。访问unicode.org可以查询具体的符号对应表。
使用4个字节表示一个字符的方法显然不够科学,因为很多英文字母只需要一个字节就可以表示了,偏要用四个字节表示就会造成很大的浪费。于是就出现了UTF-8 编码。
Unicode只是规定了字符如何编码,并没有规定如何存储和传输。 UTF-8编码就是Unicode编码的一种实现方式,它规定可以使用1~4个字节表示一个字符,根据所要表现的字符不同而变化字节长度,英文字母就用1个字节表示,汉字就用2-3个字节表示。
那么问题来了,由于计算机中的字符串是连续的0101的编码,如何既能够表示一个字符在Unicode编码表中的码点,又能够让计算机明白这个连续编码串中的一个字节就是一个英文字母,而不与他前面的编码串构成两个或三个字节表示的字符。UTF-8 的编码的设计者巧妙的解决了这个问题。
英文字符这些原本就可以用ASCII码表示的字符用UTF-8表示时就只需要一个字节的空间,和ASCII是一样的。对于多字节(n个字节)的字符,第一个字节的前n为都设为1,第n+1位设为0,后面字节的前两位都设为10。剩下的二进制位全部用该字符的Unicode码填充。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
-----------------------+---------------------------------------------
0000 0000~0000 007F | 0xxxxxxx
0000 0080~0000 07FF | 110xxxxx 10xxxxxx
0000 0800~0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000~0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
这样的编码方式很好理解,如果一个字节当中第一位是0,那么这个字节就对应着一个字符,如果第一位是1,那么看他后面连续有多少个1,就表示这个字符占用了多少个字节。例如,“我”的Unicode码点是0x6211,二进制110001000010001,落在第三行的范围内(0000 0800~0000 FFFF),因此"我"需要三个字节,格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"我"的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了"我"的UTF-8编码是"11100110 10001000 10010001 ",转换成十六进制就是E68891,这才是最终存储在计算机中的二进制编码。
这里指出一个误区,网络上有很多在线utf8编码转换工具,声称可以把汉字转换成UTF-8 编码,其实大多数工具只是把汉字转换成了与之对应的unicode码点,并不是真正在存储和传输过程中的utf-8编码。这里可以查询汉字对应的utf-8编码和unicode编码,可以看出这两者是不同的。
除了UTF-8之外,Unicode的实现方式还有UTF-16 ,UTF-32 。 UTF-16 使用2~4个字节表示一个字符,UTF-32 则使用标准的4个字节表示一个字符,与其Unicode码点一一对应。无论采用哪种表现形式,同一字符所对应的Unicode码点都是一样的,只不过在存储和传输的时候,把码点做了不同的转换。
PYTHON字符编码
下面开始讲讲Python中的编码问题。
Python的默认编码是ASCII,这跟它的诞生背景有关,Python的诞生时间是1989年,Unicode于 1994年才正式公布,在Python诞生之初并无Unicode可用,只能选择ASCII。后来做了多方改进,才使得它适用于非英语系的用户。
如果不做修改,Python将使用ASCII为所有代码编码,包括注释。
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
在编写python代码时如果不指定文件的编码方式,将默认使用ASCII编码。所以如果在代码中出现中文,将会报错
#stringtest.py
print '你好'
C:\Python27\python.exe D:/MyGit/demo/test/test.py
File "D:/MyGit/demo/test/test.py", line 1
SyntaxError: Non-ASCII character '\xe4' in file D:/MyGit/demo/test/test.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
如果想在代码中使用中文,则一定要在代码开头(第一行或第二行)声明此文件的编码方式,比如编码方式设为UTF-8
# -*- coding: utf-8 -*-
或者
#!/usr/bin/python
# -*- coding: utf-8 -*-
其中第一行注释表示这是一个可以在Unix/Linux/Mac直接运行的程序,Windows系统会忽略这个注释。
这样,在代码中就可以使用中文了。
(完)