字符编码问题是每个程序员必定会遇到的,同样,python的中文问题一直是一个非常令人头疼的问题,本文将介绍Python中涉及到中文细节问题。
建议在阅读这篇文章之前,先前往字符编码详解这篇文章了解相关字符编码的原理细节。
str和Unicode
在Python中有两种默认的字符串:str和Unicode:
- str字符串本质上是一个字节流,是原字符经过编码之后的一个个字节,但它并不存储具体的编码方式。
- Unicode字符串本质上是一个Unicode对象,它内部使用了特定的编码,但是对程序员透明。事实上Python中并没有Unicode字符串,但为了理解方便,以下所说的Unicode字符串均指的是Unicode对象。
在Windows控制台测试str字符串如下,可以看到str字符串的类型为"str",使用len得到的长度为4,表示这个字节流一共有4个字节。通过前面字符编码详解这篇文章,应该可以知道“你好”这个词中“你”的GBK编码为0xC4E3,“好”则为0xBAC3,对比结果中s的输出可知,Python中默认就是使用了GBK进行编码,然后把编码后的字节流保存到了str数组s中。
在Windows控制台测试Unicode如下,可以看到Unicode字符串的类型为"unicode",使用len得到的长度为2,表示了这个字符串的真正长度。
而通过前面字符编码详解这篇文章,应该可以知道“你好”这个词中“你”的UTF-16大端字节序编码为0x4F60,“好”则为0x597D,对比结果中u的输出可知,Python控制台下的Unicode对象默认使用UTF-16大端字节序进行编码。
由上面可知,使用str数组时得到的长度是字符串编码后的字节的个数,而实际上我们通常关注的是字符串本身的长度。另外,使用Python的切片操作会产生同样的问题,为了减少不必要的麻烦,在实际的编程工作中,我们可以按如下方法进行:
- 最先解码。首先要将输入的字符字节流解码为Unicode对象,方便后续如计数、切片等操作的进行。
- 最后编码。最后将处理后的结果输出到其他文件或者数据库的时候才将Unicode对象进行编码,以满足特定的存储显示需求。
- 默认使用utf-8编码。使用UTF-8编码可以处理任何Unicode字符,具有更好地通用性,尽量用来替代默认的ASCII或者GBK编码,但是要注意Windows中默认编码为GBK或者其他类似GBK的编码的情况。
编解码
str字符串是原字符使用特定的编码方法得到的字节流,如果知道了该编码方法,我们可以使用decode函数把str字节流解码为原先的字符串的Unicode对象。而Unicode对象也可以使用encode函数编码成为对应的字节流。
- s.decode(‘GBK’),表示s是一个编码后的字节流,现在要把它使用GBK标准进行解码得到Unicode对象。如果s本身的编码方式不是GBK,则可能会产生错误。注意不要对一个Unicode对象调用decode方法。另外,使用a = s.decode(‘GBK’)和使用a = unicode(s, ‘GBK’)效果一样。
- s.encode(‘GBK’),表示s是一个Unicode对象,现在用GBK标准把它编码成字节流。注意不要对一个str字符串调用encode方法,因为其本身已经经过了编码。
例如,根据前面我们知道了Python在控制台下str字符串默认是GBK编码后的字节流,以下我们把str字符串使用GBK解码为Unicode字符串,同样使用GBK把Unicode字符串编码为str字节流。可以看到,通过转化后得到的结果是一样的。
下面我们看一下把Unicode使用其他方法编码后再控制台下的显示结果。
由前面可以知道,Windows下控制台的默认使用的编码是GBK,所以我们把u编码为GBK时可以正常显示,而编码为UTF-8或UTF-16时则显示乱码。而在控制台下直接输出Unicode字符串也能正常显示,这是Python在向控制台输出unicode对象的时候会自动根据输出环境的编码进行转换,而如果输出的不是unicode对象而是普通的str字符串,则会直接按照该字符串的编码输出字符串,从而出现上面的现象。
IDLE上的结果
以上的测试均是在Windows控制台的Python下进行,但是在Python自带的IDE即IDLE下情况有所不同。
IDLE中str字符串也是默认采用GBK编码得到,而Unicode对象内部采用的也是GBK编码,但是不能正常输出,而且以下试图把它转为其他编码时也不能输出。
但把str字符串解码为Unicode对象时则和控制台下情况一致,并且可以正常显示。
编码声明
如果是在脚本中使用Python,因为Python默认脚本文件都是ASCII编码的,当文件中有非ASCII编码范围内的字符的时候就要添加编码声明:在第一行或第二行添加
其中编码类型可以改为GBK等其他类型。否则会出现错误:
如果使用IDLE,则会提示:
文件编码
文件编码指的是具体的代码文本在保存时使用的编码方法,如果声明的编码和文件实际的编码不一致的话,很可能会出现乱码甚至出错等问题。原因是对于类似于u’你好’这样声明的字符串,在源代码文件里经过了该文件指定编码方法的编码被保存成字节流,程序运行时读取到这个字节流,就会使用编码声明中的编码去解码得到原始字符串,然后对这个原始字符串再用Unicode的内部编码方案进行存储。而对于’你好’这样不带u的字符串,则表示字节流,此时会直接用文件编码后的字节数组进行表示,不需要使用编码声明中的编码方案进行解码。例如,建立如下内容的脚本文件然后保存成ANSI编码:
运行后得到错误
但把脚本改成:
则可以得到以下结果:
对于GBK的编码声明,代码如下:
此时如果把源文件改成UTF-8编码,对于带BOM的情况,直接返回错误:SyntaxError: encoding problem: utf-8
而对于不带BOM的情况,则返回结果:
原因是’你好’直接用其在文件中的编码表示了,而u’你好’会用编码声明GBK去解码其存储在文件中的UTF-8表示,结果得不到其原本的字符串,反而得到一个新的字符串’浣犲ソ’,这个新的字符串再用Unicode内部编码成为’\u6d63\u72b2\u30bd’。
不过现在的IDE如IDLE一般比较智能,会根据代码中的编码声明采用同样的编码方案保存代码文件,所以以上实验如果在IDLE中结果可能不一样。例如,
- 在IDLE中新建的Python脚本添加# -*- coding: GBK -*-声明,代码文件会自动保存成ANSI编码。
- 在IDLE中新建的Python脚本添加# -*- coding: UTF-8 -*-声明,代码文件会自动保存不带BOM的UTF-8编码。
但如果是使用文本编辑器编写的代码,需要特别注意保存文件时使用的编码方法要和编码声明中一致。
另外,如果使用了print输出时,也要注意编码方案是否和Windows的控制台默认编码一致。
例如,编写一下test.py脚本并使用ANSI编码保存。
运行时得到如下结果,其中str字符串根据编码声明采用了GBK编码,然后可以正常输出。Unicode字符串采用了默认的UTF-16编码,然后输出时也被自动转换成输出环境的编码,从而显示正常。
但如果把第一句声明改成:# -*- coding: UTF-8 -*-,则得到如下结果。其中str字符串根据编码声明采用了UTF-8编码,然而输出时产生了乱码,这是因为Windows控制台默认编码为GBK,str字符串(字节流)输出时不需要转化直接根据GBK的规则进行显示,由于这里str字符串本身不是GBK编码,于是不能正确显示,这时可以通过print s.decode(‘UTF-8′) 或print s.decode(‘UTF-8′).encode(‘GBK’)进行显示。而Unicode情况则跟之前一样。
另外,通过这两个例子可以知道,编码类型对Unicode对象的编码并没有影响,它内部均是采用了UTF-16进行编码,由于它对程序员透明,我们也不需要动它。注意,这里也是使用了IDLE进行运行,但是Unicode对象的内部编码并没有跟上一小节那样使用GBK进行,不同之处是这里是脚本运行,而上一节是在交互环境下运行。
文件读写
下面我们看看使用Python读取中文文件并在终端显示内容会发生什么事情。新建一个脚本并写入如下内容,
然后创建一个文本文件text.txt并写入两个字”你好“,下面看保存成不同的编码格式时不同的运行结果:
- 保存为默认ANSI编码格式: ‘\xc4\xe3\xba\xc3′ 你好
- 保存为不带BOM的UTF-8时: ‘\xe4\xbd\xa0\xe5\xa5\xbd’ 浣犲ソ
- 保存为带BOM的UTF-8时:’\xef\xbb\xbf\xe4\xbd\xa0\xe5\xa5\xbd’ 你好
可以看到,s直接就得到了文件中字符串在文本保存时的编码字节流,然后print输出时,第一个GBK编码的能正常输出,第二个UTF-8编码的被控制台用GBK解码成了别的字符串而产生乱码,第三个字节流前面有3个字节(BOM)表示使用了UTF-8编码,然后也输出正常,估计可能Python能够识别这个BOM然后使用了对应的编码格式进行解析。
可以看到,这里编码声明对结果并无影响,原因是read()函数直接把字符串在文件中的编码字节流读取进来了,而这个字节流只跟源文件的编码格式相关。
内置的open()方法打开文件时,read()读取的是str,读取后需要使用正确的编码格式进行decode()。write()写入时,如果参数是unicode,则需要使用你希望写入的编码进行encode(),如果是其他编码格式的str,则需要先用该str的编码进行decode(),转成unicode后再使用写入的编码进行encode()。如果直接将unicode作为参数传入write()方法,Python将先使用源代码文件声明的字符编码进行编码然后写入。
模块codecs提供了一个open()方法,可以指定一个编码打开文件,使用这个方法打开的文件读取返回的将是unicode。写入时,如果参数是unicode,则使用open()时指定的编码进行编码后写入;如果是str,则先根据源代码文件声明的字符编码,解码成unicode后再进行前述操作。相对内置的open()来说,这个方法比较不容易在编码上出现问题。