ASCII
每个做 JavaWeb 开发的新手都会遇到乱码问题,每个做 Python 爬虫的新手都会遇到编码问题,为什么编码问题那么蛋疼呢?
这个问题要从 1992 年 Guido van Rossum 创造 Python 这门语言说起,那时的 Guido 绝对没想到的是 Python 这门语言在今天会如此受大家欢迎,也不会想到计算机发展速度会如此惊人。 Guido 在当初设计这门语言时是不需要关心编码的,因为在英语世界里,字元的个数非常有限,26 个字母(大小写)、 10 个数字、标点符号、控制符,也就是键盘上所有的键所对应的字元加起来也不过是一百多个字元而已。这在计算机中用一个位元组的储存空间来表示一个字元是绰绰有余的,因为一个位元组相当于 8 个位元位,8 个位元位可以表示 256 个符号。于是聪明的美国人就制定了一套字元编码的标准叫 ASCII(American Standard Code for Information Interchange),每个字元都对应唯一的一个数字,比如字元 A 对应的二进位制数值是 01000001,对应的十进位制就是 65 。最开始 ASCII 只定义了 128 个字元编码,包括 96 个文字和 32 个控制符号,一共 128 个字元,只需要一个位元组的 7 位就能表示所有的字元,因此 ASCII 只使用了一个位元组的后 7 位,最高位都为 0 。每个字元与 ASCII 码的对应关系可检视网站 ascii-code 。

EASCII(ISO/8859-1)
然而计算机慢慢地普及到其他西欧地区时,他们发现还有很多西欧所特有的字元是 ASCII 编码表中没有的,于是后来出现了可扩充套件的 ASCII 叫 EASCII ,顾名思义,它是在 ASCII 的基础上扩充套件而来,把原来的 7 位扩充到 8 位,它完全相容 ASCII,扩充套件出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号。然而 EASCII 时代是一个混乱的时代,大家没有统一标准,他们各自把最高位按照自己的标准实现了自己的一套字元编码标准,比较著名的就有 CP437, CP437 是 Windows 系统中使用的字元编码,如下图:

cp437
另外一种被广泛使用的 EASCII 还有 ISO/8859-1(Latin-1),它是国际标准化组织(ISO)及国际电工委员会(IEC)联合制定的一系列 8 位元字符集的标准,ISO/8859-1 只继承了 CP437 字元编码的 128-159 之间的字元,所以它是从 160 开始定义的,不幸的是这些众多的 ASCII 扩充字集之间互不相容。

iso8859-1
GBK
随著时代的进步,计算机开始普及到千家万户,比尔盖茨让每个人桌面都有一台电脑的梦想得以实现。但是计算机进入中国不得不面临的一个问题就是字元编码,虽然咱们国家的汉字是人类使用频率最多的文字,汉字博大精深,常见的汉字就有成千上万,这已经大大超出了 ASCII 编码所能表示的字元范围了,即使是 EASCII 也显得杯水车薪,于是聪明的中国人自己弄了一套编码叫 GB2312,又称 GB0,1981 由中国国家标准总局释出。 GB2312 编码共收录了 6763 个汉字,同时它还相容 ASCII 。 GB2312 的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆 99.75% 的使用频率。不过 GB2312 还是不能 100% 满足中国汉字的需求,对一些罕见的字和繁体字 GB2312 没法处理,后来就在 GB2312 的基础上建立了一种叫 GBK 的编码。 GBK 不仅收录了 27484 个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。同样 GBK 也是相容 ASCII 编码的,对于英文字元用 1 个位元组来表示,汉字用两个位元组来标识。
Unicode
对于如何处理中国人自己的文字我们可以另立山头,按照我们自己的需求制定一套编码规范,但是计算机不止是美国人和中国人用啊,还有欧洲、亚洲其他国家的文字诸如日文、韩文全世界各地的文字加起来估计也有好几十万,这已经大大超出了 ASCII 码甚至 GBK 所能表示的范围了,况且人家为什么用采用你 GBK 标准呢?如此庞大的字元库究竟用什么方式来表示好呢?于是统一联盟国际组织提出了 Unicode 编码,Unicode 的学名是 “Universal Multiple-Octet Coded Character Set”,简称为 UCS 。
Unicode 有两种格式:UCS-2 和 UCS-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 。
UTF-8
UTF-8(Unicode Transformation Format)作为 Unicode 的一种实现方式,广泛应用于网际互联网,它是一种变长的字元编码,可以根据具体情况用 1-4 个位元组来表示一个字元。比如英文字元这些原本就可以用 ASCII 码表示的字元用 UTF-8 表示时就只需要一个位元组的空间,和 ASCII 是一样的。对于多位元组(n 个位元组)的字元,第一个位元组的前 n 为都设为 1,第 n+1 位设为 0,后面位元组的前两位都设为 10 。剩下的二进位制位全部用该字元的 UNICODE 码填充。

以汉字 “好” 为例,“好” 对应的 Unicode 是 597D,对应的区间是 0000 0800—0000 FFFF,因此它用 UTF-8 表示时需要用 3 个位元组来储存,597D 用二进位制表示是: 0101100101111101,填充到 1110xxxx 10xxxxxx 10xxxxxx 得到 11100101 10100101 10111101,转换成 16 进位制:E5A5BD,因此 “好” 的 Unicode “597D” 对应的 UTF-8 编码是 “E5A5BD” 。

中文 好
unicode 0101 100101 111101
编码规则 1110xxxx 10xxxxxx 10xxxxxx
————————–
utf-8 11100101 10100101 10111101
————————–
16 进位制 utf-8 e 5 a 5 b d

Python 字元编码
注:以下程式码和概念都是基于 Python 2.x 。
现在总算把理论说完了。再来说说 Python 中的编码问题。 Python 的诞生时间比 Unicode 要早很多,Python 的预设编码是 ASCII 。

>>> import sys
>>> sys.getdefaultencoding()
‘ascii’

所以在 Python 原始码档案中如果不显式地指定编码的话,将出现语法错误

#test.py
print “ 你好”

上面是 test.py 指令码,执行 python test.py 就会包如下错误:

File “test.py”, line 1 yntaxError: Non-ASCII character ‘xe4′ in file test.py on line 1, but no encoding declared; see http://www.python.org/ ps/pep-0263.html for details

为了在原始码中支援非 ASCII 字元,必须在原始档的第一行或者第二行显示地指定编码格式:

# coding=utf-8

或者是:

#!/usr/bin/python
# -*- coding: utf-8 -*-

在 Python 中和字串相关的资料型别,分别是 str 、 unicode 两种,他们都是 basestring 的子类,可见 str 与 unicode 是两种不同型别的字串物件。

basestring
/
/
str unicode

对于同一个汉字 “好”,用 str 表示时,它对应的就是 UTF-8 编码’xe5xa5xbd’,而用 Unicode 表示时,它对应的符号就是 u’u597d’,与 u” 好” 是等同的。需要补充一点的是,str 型别的字元其具体的编码格式是 UTF-8 还是 GBK,还是其它格式,根据操作系统相关。比如在 Windows 系统中,cmd 命令列中显示的:

# windows 终端
>>> a = ‘ 好’
>>> type(a)

>>> a
‘xbaxc3′

而在 Linux 系统的命令列中显示的是:

# linux 终端
>>> a=’ 好’
>>> type(a)

>>> a
‘xe5xa5xbd’

>>> b=u’ 好’
>>> type(b)

>>> b
u’u597d’

不论是 Python3x 、 Java 还是其他程式语言,Unicode 编码都成为了语言的预设编码格式,而资料最后储存到介质中的时候,不同的介质可有用不同的方式,有些人喜欢用 UTF-8,有些人喜欢用 GBK,这都无所谓,只要平台统一的编码规范,具体怎么实现并不关心。

encode
str 与 unicode 的转换
那么在 Python 中 str 和 unicode 之间是如何转换的呢?这两种型别的字串型别之间的转换就是靠这两个方法:decode 和 encode 。

py-encode

#从 str 型别转换到 unicode
s.decode(encoding) =====> to
#从 unicode 转换到 str
u.encode(encoding) =====> to

>>> c = b.encode(‘utf-8’)
>>> type(c)

>>> c
‘xe5xa5xbd’

>>> d = c.decode(‘utf-8’)
>>> type(d)

>>> d
u’u597d’

这个’xe5xa5xbd’ 就是 Unicode u’ 好’ 通过函式 encode 编码得到的 UTF-8 编码的 str 型别的字串。反之亦然,str 型别的 c 通过函式 decode 解码成 Unicode 字串 d 。
str(s) 与 unicode(s)
str(s) 和 unicode(s) 是两个工厂方法,分别返回 str 字串物件和 Unicode 字串物件,str(s) 是 s.encode(‘ascii’) 的简写。实验:

>>> s3 = u” 你好”
>>> s3
u’u4f60u597d’
>>> str(s3)
Traceback (most recent call last):
File ““, line 1, in
UnicodeEncodeError: ‘ascii’ codec can’t encode characters in position 0-1: ordinal not in range(128)

上面 s3 是 Unicode 型别的字串,str(s3) 相当于是执行 s3.encode(‘ascii’),因为 “你好” 两个汉字不能用 ASCII 码来表示,所以就报错了,指定正确的编码:s3.encode(‘gbk’) 或者 s3.encode(‘utf-8’) 就不会出现这个问题了。类似的 Unicode 有同样的错误:

>>> s4 = “ 你好”
>>> unicode(s4)
Traceback (most recent call last):
File ““, line 1, in
UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xc4 in position 0: ordinal not in range(128)
>>>

unicode(s4) 等效于 s4.decode(‘ascii’),因此要正确的转换就要正确指定其编码 s4.decode(‘gbk’) 或者 s4.decode(‘utf-8′) 。
乱码
所有出现乱码的原因都可以归结为字元经过不同编码解码在编码的过程中使用的编码格式不一致,比如:

# encoding: utf-8

>>> a=’ 好’
>>> a
‘xe5xa5xbd’
>>> b=a.decode(“utf-8”)
>>> b
u’u597d’
>>> c=b.encode(“gbk”)
>>> c
‘xbaxc3’
>>> print c
��

UTF-8 编码的字元 ‘好’ 占用 3 个位元组,解码成 Unicode 后,如果再用 GBK 来解码后,只有 2 个位元组的长度了,最后出现了乱码的问题,因此防止乱码的最好方式就是始终坚持使用同一种编码格式对字元进行编码和解码操作。

decode-encode
其他技巧
对于如 Unicode 形式的字串(str 型别):

s = ‘idu003d215903184u0026indexu003d0u0026stu003d52u0026sid’

转换成真正的 Unicode 需要使用:

s.decode(‘unicode-escape’)

测试:

>>> s = ‘idu003d215903184u0026indexu003d0u0026stu003d52u0026sidu003d95000u0026i’
>>> print(type(s))

>>> s = s.decode(‘unicode-escape’)
>>> s
u’id=215903184&index=0&st=52&sid=95000&i’
>>> print(type(s))

>>>