一:字符编码简介
1:ASCII
最初的计算机的使用是在美国,所用到的字符也就是现在键盘上的一些符号和少数儿个特殊的符号,一个字节所就能足以容纳所有的这些字符,实际上表示这些字符的字节最高位都为0,也就是说这些字节都在0到127之间,如字符a对应数字97。这套编码规则被称为ASCII(美国标准信息交换码)。
2:GBK、GB2312
随着计算机的应用和普及,许多国家都把本地的字符集引入了计算机,大大扩展了计算机中字符的范围。以中文为例,一个字节是不能容纳所有的中文汉字的,因此大陆将每一个中文字符都用两个字节的数字来表示,原有的ASCII字符的编码保持不变,仍用一个字节表示,为了将一个中文字符与两个ASCII码字符相区别,中文字符的每个字节的最高位都为1,这套编码规则称为GBK(国标码),后来,又在GBK的基础上对更多的中文字符(包括繁体)进行了编码,新的编码系统就是GB2312,可见GBK是GB2312的子集。
3:Unicode
每个国家和地区都制定了一套自己的编码,那么同样的一个字节,在不同的国家和地区就代表了不同的字符。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。
为了解决各个国家和地区使用本地化字符编码带来的不利影响,将全世界所有的符号进行了统一编码,称之为Unicode编码。这是一种所有符号的编码。如 “中”这个符号,在全世界的任何角落始终对应的都是一个十六进制的数字4e2d。
最初的Unicode标准UCS-2使用两个字节表示一个字符,所以你常常可以听到Unicode使用两个字节表示一个字符的说法。但过了不久有人觉得256*256太少了,还是不够用,于是出现了UCS-4标准,它使用4个字节表示一个字符,不过我们用的最多的仍然是UCS-2。
4:UTF
注意,UCS(Unicode Character Set)还仅仅是字符对应码位的一张表而已,比如"中"这个字的码位是4E2D。字符具体如何传输和储存则是由UTF(UCS Transformation Format)来负责。
一开始这事很简单,直接使用UCS的码位来保存,这就是UTF-16,比如,"汉"直接使用x4Ex2D保存(UTF-16-BE),或是倒过来使用x2Dx4E保存(UTF-16-LE)。
这里就有两个严重的问题,第一个问题是,如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第二个问题是,英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用两个字节表示,这对于存储来说是极大的浪费。于是UTF-8横空出世。
5:UTF-8
UTF-8是使用最广的一种Unicode的实现方式。其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。
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编码方式 |
00 00 00 00 - 00 00 00 7F |
0xxxxxxx |
00 00 00 80 - 00 00 07 FF |
110xxxxx 10xxxxxx |
00 00 08 00 - 00 00 FF FF |
1110xxxx 10xxxxxx 10xxxxxx |
00 01 00 00 - 00 10 FF FF |
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
跟据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面以汉字"严"为例,演示如何实现UTF-8编码。
已知"严"的unicode是4E25(1001110 00100101),根据上表,可以发现4E25处在第三行的范围内(00000800-0000 FFFF),因此"严"的UTF-8编码需要三个字节,即格式是"1110xxxx 10xxxxxx 10xxxxxx"。然后,从"严"的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,"严"的UTF-8编码是"11100100 10111000 10100101",转换成十六进制就是E4B8A5。
6:BOM
BOM(Byte Order Mark)。UTF引入了BOM来表示自身编码,如果一开始读入的几个字节是其中之一,则代表接下来要读取的文字使用的编码是相应的编码:
BOM_UTF8 'xefxbbxbf'
BOM_UTF16_LE 'xffxfe'
BOM_UTF16_BE 'xfexff'
如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。以汉字"严"为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。
一般情况下,不建议在Linux中使用BOM。
7:Unicode与UTF-8之间的转换
"严"的Unicode码是4E25,UTF-8编码是E4B8A5,以该汉字为例,利用Ultraedit查看各种编码的具体值,一般的编辑器都支持以不同的编码方式保存文本,编码方式如下:
1)ANSI是默认的编码方式。对于英文文件是ASCII编码,对于简体中文文件是GB2312编码(只针对Windows简体中文版,如果是繁体中文版会采用Big5码)。这种方式编码的文件,就是两个字节"D1 CF",这正是"严"的GB2312编码:
2)UTF16编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。编码是四个字节"FF FE 25 4E",其中"FF FE"表明是小头方式存储:
3)UTF16-NOBOM编码与上一个选项相对应,只不过没有BOM编码,也就是文件中只存储UTF16编码"25 4E":
4)UTF-8编码,文件中共6个字节:"EF BB BF E4 B8 A5",其中前三个为BOM编码,后三个为UTF8编码:
6)UTF8-NOBOM编码与上一个选项相对应,只不过没有BOM编码,也就是文件中只存储UTF8编码" E4 B8 A5":
二:Python代码文件的编码
Python解释器会使用某种编码方式来解释Python源代码文件,默认情况下,这种编码方式就是ASCII。
Python2.1版本,在Python源码文件中,只能以以基于Latin-1的“转义unicode”的方式来书写Unicode字符,这对于亚洲的程序员是很不友好的。解决该问题的方法是,在源码文件的顶部,使用某种特殊的注释方式来表明源码文件的编码。
为了表明源码文件的编码,这种特殊的注释必须位于源码文件的第一行或第二行,类似于:
#coding=<encodingname>
或:
#!/usr/bin/python # -*- coding:<encoding name> -*-
或:
#!/usr/bin/python # vim: setfileencoding=<encoding name> :
这种方式的本质是:文件的第一行或第二行必须能匹配正则表达式:
” coding[:=]s*([-w.]+)”,该表达式中的group1就会被解释为编码名称,如果Python无法识别该编码,则在编译时就会报错。
像Windows这样的平台,会在Unicode文件的最开始加上BOM字节码,UTF-8文件的字节码是:”xefxbbxbf”。为了兼容这种方式,包含这种字节码的文件,即使没有字节编码注释,也会被解释为”utf-8”。如果一个源码文件,既有编码注释,又有UTF-8的BOM字节码,则在编码注释中的编码名称只能是”utf-8”(”utf8”都不行),否则会报错: “SyntaxError: encodingproblem: utf-8”。
下面是一些使用编码注释的例子:
1. Emacs风格的文件编码注释:
#!/usr/bin/python # -*- coding: latin-1 -*- import os, sys ...
#!/usr/bin/python # -*- coding: iso-8859-15 -*- import os, sys ...
#!/usr/bin/python # -*- coding: ascii -*- import os, sys ...
2. 使用纯文本方式的注释:
# This Python file uses the following encoding:utf-8 import os, sys ...
3. 没有编码注释,则Python解释器默认使用ASCII。
如果没声明编码,但是文件中又包含非ASCII编码的字符的话,python解析器去解析的python文件,就会报错。
#!/usr/local/bin/python import os, sys ...
4. 下面这些语法不起作用
没有加”coding:”前缀:
#!/usr/local/bin/python # latin-1 import os, sys ...
编码方式不在第一行或第二行:
#!/usr/local/bin/python # # -*- coding: latin-1 -*- import os, sys ...
不支持的编码方式:
#!/usr/local/bin/python # -*- coding: utf-42 -*- import os, sys ...
5:注意:
允许的编码方式包括ASCII兼容编码以及某些多字节编码,比如SHIFT_JIS。但不包括为所有字符都是有双字节或者更多字节的编码,比如UTF-16(注:也就是通常说的Unicode,但SHIFT_JIS也好,GBK也好,因为兼容ASCII编码,所以都可以在Python源文件里使用)。
如果声明的编码与实际不符(就是说,文件实际上是以另外的编码保存的),出错的可能性很大。
文件的编码格式决定了在该源文件中声明的字符串的编码:
str = '哈哈' print repr(str)
a.如果文件格式为utf-8,则str的值为:'xe5x93x88xe5x93x88'(哈哈的utf-8编码)
b.如果文件格式为gbk,则str的值为:'xb9xfexb9xfe'(哈哈的gbk编码)
三:str和unicode
str和unicode都是basestring的子类。编码是指unicode-->str,解码是指str-->unicode。
str是一个字节数组,这个字节数组表示的是对unicode对象编码(可以是utf-8、gbk、cp936、GB2312)后的存储的格式。这里它仅仅是一个字节流,没有其它的含义,如果你想使这个字节流显示的内容有意义,就必须用正确的编码格式,解码显示。 对UTF-8编码的str'哈哈'使用len()函数时,结果是6,因为实际上,UTF-8编码的'哈哈' == 'x e5x93x88xe5x93x88'。
unicode才是真正意义上的字符串,对字节串str使用正确的字符编码进行解码后获得,例如'哈哈'的unicode对象为 u'u54c8u54c8' ,len(u”哈哈”) == 2
字符串在Python内部的表示是unicode编码,因此,在做编码转换时,通常需要以unicode作为中间编码,即先将其他编码的字符串解码(decode)成unicode,再从unicode编码(encode)成另一种编码。
decode的作用是将其他编码的字符串转换成unicode编码,如str1.decode('gb2312'),表示将gb2312编码的字符串str1转换成unicode编码。
encode的作用是将unicode编码转换成其他编码的字符串,如str2.encode('gb2312'),表示将unicode编码的字符串str2转换成gb2312编码。
因此,转码的时候一定要先搞明白,字符串str是什么编码,然后decode成unicode,然后再encode成其他编码
str.decode([encoding[, errors]])
使用encoding指示的编码,对str进行解码,返回一个unicode对象。默认情况下encoding是“字符串默认编码”,比如ascii。
errors指示如何处理解码错误,默认情况下是”strict”,也就是遇到解码错误时,直接抛出UnicodeError异常。其他的errors值可以有”ignore”,”replace”等。
unicode(object[, encoding[, errors]])
与str.decode作用相同,但是该方法要快一些:http://stackoverflow.com/questions/440320/unicode-vs-str-decode-for-a-utf8-encoded-byte-string-python-2-x
str.encode([encoding[, errors]])
返回一个经encoding编码后的str对象,默认的encoding是”默认字符串编码”。
errors指示如何处理解码错误,默认情况下是”strict”,也就是遇到解码错误时,直接抛出UnicodeError异常。其他的errors值可以有”ignore”,”replace”,'xmlcharrefreplace', 'backslashreplace'等。
一般情况下,对Unicode对象进行编码encode(),返回str对象。对str对象调用解码decode(),返回unicode对象。
但是对str调用encode也不会报错,str.encode()实际上就等价于str.decode(sys.defaultencoding).encode().而sys.defaultencoding一般是ascii。
同样的,对unicode对象进行解码unicode.decode实际上就等价于unicode.encode(sys.defaultencoding).decode()
四:示例
英文字符的ASCII、UTF-8、GBK等编码都是一样的,因此下面的示例仅讨论汉字的情况。并且保证源码文件的实际编码方式,和编码注释是一样的。
1:文件为默认编码(ASCII),无编码注释
astr ="哈哈"
运行文件,报错:SyntaxError: Non-ASCII character 'xb9' in file 2.py on line 2, butno encoding declared; seehttp://www.python.org/peps/pep-0263.htmlfor details
原因:默认的文件编码为ASCII,这种编码无法处理汉字,将文件保存为GBK或者UTF-8格式,并相应的注释声明。
2:文件编码为UTF-8
# -*- coding:UTF-8 -*- def toHexString(s): return ":".join("{0:x}".format(ord(c))for c in s) ustr =u"哈哈" print repr(ustr) print ustr, "len is ",len(ustr) print astr ="哈哈" ustr =astr.decode("UTF-8") print repr(ustr) print ustr, "len is ",len(ustr)
运行文件,结果为:
u'u54c8u54c8' 哈哈 len is 2 u'u54c8u54c8' 哈哈 len is 2
可见,上下两种方式,其实是等价的,在源码文件中直接使用u”...”编写Unicode字符串,使用文件的注释编码,将该str解码为Unicode:
Python supports writing Unicode literals in anyencoding, but you have to declare the encoding being used. This is done byincluding a special comment as either the first or second line of the sourcefile。
(https://docs.python.org/2/howto/unicode.html#the-unicode-type)
3:文件为UTF-8编码
# -*- coding:utf-8 -*- astr ="哈哈" print repr(astr) print astr, "len is ",len(astr) print ustr =astr.decode("gbk") print repr(ustr) print ustr, "len is ",len(ustr)
运行文件,结果如下:
'xe5x93x88xe5x93x88' 鍝堝搱 len is 6
(第一行输出:因为文件为UTF-8编码,所以输出”哈哈”的UTF-8编码)
(第二行输出:因为print语句它的实现是将要输出的内容传送给终端,终端会根据默认的编码(GBK)对输入的字节流进行解释,因为 'xe5x93x88xe5x93x88'用GBK去解释,其显示的出来就是“鍝堝搱”,而且,该str的长度就是编码的长度,为6)
u'u935du581du6431' 鍝堝搱 len is 3
(第一行输出:因为文件是UTF-8编码,但是却用GBK对“哈哈”,也就是'xe5x93x88xe5x93x88'进行解码,所以输出错误)
(第二行输出:Python在向控制台输出unicode对象的时候会自动根据输出环境的编码(GBK)进行转换,而如果输出的不是unicode对象而是普通的str字符串,则会直接按照该字符串的编码输出字符串(http://noalgo.info/578.html)。所以,printustr就等价于print astr了。如果将ustr = astr.decode("gbk") 替换成 ustr =astr.decode("UTF-8"),则输出:
u'u54c8u54c8' 哈哈 len is 2 54c8:54c8
这是因为将astr按照文件编码注释声明的编码方式进行解码,解码成Unicode之后,print时又自动进行GBK编码,所以会输出正确的字符)
4:打开具有汉字的文件:
文件名可以包含Unicode字符,操作系统在处理这样的文件时,将Unicode字符根据某种编码进行处理,比如Max OS X使用UTF-8编码,而Windows上的编码是可配置的,用”mbcs”来表示当前的编码。
sys.getfilesystemencoding()函数可以得到当前文件系统使用的编码,但是其实可以不用管这些,当需要打开一个包含汉字的文件时,直接使用Unicode字符即可,这样它会自动转换为正确的编码字符串:
# -*- coding:UTF-8 -*- #astr = "哈哈.txt".decode("UTF-8") #astr = u"哈哈.txt" astr ="哈哈.txt".decode("UTF-8").encode("GBK") try: with open(astr) asfobj: print fobj.read() except Exception, e: print "error is ",e
上面这三种方式,都可以正确打开该文件。
参考:
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
http://www.cnblogs.com/huxi/articles/1897271.html
https://www.python.org/dev/peps/pep-0263/
http://www.crifan.com/files/doc/docbook/char_encoding/release/html/char_encoding.html
http://www.jb51.net/article/26543.htm
http://www.jb51.net/article/17560.htm