目录
ASCII码:(American Standard Code for Information Interchange,美国信息互换标准代码)
GBXXXX / DBCS(Double Byte Charecter Set 双字节字符集)
UNICODE (Universal Multiple-Octet Coded Character Set)(万国码)
3.1. 使用字符编码声明,并且同一工程中的所有源代码文件使用相同的字符编码声明。
3.3. 使用codecs.open()替代内置的open()。
3.4. 绝对需要避免使用的字符编码:MBCS/DBCS和UTF-16。
1.编码介绍
-
编码和解码
编码: 将字符按照对应的编码类型转换成计算机能够识别的0或者1(类似于生活中的:将中文翻译成英文的翻译官)
解码: 将0和1根据对应的解码类型转换成我们能够读懂的字符(类似于生活中的: 将英文翻译成中文的翻译官)
-
字符编码
定义: 将字符集中的字符编码(映射)成集合中的某一个对象如:比特模式、自然数序列、电脉冲等,以方便字符在计算机中存储和在计算机网络中传递。
-
字符集与字符编码的区别
字符集: 多个字符的集合。
字符编码: 将字符集中的字符映射为特定的字节或者字节序列,它表示的是一种规则。通常特定的字符集采用特定的编码方式(即一种字符集对应一种字符编码,如: ASCII、ISO-8859-1、GB2312、GBK都是表示了字符集又表示了对应的字符编码,但Unicode字符集是特例,它对应的字符编码有:UTF-8、UTF-16、UTF-32)
-
常见的字符集
ASCII、GB2312、Unicode、GBK等
-
ASCII码:(American Standard Code for Information Interchange,美国信息互换标准代码)
诞生:美国定制的交换标准,目的是将ASCII字符集包含的字符转换成计算机能够识别的二进制(0和1),它是最通用的信息交换标准,到目前为止总共定义了128个字符。开始计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。他们把其中的编号从0开始的32种状态分别规定了特殊的用途,因此0x20以下的字节状态称为"控制码"。所有的空 格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号。随着计算机的推广,世界上许多的字母里有许多是ASCII里没有的。于是人们采用 127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128 到255这一页的字符集被称"扩展字符集"。
特点:单字节,8位。
-
GBXXXX / DBCS(Double Byte Charecter Set 双字节字符集)
产生原因:ASCII码不支持中文。咱们国家计算机起步较晚,但我们使用计算机时已经没有可以利用的字节状态来表示汉字。
特点:两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,是以下三种GBxxx的统称。
GB2312 :是对 ASCII 的中文扩展。咱们国家计算机起步较晚,但我们使用计算机时已经没有可以利用的字节状态来表示汉字。 于是我们直接取消了127号之后的奇异符号们直接取消掉,并规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
GBK:汉字太多不够用。不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始。GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。
GB18030:由GBK扩展,又加了几千个新的少数民族的字。
-
UNICODE (Universal Multiple-Octet Coded Character Set)(万国码)
产生原因:各国编码标准不一样,为了统一标准。由ISO(国际标谁化组织)推出。
特点:在UNICODE 中,一个字符就是两个字节。由于计算机的存储器容量极大地发展了,空间再也不成为问题。规定必须用两个字节,也就是16位来统一表示所有的字符。对于ascii里的那些“半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于"半角"英文符号只需要用到低8位,所以其高 8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。
Unicode编码系统为表达任意语言的任意字符而设计,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
Unicode编码标准现在有三种具体实现,分别是:UTF-8、UTF-16、UTF-32。
(1) UTF-8字符编码:
- 它是实现了Unicode编码方案的一种可变长字符编码(定长码),也是一种前缀码。
- 它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部份修改,即可继续使用。
- UTF-8已经逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。
- 互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。
- UTF-8编码使用一至四个字节为每个字符编码(其中ASCII字符集中的128个字符只占1字节,还有附加符文的拉丁文、希腊文等需要2个字节,其他常用的文字占用3个字节,还有极少数的字符占用4个字节)。
(2) UTF-16字符编码:
- 它是实现了Unicode编码方案的一种可变长字符编码(定长码)
- 因为Unicode字符集中收录了很多字符,但是常用的一般不会超过65535个以外的字符,所以出现了UTF-16(2字节=16位)。
- UTF-16优点: 它在空间效率上比UTF-32高两倍,因为每个字符只需要2个字节来存储(除去65535范围以外的),而不是UTF-32中的4个字节。
- UTF-16缺点: 不兼容ASCII。
(3) UTF-32字符编码:
- 它是实现了Unicode方案的一种定长字符编码。
- 它使用4个字节的数字来表示每个字母、符号,或者表意文字(ideograph)。
- 优缺点: 使用4个字节存储每个字符,效率高,处理速度快(因为不用计算需要几个字节进行存储),但是浪费空间。
2.Python中的编码问题
Python2.x 中:
str格式本质含义是“某种编码格式”,绝大多数情况下,被引号框起来的字符串,就是str,这时的字符串编码类型,其实就是你Python文件的编码类型,比如在Windows里,默认用的是GBK编码。
unicode类型的含义就是“用unicode编码的字符串”。unicode()是单独的,不像str()是byte类型。如下所示:引号前面的u表示这里创建的是一个Unicode字符串
>>> s = u"我爱我的祖国" >>> s u'\u6211\u7231\u6211\u7684\u7956\u56fd' >>> print type(s) <type 'unicode'> >>> print chardet.detect(s) TypeError: Expected object of type bytes or bytearray, got: <type 'unicode'>
Python在进入2.0版后正式定义了了Unicode字符串这个奇怪的特性,目的就是为了处理太多种语言编码的文本。从那时开始,Python语言中的字符串类型就分为两种:一种是传统的Python字符串(各种花样编码),另一种则是新出现的Unicode。
Python3.x 中:
str就是unicode。默认编码是UTF-8。
str将不再是python2中的字节码,不能作为chardet.detect的参数。
import chardet s = "我爱我的祖国" print (type(s)) print (chardet.detect(s)) ''' <class 'str'> TypeError: Expected object of type bytes or bytearray, got: <class 'str'> '''
str也没有了解码方法decode()
s = "我爱我的祖国" print (hasattr(s, "decode")) print (hasattr(s, "encode")) ''' False True '''
Python2里面的编码和解码也就是 unicode和 str这两种形式的相互转化。编码是 unicode-> str,相反的,解码就是 str-> unicode。剩下的问题就是确定何时需要进行编码或者解码了.关于文件开头的"编码指示",也就是 # -*- coding: -*- 这个语句。Python 默认脚本文件都是 UTF-8 编码的,当文件中有非 UTF-8 编码范围内的字符的时候就要使用"编码指示"来修正.
字符串是由字符构成,字符在计算机硬件中通过二进制形式存储,这种二进制形式就是编码。如果直接使用 “字符串↔️字符↔️二进制表示(编码)” ,会增加不同类型编码之间转换的复杂性。所以引入了一个抽象层,“字符串↔️字符↔️与存储无关的表示↔️二进制表示(编码)” ,这样,可以用一种与存储无关的形式表示字符,不同的编码之间转换时可以先转换到这个抽象层,然后再转换为其他编码形式。在这里,unicode 就是 “与存储无关的表示”,utf—8 就是 “二进制表示”。
python2默认以ASCII编码,但是在实际编码过程中,我们会用到很多中文,为了不使包含中文的程序报错,也是为了符合国际通用惯例,一般将我们的文件编码设置为utf-8格式。设定编码的格式有很多种,一般的声明方式为 # -*- coding:utf-8 -*- 或者 # coding:utf-8
python2中字符串有两种表示形式,str和unicode。str可以理解为上面这段话中的二进制编码格式,unicode可以理解为抽象层。encode是编码,即从unicode格式到二进制的编码格式如utf-8、gb2312等。decode是解码,即从二进制编码格式到unicode编码格式。
python3 默认utf-8编码;一般不会出现中文乱码问题。
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'汉' printrepr(u) # u'\u6c49' s =u.encode('UTF-8') printrepr(s) # '\xe6\xb1\x89' u2 =s.decode('UTF-8') printrepr(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。参见codecs — Codec registry and base classes — Python 3.11.2 documentation。
另外需要注意的是声明的编码必须与文件实际保存时用的编码一致,否则很大几率会出现代码解析异常。现在的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() printtype(s) # # 已知是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 importcodecs f =codecs.open('test.txt', encoding='UTF-8') u =f.read() f.close() printtype(u) # f =codecs.open('test.txt', 'a', encoding='UTF-8') # 写入unicode f.write(u) # 写入str,自动进行解码编码操作 # GBK编码的str s ='汉' printrepr(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 importsys importlocale defp(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是不推荐使用的编码,这里仅作测试表明为什么不应该用 printr"'\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用的相当少,但用到的时候还是需要注意。