总会遇到乱码的问题,也总是按照网上的教程一步一步操作,解决的过程就像碰运气,从来没有总结过,再次遇到的时候还是不知道是什么问题,所以,花一点时间,总结一下。

字符集和字符编码

字符集是一个系统支持的抽象字符的集合。这个集合是有限集合,包括文字、标点和数字等,由于计算机内部全部都是数字存储,所以字符集本身也是一个符号与数字之间的映射关系,比如ASCII字符集中大写字母A的编号是65。存储或传输的时候计算机根据字符集将人能看懂的字符换算成计算机能看懂的数字,输出的时候再换算成人能看懂的字符。

字符编码,计算机系统内部全部采用二进制,那么将数字转换成二进制的时候采用什么规则呢,比如采用几位,高位代表什么,低位代表什么等,这就是所谓的编码规则。

乱码,计算机按照指定或默认的编码规则对bit位进行解码,由于与存储或接收时使用的编码规则不一致,就导致了翻译出来的字符不是原来的字符,造成所谓的乱码,即我们人类看不懂。

常用的字符集和字符编码

常见的字符集有ASCII,能表示128个字符,扩展ASCII能表示256个字符,GBXXX字符集是我国专家设计的一套字符集,还有包括繁体字的汉字字符集BIG5,这些字符集同时规定了编码规则,所以他们同时也是字符编码,需要说明的是,他们都是兼容ASCII编码的。

Unicode 和 UTF-8

最容易搞混的是Unicode和UTF-8了,因为像上面那样每个语言或者地区都搞一套,在互联网中普及非常不便,于是就出现了Unicode字符集,目前已经超过了十万字符,可以包括多种文字,这个字符集规定了符号系统到数字的映射,但是并没有规定统一的编码规则。

UTF-8,UTF-16,UTF-32都是Unicode的编码规则,其中UTF-32同意采用4个字节表示字符,空间浪费较大,所以不常见,UTF-8是一种变长的编码方式,因为文本的重点不在于关注编码规则的细节,所以就不再赘述,具体实现可以参见其他作者的文章。

所以,我们常见的GBXXX,ASCII,BIG5等本身即是指代字符集,也是字符编码,而Unicode只是字符集,UTF-8只是字符编码。

Python 中的编码

这里主要有三个问题:

  1. str和unicode类型有什么区别?
  2. 源文件开头的#coding:utf-8是做什么用的,跟源文件的编码有什么关系?
  3. 源文件的编码对程序有没有影响?
  4. file = open(‘xxx’),file.read()是怎样对文件进行解码的?

首先来看第一个问题,再Python2中,通过s1='str字符串'得到的是str对象,而s2=u'unicode字符串'得到的是unicode对象,这两种都是basestring类型的子类,从官方文档Unicode HOWTO中的解释

Python represents Unicode strings as either 16- or 32-bit integers, depending on how the Python interpreter was compiled.

可以看出,unicode字符串可以看做直接存储的是该字符对应的Unicode数字码,在PYTHON-进阶-编码处理小结一文中,还对比了用len()求两者长度的区别,其中,len('中文')得到的结果为6,而len(u'中文')得到的结果才是实际意义上的2。在Overcoming frustration: Correctly using unicode in python2中也解释了str类型和unicode类型的不同,str类型是实际上bytes序列,len(str)所得到的也是序列的长度,而不是实际意义上的字符串长度。

In python, the unicode type stores an abstract sequence of code points. Each code point represents a grapheme. By contrast, byte str stores a sequence of bytes which can then be mapped to a sequence of code points. Each unicode encoding (UTF-8, UTF-7, UTF-16, UTF-32, etc) maps different sequences of bytes to the unicode code points.

Python开发者通常会写#coding: utf-8或者类似的encoding hint在源文件的前两行(也只有在前两行才起作用,并且该编码必须兼容ASCII,UTF-16就不能正常工作)。 很多人明白的一点就是:如果源代码文件中出现了非ASCII字符集中的字符,我们需要写这样的注释。 但是对于像我这样的新手来说,常常会有一个问题,这句话的作用是什么,又和源文件本身的编码什么关系?

在Python2.1中,Unicode字符串只能采用”unicode-escape”的方式,比如需要定义”中文”两个字,须得s = u'\u4e2d\u6587',而不能直接出现s = u'中文'这样的代码,这就对使用非拉丁字符的开发者非常不友好,所以就有了PEP263,该网页中也解释了该lint的作用,即使用指定的编码将源代码中的字符串字面量转换成unicode,而这与源文件编码本身并没有直接关系

This PEP proposes to introduce a syntax to declare the encoding of a Python source file. The encoding information is then used by the Python parser to interpret the file using the given encoding. Most notably this enhances the interpretation of Unicode literals in the source code and makes it possible to write Unicode literals using e.g. UTF-8 directly in an Unicode aware editor.

In Python 2.1, Unicode literals can only be written using the Latin-1 based encoding “unicode-escape”. This makes the programming environment rather unfriendly to Python users who live and work in non-Latin-1 locales such as many of the Asian countries. Programmers can write their 8-bit strings using the favorite encoding, but are bound to the “unicode-escape” encoding for Unicode literals .

有的读者可能看到了,刚才的引用中明明提到了

The encoding information is then used by the Python parser to interpret the file using the given encoding.

怎么能说与源文件的编码无关呢?

这里就需要了解Python解释器工作的流程了,在PEP263中有说明:

Python’s tokenizer/compiler combo will need to be updated to work as follows:
A. read the file
B. decode it into Unicode assuming a fixed per-file encoding
C. convert it into a UTF-8 byte string
D. tokenize the UTF-8 content
E. compile it, creating Unicode objects from the given Unicode data and creating string objects from the Unicode literal data by first reencoding the UTF-8 data into 8-bit string data using the given file encoding

注意:步骤B中的encoding指的不是我们声明的coding:encoding,而是源文件保存在磁盘上的编码,我们用到的编码只在步骤E中使用到,下面的实验中也证明了这一点。

最后需要说明的是open(‘file’).read()得到的按字节读取的str类型,可以参考PYTHON-进阶-编码处理小结

现在有两个文件及其运行结果,因为我的终端环境为UTF-8,所以GB18030的str输出始终是乱码的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#utf8.py
#coding:utf-8
if __name__ == '__main__':

print 'UTF-8源py中的str中文字符'
print u'UTF-8源py中的unicode中文字符'

file = open('./utf8text','r')
print file.readline()
file.close()

file = open('./gb18030text','r')
print file.readline().decode('gb18030')
file.close()

# 运行:python utf8.py

UTF-8py中的str中文字符
UTF-8py中的unicode中文字符
UTF8文件中的编码

GB18030�ļ��еı���
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#gb18030.py
#coding: GB18030
if __name__ == '__main__':
print 'GB18030源py中的str中文'
print u'GB18030源py中的unicode中文'

file = open('./utf8text','r')
print file.readline()
file.close()
file = open('./gb18030text','r')
print file.readline()
file.close()

#python gb18030.py
GB18030Դpy�е�str����
GB18030源py中的unicode中文
UTF8文件中的编码

GB18030�ļ��еı���

还有一个文件utf8gb18030.py,它的源文件编码为UTF-8,但是我在头部声明了coding:utf-8,来看一下这个文件的运行结果为什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#coding: GB18030
if __name__ == '__main__':

print 'str:UTF-8源py中的中文字符,但是coding声明为GB18033'
print u'unicode:UTF-8源py中的中文字符,但是coding声明为GB18033'

file = open('./utf8text','r')
print file.readline()
file.close()

file = open('./gb18030text','r')
print file.readline()
file.close()

# python utf8gb18030.py
str:UTF-8py中的中文字符,但是coding声明为GB18033
unicode:UTF-8婧恜y涓殑涓枃瀛楃,浣嗘槸coding澹版槑涓篏B18033
UTF8文件中的编码

GB18030�ļ��еı���

我们知道,unicode对象在print到时候会根据sys.out的默认编码进行encode()的,所以不应该出现乱码情况,但是在第三次试验中,本该乱码的str没有问题,不该出现乱码的unicode却乱码了,这说明在上述解释器运行的过程中,步骤B和步骤E使用的不是同一编码,否则运行结果中unicode是不会乱码的。虽然两者不必相等,但是我们也看到了,这会带来更大的困扰,更难定位问题所在,所以一定要保证源文件的编码与声明的#coding:encoding一致,否则很难跳出坑的

总结

  1. 作为一个非纯Latin-1开发者,一定要在头部声明encoding hint。
  2. 为了避免不必要的麻烦,声明的encoding hint 一定要与源文件的编码一致。
  3. 在程序内部最好统一处理为unicode进行,在输出的时候在进行encode,如file.readline().decode(‘你的文件编码’)得到unicode,在写文件的时候可以指定unicode_str.encode(‘你需要的编码’)。
  4. 字面量字符串只是用u’xxx’得到unicode。
  5. str可以decode得到unicode,unicode可以encode得到str,其他方向的编解码是不可行的!

难免出错,还请不吝指教。

参考

字符编码笔记:ASCII,Unicode和UTF-8
字符集和字符编码(Charset & Encoding)
关于Python脚本开头两行的:#!/usr/bin/python和# -- coding: utf-8 --的作用 – 指定文件编码类型
PEP 263 – Defining Python Source Code Encodings
Overcoming frustration: Correctly using unicode in python2
Unicode HOWTO
python中的字符编码