Python深入:编码问题总结

时间:2022-05-10 10:06:53

转自:http://blog.csdn.net/gqtcgq/article/details/47068817

一:字符编码简介

         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,比如,"汉"直接使用\x4E\x2D保存(UTF-16-BE),或是倒过来使用\x2D\x4E保存(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             '\xef\xbb\xbf'

BOM_UTF16_LE    '\xff\xfe'

BOM_UTF16_BE    '\xfe\xff'

         如果一个文本文件的头两个字节是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编码:

Python深入:编码问题总结

         2)UTF16编码指的是UCS-2编码方式,即直接用两个字节存入字符的Unicode码。编码是四个字节"FF FE 25 4E",其中"FF FE"表明是小头方式存储:

Python深入:编码问题总结

         3)UTF16-NOBOM编码与上一个选项相对应,只不过没有BOM编码,也就是文件中只存储UTF16编码"25 4E":

Python深入:编码问题总结

         4)UTF-8编码,文件*6个字节:"EF BB BF E4 B8 A5",其中前三个为BOM编码,后三个为UTF8编码:

Python深入:编码问题总结

         6)UTF8-NOBOM编码与上一个选项相对应,只不过没有BOM编码,也就是文件中只存储UTF8编码" E4 B8 A5":

Python深入:编码问题总结

二:Python代码文件的编码

Python解释器会使用某种编码方式来解释Python源代码文件,默认情况下,这种编码方式就是ASCII。

Python2.1版本,在Python源码文件中,只能以以基于Latin-1的“转义unicode”的方式来书写Unicode字符,这对于亚洲的程序员是很不友好的。解决该问题的方法是,在源码文件的顶部,使用某种特殊的注释方式来表明源码文件的编码。

为了表明源码文件的编码,这种特殊的注释必须位于源码文件的第一行或第二行,类似于:

  1. #coding=<encodingname>

或:

  1. #!/usr/bin/python
  2. # -*- coding:<encoding name> -*-

或:

  1. #!/usr/bin/python
  2. # vim: setfileencoding=<encoding name> :

这种方式的本质是:文件的第一行或第二行必须能匹配正则表达式:

” coding[:=]\s*([-\w.]+)”,该表达式中的group1就会被解释为编码名称,如果Python无法识别该编码,则在编译时就会报错。

像Windows这样的平台,会在Unicode文件的最开始加上BOM字节码,UTF-8文件的字节码是:”xef\xbb\xbf”。为了兼容这种方式,包含这种字节码的文件,即使没有字节编码注释,也会被解释为”utf-8”。如果一个源码文件,既有编码注释,又有UTF-8的BOM字节码,则在编码注释中的编码名称只能是”utf-8”(”utf8”都不行),否则会报错:

“SyntaxError: encodingproblem: utf-8”。

下面是一些使用编码注释的例子:

1. Emacs风格的文件编码注释:

  1. #!/usr/bin/python
  2. # -*- coding: latin-1 -*-
  3. import os, sys
  4. ...
  1. #!/usr/bin/python
  2. # -*- coding: iso-8859-15 -*-
  3. import os, sys
  4. ...
  1. #!/usr/bin/python
  2. # -*- coding: ascii -*-
  3. import os, sys
  4. ...

2. 使用纯文本方式的注释:

  1. # This Python file uses the following encoding:utf-8
  2. import os, sys
  3. ...

3. 没有编码注释,则Python解释器默认使用ASCII。

如果没声明编码,但是文件中又包含非ASCII编码的字符的话,python解析器去解析的python文件,就会报错。

  1. #!/usr/local/bin/python
  2. import os, sys
  3. ...

4. 下面这些语法不起作用

没有加”coding:”前缀:

  1. #!/usr/local/bin/python
  2. # latin-1
  3. import os, sys
  4. ...

编码方式不在第一行或第二行:

  1. #!/usr/local/bin/python
  2. #
  3. # -*- coding: latin-1 -*-
  4. import os, sys
  5. ...

不支持的编码方式:

  1. #!/usr/local/bin/python
  2. # -*- coding: utf-42 -*-
  3. import os, sys
  4. ...

5:注意:

允许的编码方式包括ASCII兼容编码以及某些多字节编码,比如SHIFT_JIS。但不包括为所有字符都是有双字节或者更多字节的编码,比如UTF-16(注:也就是通常说的Unicode,但SHIFT_JIS也好,GBK也好,因为兼容ASCII编码,所以都可以在Python源文件里使用)。

如果声明的编码与实际不符(就是说,文件实际上是以另外的编码保存的),出错的可能性很大。

文件的编码格式决定了在该源文件中声明的字符串的编码,str = '哈哈'

print repr(str)

a.如果文件格式为utf-8,则str的值为:'\xe5\x93\x88\xe5\x93\x88'(哈哈的utf-8编码)

b.如果文件格式为gbk,则str的值为:'\xb9\xfe\xb9\xfe'(哈哈的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 e5\x93\x88\xe5\x93\x88'。

unicode才是真正意义上的字符串,对字节串str使用正确的字符编码进行解码后获得,例如'哈哈'的unicode对象为 u'\u54c8\u54c8' ,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://*.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),无编码注释

  1. 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

  1. # -*- coding:UTF-8 -*-
  2. def toHexString(s):
  3. return ":".join("{0:x}".format(ord(c))for c in s)
  4. ustr =u"哈哈"
  5. print repr(ustr)
  6. print ustr, "len is ",len(ustr)
  7. print
  8. astr ="哈哈"
  9. ustr =astr.decode("UTF-8")
  10. print repr(ustr)
  11. print ustr, "len is ",len(ustr)

运行文件,结果为:

u'\u54c8\u54c8'

哈哈 len is  2

u'\u54c8\u54c8'

哈哈 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编码,

  1. # -*- coding:utf-8 -*-
  2. astr ="哈哈"
  3. print repr(astr)
  4. print astr, "len is ",len(astr)
  5. print
  6. ustr =astr.decode("gbk")
  7. print repr(ustr)
  8. print ustr, "len is ",len(ustr)

运行文件,结果如下:

'\xe5\x93\x88\xe5\x93\x88'

鍝堝搱 len is  6

(第一行输出:因为文件为UTF-8编码,所以输出”哈哈”的UTF-8编码)

(第二行输出:因为print语句它的实现是将要输出的内容传送给终端,终端会根据默认的编码(GBK)对输入的字节流进行解释,因为 '\xe5\x93\x88\xe5\x93\x88'用GBK去解释,其显示的出来就是“鍝堝搱”,而且,该str的长度就是编码的长度,为6)

u'\u935d\u581d\u6431'

鍝堝搱 len is  3

(第一行输出:因为文件是UTF-8编码,但是却用GBK对“哈哈”,也就是'\xe5\x93\x88\xe5\x93\x88'进行解码,所以输出错误)

(第二行输出:Python在向控制台输出unicode对象的时候会自动根据输出环境的编码(GBK)进行转换,而如果输出的不是unicode对象而是普通的str字符串,则会直接按照该字符串的编码输出字符串(http://noalgo.info/578.html)。所以,printustr就等价于print astr了。如果将ustr = astr.decode("gbk") 替换成 ustr =astr.decode("UTF-8"),则输出:

u'\u54c8\u54c8'

哈哈 len is  2

54c8:54c8

这是因为将astr按照文件编码注释声明的编码方式进行解码,解码成Unicode之后,print时又自动进行GBK编码,所以会输出正确的字符)

4:打开具有汉字的文件:

文件名可以包含Unicode字符,操作系统在处理这样的文件时,将Unicode字符根据某种编码进行处理,比如Max OS X使用UTF-8编码,而Windows上的编码是可配置的,用”mbcs”来表示当前的编码。

sys.getfilesystemencoding()函数可以得到当前文件系统使用的编码,但是其实可以不用管这些,当需要打开一个包含汉字的文件时,直接使用Unicode字符即可,这样它会自动转换为正确的编码字符串:

  1. # -*- coding:UTF-8 -*-
  2. #astr = "哈哈.txt".decode("UTF-8")
  3. #astr = u"哈哈.txt"
  4. astr ="哈哈.txt".decode("UTF-8").encode("GBK")
  5. try:
  6. with open(astr) asfobj:
  7. print fobj.read()
  8. except Exception, e:
  9. 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