Not Only Algorithm,不仅仅是算法,关注数学、算法、数据结构、程序员笔试面试以及一切涉及计算机编程之美的内容 。。
你的位置:NoAlGo博客 » 程序设计 » , ,

Python编程的中文问题

字符编码问题是每个程序员必定会遇到的,同样,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中。

>>> s = '你好'
>>> type(s)
<type 'str'>
>>> s
'\xc4\xe3\xba\xc3'
>>> print s
你好
>>> len(s)
4

在Windows控制台测试Unicode如下,可以看到Unicode字符串的类型为"unicode",使用len得到的长度为2,表示了这个字符串的真正长度。
而通过前面字符编码详解这篇文章,应该可以知道“你好”这个词中“你”的UTF-16大端字节序编码为0x4F60,“好”则为0x597D,对比结果中u的输出可知,Python控制台下的Unicode对象默认使用UTF-16大端字节序进行编码。

>>> u = u'你好'
>>> type(u)
<type 'unicode'>
>>> u
u'\u4f60\u597d'
>>> print u
你好
>>> len(u)
2

由上面可知,使用str数组时得到的长度是字符串编码后的字节的个数,而实际上我们通常关注的是字符串本身的长度。另外,使用Python的切片操作会产生同样的问题,为了减少不必要的麻烦,在实际的编程工作中,我们可以按如下方法进行:

  1. 最先解码。首先要将输入的字符字节流解码为Unicode对象,方便后续如计数、切片等操作的进行。
  2. 最后编码。最后将处理后的结果输出到其他文件或者数据库的时候才将Unicode对象进行编码,以满足特定的存储显示需求。
  3. 默认使用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字节流。可以看到,通过转化后得到的结果是一样的。

>>> s = '你好'
>>> s
'\xc4\xe3\xba\xc3'
>>> s.decode('GBK')
u'\u4f60\u597d'
>>> u = u'你好'
>>> u
u'\u4f60\u597d'
>>> u.encode('GBK')
'\xc4\xe3\xba\xc3'

下面我们看一下把Unicode使用其他方法编码后再控制台下的显示结果。

>>> u = u'你好'
>>> print u
你好
>>> print u.encode('GBK')
你好
>>> print u.encode('UTF-8')
浣犲ソ
>>> print u.encode('UTF-16')
O}Y

由前面可以知道,Windows下控制台的默认使用的编码是GBK,所以我们把u编码为GBK时可以正常显示,而编码为UTF-8或UTF-16时则显示乱码。而在控制台下直接输出Unicode字符串也能正常显示,这是Python在向控制台输出unicode对象的时候会自动根据输出环境的编码进行转换,而如果输出的不是unicode对象而是普通的str字符串,则会直接按照该字符串的编码输出字符串,从而出现上面的现象。

IDLE上的结果

以上的测试均是在Windows控制台的Python下进行,但是在Python自带的IDE即IDLE下情况有所不同。

>>> s = '你好'
>>> s
'\xc4\xe3\xba\xc3'
>>> u = u'你好'
>>> u
u'\xc4\xe3\xba\xc3'
>>> print s
你好
>>> print u
??o?

IDLE中str字符串也是默认采用GBK编码得到,而Unicode对象内部采用的也是GBK编码,但是不能正常输出,而且以下试图把它转为其他编码时也不能输出。

>>> print u.encode('GBK')
Traceback (most recent call last):
  File "<pyshell#30>", line 1, in <module>
    print u.encode('GBK')
UnicodeEncodeError: 'gbk' codec can't encode character u'\xc4' in position 0: illegal multibyte sequence
>>> print u.encode('UTF-8')
脛茫潞脙

但把str字符串解码为Unicode对象时则和控制台下情况一致,并且可以正常显示。

>>> uu = s.decode('GBK')
>>> uu
u'\u4f60\u597d'
>>> print uu
你好

编码声明

如果是在脚本中使用Python,因为Python默认脚本文件都是ASCII编码的,当文件中有非ASCII编码范围内的字符的时候就要添加编码声明:在第一行或第二行添加

# -*- coding=UTF-8 -*- 或 #coding=UTF-8

其中编码类型可以改为GBK等其他类型。否则会出现错误:

SyntaxError: Non-ASCII character '\xc4' in file C:\Users\Erichsh\a.py on line 1,
but no encoding declared; see http://www.python.org/peps/pep-0263.html for details。

如果使用IDLE,则会提示:

Non-ASCII found, yet no encoding declared. Add a line like # -*- coding: cp936 -*- to you file

文件编码

文件编码指的是具体的代码文本在保存时使用的编码方法,如果声明的编码和文件实际的编码不一致的话,很可能会出现乱码甚至出错等问题。原因是对于类似于u’你好’这样声明的字符串,在源代码文件里经过了该文件指定编码方法的编码被保存成字节流,程序运行时读取到这个字节流,就会使用编码声明中的编码去解码得到原始字符串,然后对这个原始字符串再用Unicode的内部编码方案进行存储。而对于’你好’这样不带u的字符串,则表示字节流,此时会直接用文件编码后的字节数组进行表示,不需要使用编码声明中的编码方案进行解码。例如,建立如下内容的脚本文件然后保存成ANSI编码:

# -*- coding: UTF-8 -*-
s = '你好'
print repr(s), s
u = u'你好'
print repr(u), u

运行后得到错误

SyntaxError: (unicode error) ‘utf8′ codec can’t decode byte 0xc4 in position 0: invalid continuation byte

但把脚本改成:

# -*- coding: UTF-8 -*-
s = '你好'
print repr(s), s

则可以得到以下结果:

'\xc4\xe3\xba\xc3' 你好 #原文件使用的是ANSI编码,所以得到GBK编码,GBK编码可以正常显示

对于GBK的编码声明,代码如下:

# -*- coding: GBK -*-
s = '你好'
print repr(s), s
u = u'你好'
print repr(u), u

此时如果把源文件改成UTF-8编码,对于带BOM的情况,直接返回错误:SyntaxError: encoding problem: utf-8
而对于不带BOM的情况,则返回结果:

'\xe4\xbd\xa0\xe5\xa5\xbd' 浣犲ソ
u'\u6d63\u72b2\u30bd' 浣犲ソ

原因是’你好’直接用其在文件中的编码表示了,而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编码保存。

# -*- coding: GBK -*-
s = '你好'
print repr(s), s
u = u'你好'
print repr(u), u

运行时得到如下结果,其中str字符串根据编码声明采用了GBK编码,然后可以正常输出。Unicode字符串采用了默认的UTF-16编码,然后输出时也被自动转换成输出环境的编码,从而显示正常。

'\xc4\xe3\xba\xc3' 你好
u'\u4f60\u597d' 你好

但如果把第一句声明改成:# -*- 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情况则跟之前一样。

'\xe4\xbd\xa0\xe5\xa5\xbd' 浣犲ソ
u'\u4f60\u597d' 你好

另外,通过这两个例子可以知道,编码类型对Unicode对象的编码并没有影响,它内部均是采用了UTF-16进行编码,由于它对程序员透明,我们也不需要动它。注意,这里也是使用了IDLE进行运行,但是Unicode对象的内部编码并没有跟上一小节那样使用GBK进行,不同之处是这里是脚本运行,而上一节是在交互环境下运行。

文件读写

下面我们看看使用Python读取中文文件并在终端显示内容会发生什么事情。新建一个脚本并写入如下内容,

# -*- coding: UTF-8 -*-
s = open("text.txt").read()
print repr(s), s

然后创建一个文本文件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()来说,这个方法比较不容易在编码上出现问题。

# -*- coding: GBK -*-
import codecs
f = codecs.open('text.txt', encoding='UTF-8')
u = f.read() #u为Unicode对象
f.close() 
f = codecs.open('text.txt','a', encoding='UTF-8')
u = u'你好'
f.write(u)
上一篇: 下一篇:

我的博客

NoAlGo头像编程这件小事牵扯到太多的知识,很容易知其然而不知其所以然,但真正了不起的程序员对自己程序的每一个字节都了如指掌,要立足基础理论,努力提升自我的专业修养。

站内搜索

最新评论