Python中文处理

目录
  1. 摘要
  2. 基础知识
  3. 字符编码
    1. ASCII
    2. MBCS
    3. Unicode
    4. UTF-8
  4. Python 的中文编码
    1. 字符编码声明
    2. str和unicode
    3. 中文字符编解码

摘要

在Python(Python2.x)中使用中文字符常常给我们带来困扰。经常会遇到以下的错误:

1
UnicodeDecodeError: ’ascii’ codec can’t decode byte 0xe9 in position 1: ordinal not in range(128)

这是由于中文的字符编解码错误造成的。下面就让我们来研究一下字符编码和Python 的字符处理机制。注:本文Python 的内容仅适用于2.x,如果你使用3.x,那么恭喜你,你将不存在这些问题。

借用《Dive into Python3》中的一句话:

“你所了解的关于字符串的知识都是错的。”

基础知识

当人们说起“文本”,他们通常指显示在屏幕上的字符或者它的记号;但是计算机不能直接处理这些字符和标记;它们只认识位(bit) 和字节(byte)。实际上,从屏幕上的每一块文本都是以某种字符编码(character encoding) 的方式保存的。粗略地说就是,字符编码提供一种映射,使屏幕上显示的内容和内存、磁盘内存储的内容对应起来。有许多种不同的字符编码,有一些是为特定的语言,比如俄语、中文或者英语,设计、优化的,另外一些则可以用于多种语言的编码。

在实际操作中则会比上边描述的更复杂一些。许多字符在几种编码里是共用的,但是在实际的内存或者磁盘上,不同的编码方式可能会使用不同的字节序列来存储他们。所以,你可以把字符编码当做一种解码密钥。当有人给你一个字节序列—文件,网页,或者别的什么—并且告诉你它们是“文本”时,就需要知道他们使用了何种编码方式,然后才能将这些字节序列解码成字符。如果他们给的是错误的“密钥”或者根本没有给你“密钥”,那就得自己来破解这段编码,这可是一个艰难的任务。有可能你使用了错误的解码方式,然出现一些莫名其妙的结果。

现有的字符编码各类给世界上每种主要的语言都提供了编码方案。由于每种语言的各不相同,而且在以前内存和硬盘都很昂贵,所以每种字符编码都为特定的语言做了优化,每种编码都使用数字(0–255) 来代表这种语言的字符。比如,你熟悉ascii 编码,它将英语中的字符都当做从0–127 的数字来存储。西欧的一些语言,比如法语,西班牙语和德语等,比英语有更多的字母。这些语言最常用的编码方式是CP-1252,CP-1252和ascii 在0–127 这个范围内的字符是一样的,而比ascii 多的字符扩展到了128–255 这个范围。然而,它仍然是一种单字节的编码方式;可能的最大数字为255,这仍然可以用一个字节来表示。然而,像中文,日语和韩语等语言,他们的字符如此之多而不得不需要多字节编码的字符集。

字符编码

ASCII

ASCII(American Standard Code for Information Interchange),是一种单字节的编码。计算机世界里一开始只有英文,而单字节可以表示256 个不同的字符,可以表示所有的英文字符和许多的控制符号。ASCII码一共规定了128 个字符的编码,比如空格”SPACE” 是32(二进制00100000),大写的字母A 是65(二进制01000001)。这128 个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7 位,最前面的1 位统一规定为0。

MBCS

英语用128 个符号编码就够了,但是用来表示其他语言,单字节的ASCII 已无法满足需求。后来每个语言就制定了一套自己的编码,由于单字节能表示的字符太少,而且同时也需要与ASCII 编码保持兼容,所以这些编码纷纷使用了多字节来表示字符。他们的规则是,如果第一个字节是\x80 以下,则仍然表示ASCII字符;而如果是\x80 以上,则跟下一个字节(或几个字节)一起表示一个字符。

这里,IBM 发明了一个叫Code Page 的概念,将这些编码都收入囊中并分配页码,GBK 是第936 页,也就是CP936。所以,也可以使用CP936 表示GBK。MBCS(Multi-Byte Character Set) 是这些编码的统称。目前为止大家都是用了双字节,所以有时候也叫做DBCS(Double-Byte Character Set)。

Unicode

正如上面所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。于是大家坐在一起拍脑袋想出来一个方法:所有语言的字符都用同一种字符集来表示,这就是Unicode。

Unicode 当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+548C表示汉字“和”。具体的符对应表,可以查询unicode.org,或者专门的汉字对应表

最初的Unicode 标准UCS-2 使用两个字节表示一个字符,但过了不久有人觉得256*256 太少了,还是不够用,于是出现了UCS-4 标准,它使用4 个字节表示一个字符,不过我们用的最多的仍然是UCS-2。需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码(比如“和”这个字的码位是548C),却没有规定这个二进制代码应该如何存储。字符具体如何传输和储存则是由UTF(UCS Transformation Format)来负责。

一开始这事很简单,直接使用UCS 的码位来保存,这就是UTF-16 或和UTF-32,比如,“和”直接使用\x54x8C保存(UTF-16-BE),或是倒过来使用\x8C\x54保存(UTF-16-LE)。

UTF-8

上面说使用UTF-16要用两个字节来存储一个字符。但用着用着美国人觉得自己吃了大亏,以前英文字母只需要一个字节就能保存了,现在大锅饭一吃变成了两个字节,这对于存储来说是极大的浪费,文本文件的大小会因此大出一倍,这是无法接受的⋯⋯于是UTF-8横空出世。

UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8 的编码规则很简单,只有二条:

  1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII 码是相同的。
  2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1 位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode 码。

下表总结了编码规则,字母x 表示可用编码的位。

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

根据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。下面,还是以汉字“和”为例,演示如何实现UTF-8编码。已知“和”的unicode是548C(101010010001100),根据上表,可以发现548C处在第三行的范围内(0000 0800 – 0000 FFFF),因此“和”的UTF-8编码需要三个字节,即格式是”1110xxxx 10xxxxxx 10xxxxxx“。然后,从“和”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“和”的UTF-8编码是”11100101 10010010 10001100“,转换成十六进制就是E5928C

缺点:因为每个字符使用不同数量的字节编码,所以寻找串中第N个字符是一个O(N)复杂度的操作—即,串越长,则需要更多的时间来定位特定的字符。同时,还需要位变换来把字符编码成字节,把字节解码成字符。

优点:在处理经常会用到的ascii字符方面非常有效。在处理扩展的拉丁字符集方面也不比UTF-16差。对于中文字符来说,比UTF-32要好。同时,由位操作的天性使然,使用UTF-8不再存在字节顺序的问题了。一份以utf-8编码的文档在不同的计算机之间是一样的比特流。

Python 的中文编码

字符编码声明

源代码文件中,如果有用到非ASCII字符,则需要在文件头部进行字符编码的声明,如下:

1
#-*- coding:encoding -*-

根据这个声明,Python会尝试将文件中的字符编码转为encoding编码,并且,它尽可能的将指定地编码直接写成Unicode文本。注意,coding:encoding只是告诉Python文件使用了encoding格式的编码,但是编辑器可能会以自己的方式存储.py文件,因此最后文件保存的时候还需要编码中选指定的ecoding才行。

str和unicode

Python 有两种字符串类型,str与unicode。str和unicode都是basestring的子类。严格意义上说,str其实是字节串,它是unicode经过编码后的字节组成的序列(根据不同的编码规则其编码长度不一样)。unicode才是真正意义上的字符串,对字节串str使用正确的字符编码进行解码后获得(其长度和“显示”的长度一致)。在Python3已经取消了str,让所有的字符串都是unicode 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding:utf-8 -*-

s = '我爱中国' # 这个是str的字符串
u = u'我爱中国' # 这个是unicode的字符串

type(s) # str
type(u) # unicode

print s.__class__ # <type 'str'>
print u.__class__ # <type 'unicode'>

s # '\xe6\x88\x91\xe7\x88\xb1\xe4\xb8\xad\xe5\x9b\xbd'
len(s) # 12

u # u'\u6211\u7231\u4e2d\u56fd'’
len(u) # 4

中文字符编解码

Python中unicode才是字符的唯一内码,而大家常用的字符集如gb2312,gbk,utf-8,以及ascii都是字符的二进制(字节)编码形式。把字符从unicode 转换成二进制编码,当然是要encode(编码)。反过来,在Python中出现的str都是用字符集编码的ansi字符串。Python本身并不知道str的编码,需要由开发者指定正确的字符集decode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: utf-8 -*-

s = '我爱中国' # 这个是str的字符串
u = u'我爱中国' # 这个是unicode的字符串

s # '\xe6\x88\x91\xe7\x88\xb1\xe4\xb8\xad\xe5\x9b\xbd'
u # u'\u6211\u7231\u4e2d\u56fd'’

s.decode('utf-8') # u'\u6211\u7231\u4e2d\u56fd'
u.encode('utf-8') # '\xe6\x88\x91\xe7\x88\xb1\xe4\xb8\xad\xe5\x9b\xbd'

u.encode('gb2312') # '\xce\xd2\xb0\xae\xd6\xd0\xb9\xfa'
s.decode('utf-8').encode('gb2312') # '\xce\xd2\xb0\xae\xd6\xd0\xb9\xfa'
s.decode('gb2312') # UnicodeDecodeError: 'gb2312' codec can't decode bytes
# in position 0-1: illegal multibyte sequence

str的编码是根据系统的默认编码来的。简体中文Windows的默认编码是GBK,在我的机器上(Archlinux)的默认编码是UTF-8。因此,s.decode(‘utf-8’)能成功解码,而s.decode(‘gb2312’) 就不能解码。任何编码decode都会得到unicode,且unicode可以encode成任何编码。

因此,处理中文数据时最好采用如下方式:

  1. Decode early(尽早decode,将文件中的内容转化成unicode再进行下一步处理)
  2. Unicode everywhere(程序内部处理都用unicode)
  3. Encode late(最后encode回所需的encoding, 例如把最终结果写进结果文件)

评论