Python字符编码详解

本文简单介绍了各种常用的字符编码的特点,并介绍了在python2.x中如何与编码问题作战 :) 
请注意本文关于Python的内容仅适用于2.x,3.x中str和unicode有翻天覆地的变化,请查阅其他相关文档。 
尊重作者的劳动,转载请注明作者及原文地址 >.<

1. 字符编码简介

1.1. ASCII

ASCII(American Standard Code for Information Interchange),是一种单字节的编码。计算机世界里一开始只有英文,而单字节可以表示256个不同的字符,可以表示所有的英文字符和许多的控制符号。不过ASCII只用到了其中的一半(\x80以下),这也是MBCS得以实现的基础。

1.2. MBCS

然而计算机世界里很快就有了其他语言,单字节的ASCII已无法满足需求。后来每个语言就制定了一套自己的编码,由于单字节能表示的字符太少,而且同时也需要与ASCII编码保持兼容,所以这些编码纷纷使用了多字节来表示字符,如GBxxxBIGxxx等等,他们的规则是,如果第一个字节是\x80以下,则仍然表示ASCII字符;而如果是\x80以上,则跟下一个字节一起(共两个字节)表示一个字符,然后跳过下一个字节,继续往下判断。

这里,IBM发明了一个叫Code Page的概念,将这些编码都收入囊中并分配页码,GBK是第936页,也就是CP936。所以,也可以使用CP936表示GBK。

MBCS(Multi-Byte Character Set)是这些编码的统称。目前为止大家都是用了双字节,所以有时候也叫做DBCS(Double-Byte Character Set)。必须明确的是,MBCS并不是某一种特定的编码,Windows里根据你设定的区域不同,MBCS指代不同的编码,而Linux里无法使用MBCS作为编码。在Windows中你看不到MBCS这几个字符,因为微软为了更加洋气,使用了ANSI来吓唬人,记事本的另存为对话框里编码ANSI就是MBCS。同时,在简体中文Windows默认的区域设定里,指代GBK。

1.3. Unicode

后来,有人开始觉得太多编码导致世界变得过于复杂了,让人脑袋疼,于是大家坐在一起拍脑袋想出来一个方法:所有语言的字符都用同一种字符集来表示,这就是Unicode。

最初的Unicode标准UCS-2使用两个字节表示一个字符,所以你常常可以听到Unicode使用两个字节表示一个字符的说法。但过了不久有人觉得256*256太少了,还是不够用,于是出现了UCS-4标准,它使用4个字节表示一个字符,不过我们用的最多的仍然是UCS-2。

UCS(Unicode Character Set)还仅仅是字符对应码位的一张表而已,比如"汉"这个字的码位是6C49。字符具体如何传输和储存则是由UTF(UCS Transformation Format)来负责。

一开始这事很简单,直接使用UCS的码位来保存,这就是UTF-16,比如,"汉"直接使用\x6C\x49保存(UTF-16-BE),或是倒过来使用\x49\x6C保存(UTF-16-LE)。但用着用着美国人觉得自己吃了大亏,以前英文字母只需要一个字节就能保存了,现在大锅饭一吃变成了两个字节,空间消耗大了一倍……于是UTF-8横空出世。

UTF-8是一种很别扭的编码,具体表现在他是变长的,并且兼容ASCII,ASCII字符使用1字节表示。然而这里省了的必定是从别的地方抠出来的,你肯定也听说过UTF-8里中文字符使用3个字节来保存吧?4个字节保存的字符更是在泪奔……(具体UCS-2是怎么变成UTF-8的请自行搜索)

另外值得一提的是BOM(Byte Order Mark)。我们在储存文件时,文件使用的编码并没有保存,打开时则需要我们记住原先保存时使用的编码并使用这个编码打开,这样一来就产生了许多麻烦。(你可能想说记事本打开文件时并没有让选编码?不妨先打开记事本再使用文件 -> 打开看看)而UTF则引入了BOM来表示自身编码,如果一开始读入的几个字节是其中之一,则代表接下来要读取的文字使用的编码是相应的编码:

BOM_UTF8 '\xef\xbb\xbf' 
BOM_UTF16_LE '\xff\xfe' 
BOM_UTF16_BE '\xfe\xff'

并不是所有的编辑器都会写入BOM,但即使没有BOM,Unicode还是可以读取的,只是像MBCS的编码一样,需要另行指定具体的编码,否则解码将会失败。

你可能听说过UTF-8不需要BOM,这种说法是不对的,只是绝大多数编辑器在没有BOM时都是以UTF-8作为默认编码读取。即使是保存时默认使用ANSI(MBCS)的记事本,在读取文件时也是先使用UTF-8测试编码,如果可以成功解码,则使用UTF-8解码。记事本这个别扭的做法造成了一个BUG:如果你新建文本文件并输入"姹塧"然后使用ANSI(MBCS)保存,再打开就会变成"汉a",你不妨试试 :)

2. Python2.x中的编码问题

2.1. str和unicode

str和unicode都是basestring的子类。严格意义上说,str其实是字节串,它是unicode经过编码后的字节组成的序列。对UTF-8编码的str'汉'使用len()函数时,结果是3,因为实际上,UTF-8编码的'汉' == '\xE6\xB1\x89'。

unicode才是真正意义上的字符串,对字节串str使用正确的字符编码进行解码后获得,并且len(u'汉') == 1。

再来看看encode()和decode()两个basestring的实例方法,理解了str和unicode的区别后,这两个方法就不会再混淆了:

1
2
3
4
5
6
7
8
9
10
11
12
13
# coding: UTF-8
 
u = u'汉'
print repr(u) # u'\u6c49'
s = u.encode('UTF-8')
print repr(s) # '\xe6\xb1\x89'
u2 = s.decode('UTF-8')
print repr(u2) # u'\u6c49'
 
# 对unicode进行解码是错误的
# s2 = u.decode('UTF-8')
# 同样,对str进行编码也是错误的
# u2 = s.encode('UTF-8')

需要注意的是,虽然对str调用encode()方法是错误的,但实际上Python不会抛出异常,而是返回另外一个相同内容但不同id的str;对unicode调用decode()方法也是这样。很不理解为什么不把encode()和decode()分别放在unicode和str中而是都放在basestring中,但既然已经这样了,我们就小心避免犯错吧。

2.2. 字符编码声明

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

1
#-*- coding: UTF-8 -*-

实际上Python只检查#、coding和编码字符串,其他的字符都是为了美观加上的。另外,Python中可用的字符编码有很多,并且还有许多别名,还不区分大小写,比如UTF-8可以写成u8。参见http://docs.python.org/library/codecs.html#standard-encodings

另外需要注意的是声明的编码必须与文件实际保存时用的编码一致,否则很大几率会出现代码解析异常。现在的IDE一般会自动处理这种情况,改变声明后同时换成声明的编码保存,但文本编辑器控们需要小心 :)

2.3. 读写文件

内置的open()方法打开文件时,read()读取的是str,读取后需要使用正确的编码格式进行decode()。write()写入时,如果参数是unicode,则需要使用你希望写入的编码进行encode(),如果是其他编码格式的str,则需要先用该str的编码进行decode(),转成unicode后再使用写入的编码进行encode()。如果直接将unicode作为参数传入write()方法,Python将先使用源代码文件声明的字符编码进行编码然后写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# coding: UTF-8
 
f = open('test.txt')
s = f.read()
f.close()
print type(s) # <type 'str'>
# 已知是GBK编码,解码成unicode
u = s.decode('GBK')
 
f = open('test.txt', 'w')
# 编码成UTF-8编码的str
s = u.encode('UTF-8')
f.write(s)
f.close()

另外,模块codecs提供了一个open()方法,可以指定一个编码打开文件,使用这个方法打开的文件读取返回的将是unicode。写入时,如果参数是unicode,则使用open()时指定的编码进行编码后写入;如果是str,则先根据源代码文件声明的字符编码,解码成unicode后再进行前述操作。相对内置的open()来说,这个方法比较不容易在编码上出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# coding: GBK
 
import codecs
 
f = codecs.open('test.txt', encoding='UTF-8')
u = f.read()
f.close()
print type(u) # <type 'unicode'>
 
f = codecs.open('test.txt', 'a', encoding='UTF-8')
# 写入unicode
f.write(u)
 
# 写入str,自动进行解码编码操作
# GBK编码的str
s = '汉'
print repr(s) # '\xba\xba'
# 这里会先将GBK编码的str解码为unicode再编码为UTF-8写入
f.write(s)
f.close()

2.4. 与编码相关的方法

sys/locale模块中提供了一些获取当前环境下的默认编码的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# coding:gbk
 
import sys
import locale
 
def p(f):
    print '%s.%s(): %s' % (f.__module__, f.__name__, f())
 
# 返回当前系统所使用的默认字符编码
p(sys.getdefaultencoding)
 
# 返回用于转换Unicode文件名至系统文件名所使用的编码
p(sys.getfilesystemencoding)
 
# 获取默认的区域设置并返回元祖(语言, 编码)
p(locale.getdefaultlocale)
 
# 返回用户设定的文本数据编码
# 文档提到this function only returns a guess
p(locale.getpreferredencoding)
 
# \xba\xba是'汉'的GBK编码
# mbcs是不推荐使用的编码,这里仅作测试表明为什么不应该用
print r"'\xba\xba'.decode('mbcs'):", repr('\xba\xba'.decode('mbcs'))
 
#在笔者的Windows上的结果(区域设置为中文(简体, 中国))
#sys.getdefaultencoding(): gbk
#sys.getfilesystemencoding(): mbcs
#locale.getdefaultlocale(): ('zh_CN', 'cp936')
#locale.getpreferredencoding(): cp936
#'\xba\xba'.decode('mbcs'): u'\u6c49'

3.一些建议

3.1. 使用字符编码声明,并且同一工程中的所有源代码文件使用相同的字符编码声明。

这点是一定要做到的。

3.2. 抛弃str,全部使用unicode。

按引号前先按一下u最初做起来确实很不习惯而且经常会忘记再跑回去补,但如果这么做可以减少90%的编码问题。如果编码困扰不严重,可以不参考此条。

3.3. 使用codecs.open()替代内置的open()。

如果编码困扰不严重,可以不参考此条。

3.4. 绝对需要避免使用的字符编码:MBCS/DBCS和UTF-16。

这里说的MBCS不是指GBK什么的都不能用,而是不要使用Python里名为'MBCS'的编码,除非程序完全不移植。

Python中编码'MBCS'与'DBCS'是同义词,指当前Windows环境中MBCS指代的编码。Linux的Python实现中没有这种编码,所以一旦移植到Linux一定会出现异常!另外,只要设定的Windows系统区域不同,MBCS指代的编码也是不一样的。分别设定不同的区域运行2.4小节中的代码的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#中文(简体, 中国)
#sys.getdefaultencoding(): gbk
#sys.getfilesystemencoding(): mbcs
#locale.getdefaultlocale(): ('zh_CN', 'cp936')
#locale.getpreferredencoding(): cp936
#'\xba\xba'.decode('mbcs'): u'\u6c49'
 
#英语(美国)
#sys.getdefaultencoding(): UTF-8
#sys.getfilesystemencoding(): mbcs
#locale.getdefaultlocale(): ('zh_CN', 'cp1252')
#locale.getpreferredencoding(): cp1252
#'\xba\xba'.decode('mbcs'): u'\xba\xba'
 
#德语(德国)
#sys.getdefaultencoding(): gbk
#sys.getfilesystemencoding(): mbcs
#locale.getdefaultlocale(): ('zh_CN', 'cp1252')
#locale.getpreferredencoding(): cp1252
#'\xba\xba'.decode('mbcs'): u'\xba\xba'
 
#日语(日本)
#sys.getdefaultencoding(): gbk
#sys.getfilesystemencoding(): mbcs
#locale.getdefaultlocale(): ('zh_CN', 'cp932')
#locale.getpreferredencoding(): cp932
#'\xba\xba'.decode('mbcs'): u'\uff7a\uff7a'

可见,更改区域后,使用mbcs解码得到了不正确的结果,所以,当我们需要使用'GBK'时,应该直接写'GBK',不要写成'MBCS'。

UTF-16同理,虽然绝大多数操作系统中'UTF-16'是'UTF-16-LE'的同义词,但直接写'UTF-16-LE'只是多写3个字符而已,而万一某个操作系统中'UTF-16'变成了'UTF-16-BE'的同义词,就会有错误的结果。实际上,UTF-16用的相当少,但用到的时候还是需要注意。

--END--

分类: Python
30
0
« 上一篇:Python正则表达式指南
» 下一篇:Python自省(反射)指南
posted @ 2010-12-05 23:11 AstralWind 阅读(113172) 评论(40编辑 收藏

  
#1楼 2010-12-06 10:34 心在天  
刚好碰到编码问题,谢谢了!整理的相当到位
  
#2楼 2010-12-16 10:42 iTech  
顶,我用过str.decode
  
#3楼 2011-05-04 10:06 fykknd  
太专业了,看不懂啊
  
#4楼 2011-11-14 17:42 XueM  
楼主好:
请教您一个问题:像这样的"&#20320;"这样的字符串,怎样转换成汉字呢?
  
#5楼[楼主2011-11-14 22:30 AstralWind  
@ XueM
弄清楚"&#20320;"这样的HTML转义符使用的是Unicode码表就很容易了:
1
2
3
4
5
6
>>> hex(20320)
'0x4f60'
>>> u'你'
u'\u4f60'
>>> unichr(int("&#20320;"[2:-1]))
u'\u4f60'
  
#6楼 2011-11-24 17:24 XueM  
@ AstralWind
您好,上次请教的问题非常感谢,我在编码这方面还是有很多疑问:
比如下面这几种:
旅行,
'\xe6\x97\x85\xe8\xa1\x8c',
u'\u65c5\u884c'
\u65c5\u884c
怎样将这些编码转换成中文呢?
  
#7楼 2011-12-29 15:58 YuxuanWang  
楼主讲真不错
简单清晰
很多年都搞不清楚编码问题,这个清楚了,多谢
  
#8楼 2012-01-17 19:35 鸪斑兔  
Solaris上是这样,能否解释一下?
>>> u = u'汉'
>>> print repr(u)
u'\xba\xba'
>>> s = u.encode('UTF-8')
>>> print repr(s)
'\xc2\xba\xc2\xba'
>>>
  
#9楼[楼主2012-02-04 00:21 AstralWind  
引用 XueM:@AstralWind
您好,上次请教的问题非常感谢,我在编码这方面还是有很多疑问:
比如下面这几种:
旅行,
'\xe6\x97\x85\xe8\xa1\x8c',
u'\u65c5\u884c'
\u65c5\u884c
怎样将这些编码转换成中文呢?

你还是没有明白意思,字节串本身是不带编码信息的,所以必须你事先知道字节串使用的编码才能进行正确的解码。解码得到的就是unicode了,在码表中对应的就是中文。
但是两种不同的编码一般会存在一些互相不同的“真空区域”,如果字节串中包含这里面的字节,解码时会抛出异常。中文我们一般都是使用GBK和UTF-8居多,所以有一个方法就是依次使用这两种编码尝试解码,哪个没有抛出异常就认为是哪种编码格式。
这个方法并不是完全准确。比如我文章里有写到的记事本bug,演示的就是同样的字节串使用不同的编码格式进行解码得到了不同的结果,很难说哪种编码格式是正确的。
  
#10楼[楼主2012-02-04 00:27 AstralWind  
引用 tuzkee:Solaris上是这样,能否解释一下?
>>> u = u'汉'
>>> print repr(u)
u'\xba\xba'
>>> s = u.encode('UTF-8')
>>> print repr(s)
'\xc2\xba\xc2\xba'
>>>

这种情况确实有问题,不过我的suse linux和windows上都没有重现
>>> u = u'汉'
>>> u
u'\u6c49'
>>> repr(u)
"u'\\u6c49'"
>>> 
我没有使用过solaris环境,初步估计可能是输入法写入了 \xba\xba(GBK编码而不是unicode),终端上却显示为 汉 的缘故。
  
#11楼 2012-03-11 21:30 松下裤腰带  
楼主请看下我这是什么情况:
>>> #coding:utf-8
>>> u = u'汉'
>>> print repr(u)
u'\xba\xba'
>>> s = u.encode('utf-8')
>>> print repr(s)
'\xc2\xba\xc2\xba'
>>> u2 = s.decode('utf-8')
>>> print repr(u2)
u'\xba\xba'
>>> 
我用的是Xp。python2.7
  
#12楼[楼主2012-03-13 21:21 AstralWind  
@ scncpb
你这个问题与楼上一位用solaris的同学一模一样……
但我在我的xp坏境下跑了一遍没有这个问题。
我不一定能解决这个问题,只能试试看:
如果你方便的话请贴一下
sys.getdefaultencoding()
locale.getdefaultlocale()
locale.getpreferredencoding()
这几个方法的结果(需要引入相关模块)
  
#13楼 2012-03-13 22:48 松下裤腰带  
>>> print sys.getdefaultencoding()
ascii
>>> print locale.getdefaultlocale()
('zh_CN', 'cp936')
>>> print locale.getpreferredencoding()
cp936
>>> 
谢谢。。。
  
#14楼[楼主2012-03-14 00:06 AstralWind  
@ scncpb
这个问题我搞不定…… 我会转给大牛们去分析的,有结论以后再通知你
  
#15楼[楼主2012-03-14 01:06 AstralWind  
@ scncpb
你再试试看print u'\xba\xba'会有什么结果?
  
#16楼 2012-03-14 16:17 松下裤腰带  
>>> print u'\xba\xba'
ºº
>>> 
谢谢哈。。。
  
#17楼[楼主2012-03-15 00:25 AstralWind  
@ scncpb
这个问题与python没有关系,你可以试试看把这些代码写入文件中执行,结果应该是正确的。
具体问题由于不在现场不好判断…
  
#18楼 2012-03-30 11:27 ade921068180  
楼主好:
像上面说的'\xe6\x97\x85\xe8\xa1\x8c' 
直接print 就可以正常显示“旅行” 
而我的系统默认编码是ascii,
sys.getdefaultencoding()
显示:acsii
我能不能理解为上面的字符串编码格式就是ascii呀?

还有,像这种情况,直接打印显示正常,但是我写到.html文件中,打开的时候,就显示乱码,是什么意思,怎么办呀?
楼主赐教~
  
#19楼 2012-04-02 10:28 ade921068180  
楼主,#为什么第一组总是4个字符呢"hell"呢

import re
pattern=re.compile(r'(\w+)(\w+)(?P<sign>.*)')
m=pattern.match('hello beautiful world! sdf asdf aaa bbb')
i=1
print m.groups()
while i<=len(m.groups()):
print m.group(i)
i=i+1

我感觉自己还是对(?P<sign>.*)不明白
  
#20楼 2012-04-21 10:45 mimicom  
UCS和Unicode是一回事么? 
怎么看到, (Universal Character Set,UCS) 这样的. 
UCS的U不是Unicode ?
  
#21楼 2012-05-21 20:48 Jesse_Luo  
@ ade921068180
大约是html需要指定字符集是utf-8,head里加上
<meta charset="UTF-8">
  
#22楼 2012-10-25 14:10 mimicom  
我又来看了一遍... 呵呵...
  
#23楼 2012-12-24 17:25 Honghe  
引用 而Linux里无法使用MBCS作为编码

为何?
  
#24楼 2013-09-06 20:42 itatkakaxi2  
大牛说话真幽默,很好,很强大
  
#25楼 2013-09-16 15:54 自加吧,少年  
非常有价值!
  
#26楼 2013-12-17 15:50 Arts&Crafts  
1
2
3
4
5
= '汉'
= u'汉'
s2 = u.encode('UTF-8')
print repr(s)       #输出   '\xe6\xb1\x89'
print repr(s2)     #输出   '\xe6\xb1\x89'


我有一个疑问:Python中默认的编码集为ASCII。如你所说 str 实际上是一个字节串,也就是说 s 指向的是一个 unicode 编码后的字节串。u 进行 UTF-8编码后出的字节串是
'\xe6\xb1\x89', 可是Python自动对 s 进行编码后字节串也是 '\xe6\xb1\x89'
也就是说Python自动按照UTF-8对'汉'进行编码,可是Python默认的编码集明明是ASCII这不是有点矛盾吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值