在网上可以找到很多博文,解答了python代码字符编码的问题,讲得都挺详细的,我这里不想再细说那些内容,而是讨论一下我感兴趣的方面。本文会涉及unicode编码及其程序转换格式utf-8、utf-16,若是对这些编码感兴趣,可以参看百科,里面十分详尽:
另外,本文中出现的源文件代码、控制台代码、运行结果,如无特殊说明,都是在pyscripter
portable2.7.3.1中编辑、运行得到的。操作系统为win10专业版64bit,区域为中国内地,系统语言为中文简体。不妨先假定本文得出的结论不受程序环境影响,而是python2.7原原本本的特性。
下面开始正文。
一、{str}和{unicode}
在python2.x当中,其实可以说有两种字符串对象,一种叫{str},另一种叫{unicode},图1表示这两种对象的两个常量实例(在python中{unicode}常量以前置的’u’来表示),可以看到它们的类型。
图1 {str}和{unicode}的类型图2 {str}和{unicode}的长度
{str}是我们常说的字符串,当我们输入输出或读写文件时,都会用到这种对象。它可以是各种各样的编码,比如gbk、utf-8、ascii等,然而{str}实际上只是字节数组,从图2中就可以看出,它返回的是字节数量。python程序并不知道也并不关心这个{str}对象用的什么编码,一切都要看程序员心里有没有[哔——]数了。
与{str}相比,{unicode}才更像字符串,求长度可以真正得到字符数量,取子串也不怕会把一个字从中砍断。只是,我们知道unicode只是一种抽象编码,只能以某种utf的形式真正出现在代码或文本里,所以{unicode}仅是python程序内部利用unicode编码对字符串的一种封装,不能直接输出或写入文件。
二、交互中的编码
很多博文都讲得很明白了,{str}.decode()解码为{unicode},{unicode}.encode()编码为{str},这些都需要人为、显式地指定编码才能进行转换,所以不难看懂,但是除此以外呢,有没有自动转换编码或隐式指定编码的情况?下面讨论了几种这类情况,都是很常见很基本的,我自己也就到这儿为止了,想要更加深入学习的看官还请另寻他处。
1、源代码的编码——声明源代码编码是个好习惯
前面提到python鸟都不会鸟{str}是什么编码,这是有例外的。当我使用如u”某某”这样的{unicode}常量时,python会在编译时以某种编码把”某某”解码为{unicode}再读入内存,那么那个编码是什么呢?
比如代码文件用utf-8保存,就是说这个”某某”是utf-8编码的,那我肯定希望用utf-8来解码,从而得到正确的结果,就是说编译时用源文件编码来解码。那么python怎么知道源文件用什么编码呢?
如果写代码的人一个屁都不放,python就默认以为源代码文件是用ascii保存的,会以ascii来解读”某某”,这肯定是不合适的,所以需要告诉python这个py文件是用的什么编码。一种方法就是在文件头部加上声明# -*- coding: xxx -*-,在这个例子中,要加这样子的——# -*- coding: utf-8 -*-,声明文件以utf-8保存。
以上只是就理论而言
,然而实际上
……
图3
以utf-8保存且没有进行编码声明的代码及其运行结果
(解释一下print([string])这种用法:这样相当于print(repr(string)),会打印出string的十六进制表示,引号前带’u’的表示,引号里面’\x’开头表示以1字节为1单位,如果该字节在0-127范围内就显示成ascii字符,’\x’也不显示了,’\u’表示以2字节为1单位,’\U’表示以4字节为1单位。)
(1)注1:上面说了这么多其实都是废话
,因为python现在已经不默认源代码编码了。如图3所示,这个py文件我以utf-8保存,且没有进行编码声明,按照上面的理论string2应该是把”汉字”用ascii进行解码,变成6个字符。然而事实上从第2、4、7、9条打印内容可以看出,string确实是用的utf-8编码,string2的解码结果也正确,确实是用utf-8解码。另外,如果我声明这个文件是用gbk保存的,python会扔出“编码错误:文件编码为utf-8”。这两件事都说明至少在编译时python现在已经会自己检测文件编码了
。
(2)注2:还是同一个文件(图3),来看看我们能不能得出其他结论。从打印内容第3、5条可以看出,unicode()函数用了gbk来解码;本文后面会说明,{unicode}在打印时会先自动用控制台编码来编码再输送到控制台,而{str}不处理直接送,再来看看打印内容第6、8条,结果相同,说明string3用控制台编码编码后会变为string,而string3是string用gbk解码来的,这意味着控制台编码就是gbk;最后看看string6怎么来的,最后3条打印内容,我们发现{str}+{unicode}时,python会把{str}解码为{unicode}再相加得到{unicode},而且这里用的也是gbk。在代码中加了编码是utf-8的声明后,运行结果仍是一样。好的我们小结一下,unicode()用gbk,控制台用gbk,类型转换时也用gbk,操作系统是中文的,我们可以推论了——①控制台使用系统语言环境编码;②代码执行中需要解码时默认使用系统语言环境编码(或该函数定义所在文件使用的编码?),与文件声明编码无关。
(3)注3:在用utf-16
with BOM编码源代码时遇到了问题,多次尝试总结如下——
①不管有无编码声明,都无法直接用python.exe运行,说在文件开头发现了非ascii字符;
②为了避开①用了pyscripter的解释器,没有编码声明可以运行,有编码声明反而出现语法错误;
③用pyscripter的解释器在没有编码声明的情况下运行,发现本应是utf-16的{str}变成了gbk(见图4打印内容第6条,string未经处理打印成功,已知控制台编码为gbk),而用其他途径查看源文件编码,{str}在文件中确实是utf-16的;
④通过打印内容第2、5条发现,string2既没有用utf-16解码也没有用gbk解码,而是用了iso8859-1。
图4
以utf-16保存且没有进行编码声明的代码及其运行结果
试着解答。对于问题,或许只能说python.exe的牛皮只能吹到utf-8而已,对utf-8编码的文件还能自主识别,但对utf-16就束手无策了。束手无策了怎么办?回到一开始的理论——“万能”的ascii——既然我识别不了你又“不给编码声明”,那别怪我照章办事,直接把你当做ascii——自然就出错了。诶?为什么当成ascii文件开头就会出错?为什么我给了编码声明还是一样?因为utf-16的BOM啊。BOM出现在文件头,就算加了编码声明,也是在BOM后面,而BOM第一个字符是”0xff”或”0xfe”,明显不是ascii,所以python根本没看到编码声明就报错了。那么如果我用不带BOM的utf-16保存文件,再加个编码声明呢?应该还是没用。因为python一开始识别不出文件是什么编码的,所以编码声明必须兼容ascii,否则python连声明都读不懂。我们来演一下——源代码给python,好,开始读吧——如果是utf-8、gbk等兼容ascii的码,第一个字符,”#”,很好,能懂,然后一直把编码声明读完,哦~原来是utf-8/gbk啊,哦了;但是utf-16呢,第一个字符,”0x00”,null
byte[黑人问号],后面,”#”,懂了,再来,”0x00”,又null[黑人问号],然后……每个有意义的字符间都夹了个”0x00”,你让python怎么处理?忽略吗?竟然要先知道是什么编码才能读懂说明是什么编码的声明,你让python情何以堪?不过话说回来,在用命令行执行时或许可以加上说明编码的参数,这样python就能读懂源代码了。我没有研究过能加什么参数,怎么加,这办法或许可行,也可能不可行。
就算用命令行参数能让python读懂代码,在运行时utf-16还是会有很大问题,就比如图4中的decode(“gbk”)这一句——
python知道代码是utf-16的,所以能读懂你是要执行decode()这个函数,可是对于作为参数的{str},python不会用什么编码去解读它,而是原样放入内存(唯一会解读的情况是转义字符比如”\\”,源代码里是用两个”\”存储的,而python在编译时会理解到程序员想要表达一个实在的”\”,会把一个”\”而不是两个”\”放入内存,但是这个适用于utf-16吗?)。decode()执行时,就把参数拿过来,像是”gbk”这种英文字符,用兼容ascii的编码函数一般都读得懂,知道要用gbk解码,但是decode()就不能理解utf-16编码的”gbk”,因为他不接受参数含有”0x00”字节。
所以说,python对ascii的依赖根深蒂固,基本上没法用utf-16。这件事pyscripter应该也是知道的,故对于问题②~④,我有两个合理的猜想——
猜想一:pyscripter知道python玩不转utf-16,把源代码先转码为gbk,再交给python.exe编译执行。这样就说明了问题②——python能解读gbk,故没有声明能运行,声明了utf-16后反而与实际编码不符报错;也解决了问题③——源代码中的{str}常量被转为了gbk编码。
但是string2=u”汉字”怎么就用了iso8859-1来解码?
猜想二:python错误地将gbk编码的源代码检测为了iso8859-1。因而编译时把”汉字”用iso8859-1解码。
2、控制台的编码——都打印{unicode},肯定不会错
前文提到了,{unicode}在打印时会先自动用控制台编码来编码再输送到控制台,而{str}不处理直接送,这个容易出现问题。举个例,{str}是utf-8的,控制台是gbk的,来吧。
图5
不同编码在控制台打印的结果
输出的流程见图5,可以断定,如果{str}编码和控制台编码不同,输出的结果可能就会是乱码,想解决要么把{str}解码,要么采用和控制台相同的编码。因为不是什么时候都知道控制台编码的,所以最保险的办法就是只打印{unicode},让python自己编码去。
(1)注1:刚刚说不知道控制台编码,也就是说不知道操作系统语言环境的编码,那有没有略窥一二的办法呢?有,如下——
import sys
sys.getdefaultencoding()
# 返回系统使用的字符编码(本机为cp936,似乎就是gbk)
sys.getfilesystemencoding()
# 返回文件系统使用的编码(本机为mbcs,似乎是gb2312)
有没有设置环境编码的办法呢?有,但是有效性无法保证,如下——
import sys
reload(sys) #
重载一下,不然无法设置,可能会报错
sys.setdefaultencoding('utf-8') # 将系统使用的编码设置为utf-8
3、读写文件的编码——保证编码一致
本文只涉及python读写文件的基本语句——open()、write()、read()、close()。read()只是把文件字节流读出来存成{str},与编码无关;close()就更没关系了。但是,open()要提供文件路径,write()要提供写入内容,这些参数就跟编码有关了。先说结论吧,如果路径或内容等参数是{str},则会直接按着这个{str}来寻找文件或者直接把{str}写入文件;如果路径参数是{unicode},则会把它用文件系统编码来编码成{str}后再寻找文件,如果写入内容是{unicode},则会把它用系统语言环境编码(或该函数定义所在文件使用的编码?)来编码成{str}后再写入文件,与代码文件声明编码无关。举个例子,见图6:
图6
utf-8编码且有声明的源代码,完成打开并写入文件再读取的操作,以及其执行结果
解释一哈。源代码是utf-8的,所以测试utf-8的有关参数都是utf-8编码的{str},根据前面说的结论,一开始直接用utf-8编码创建了测试文件的文件名,但文件系统编码是gbk啊,来解读utf-8编码的文件名肯定就是一堆乱码了;再看看控制台第1条输出,说明文件里写入的内容编码为utf-8,符合结论。再看测试unicode的部分,系统显示文件名正常,读取文件能成功,说明文件名没错,这个文件名是gbk编码的,就说明创建文件时{unicode}被用gbk编了码;读取到的内容也能被gbk良好解码,说明写入的{unicode}也被用gbk编了码,结论得以验证。(其实根据结论,open()里的{unicode}应该被gb2312编码,它是被gbk兼容的(有例外如下文注3),所以图6最后一组以gbk来编码也无可厚非)
这件事告诉我们,文件名编码和文件内容编码不一定一致,文件内容前后编码也不一定一致,我可以用utf-8创建文件名,先写进一段gbk,再写进一段utf-8,python吱一声都不会,全靠程序员自己注意,所以要万分小心。
看到这里或许可以推论,原则上{unicode}只出现在python程序内部,涉及到与外界交互时,都会自动隐式地进行编码。
(1)注1:os.listdir(path)也会自动转换编码,从而保证输入输出的类型是一样的。如果path是{unicode},那就先用文件系统编码(明面上可能是gb2312,但鉴于目录名可以用繁体、日文等等超出gb2312的字符,可能是gb2312和gbk混合使用。为什么不能单纯是gbk?见注3)来编码成{str}后再搜索路径,然后把找到的目录名用文件系统编码来解码后存为{unicode}的列表;如果path是{str},那么就直接用来搜索路径,然后才是问题的关键——它并没有把路径下的目录名直接读取过来,而是不知怎的读取到了用文件系统编码解码后的目录名,然后用gbk编码后(无法识别的字符用’?’代替)存为{str}的列表。怎么读取到unicode先不问了,为什么会用gbk编码而不用文件系统编码来编码?
(2)注2:一个题外话——pyscripter的问题。当它运行源代码时会把代码文件所在路径用文件系统编码正确解码为{unicode},再用gbk编码后交给python.exe编译执行。如果路径里有gbk无法编码的字符,就会出现unicode error而失败。问题就在于为什么解码的时候很正常,编码的时候非要用gbk呢?不该也用文件系统编码吗?
(真可怕。前面推论说{unicode}不会用到python外,或许是错的。假设用{unicode}作为路径来搜索目录或打开文件时(这时肯定调用了系统API),不是由python编码后交给系统API,而是直接把{unicode}给了系统API,API用文件系统编码处理后再去完成目的,如果需要返回给python一些东西的话,就解码后把{unicode}返回给python;跟什么系统API无关,python自己需要编码时,就只会用gbk,因为它不知道文件系统编码是什么。这样假设后,就能解决上面的疑问。真可怕。)
(3)注3:能发现读写文件和os.listdir(path)的编码情况,归功于一个字符——'
'(浏览器无法显示)。这个小圆点呢,unicode码是30fb,把它以{unicode}、gbk编码的{str}、gb2312编码的{str}三种形式分别交给gbk编码的控制台打印,结果各不相同,见图7:
图7 小圆点‘’的编码结果以及它在资源管理器中的显示
{unicode}打印时自动用gbk编码,然而失败了。那么显式用gbk编码呢?直接报错了。用gb2312编码可行,但竟然在gbk编码的控制台能正确显示。于是把它用gbk解码看看,是u’\xb7’,查表发现也是小圆点,不过是这个’·’半宽的,所以’\xa1\xa4’能在gbk环境下正确显示其实是个错觉。虽然尝试后发现两个小圆点在某些字体看起来确实是一样的,但不管怎么说,u’\u30fb’能被gb2312编码不能被gbk编码,已经让人怀疑gbk是否真的完全兼容gb2312。
再看看资源管理器中目录名显示的小圆点,可能会因为字体原因显示相同而其实是u’\u30fb’或u’\xb7’其中之一,但是把它粘贴到txt里保存为utf-16be,用python读取文件字节后发现是’\x30\xfb’,既然gbk编不了u’\u30fb’,还敢说文件系统编码是纯gbk吗?
三、python中utf系列解编码时的BOM议题
1、utf-8——BOM作用不被识别,而是当做普通字符解码
utf-8的BOM是'\xef\xbb\xbf',因为utf-8没有字节顺序问题,所以其BOM实际上可以作为utf-8编码的标志看待。在python中并不会识别该标志,而是单纯看做一个字符——它的unicode码是feff,表示一个零宽无间断间隔符,打印出来类似一个空格。见图8:
图8
utf-8的BOM被当做字符
因此,python中utf-8编码的{str}一般不希望带有BOM。同样的,php文件也不希望在用utf-8编码时有BOM,带BOM相当于在文件开头放了个空格,文件内容怎么也没法到顶,产生不想要的效果。这一点在编程时需要注意。
2、utf-16——解编码时的参数跟BOM关系大
直接就先来看图9中的实例展示吧:
图9
utf-16的BOM识不识别要看参数
很明显,可以总结出以下规律——
①从第一组输出得出,编码时用’’utf-16”会默认按照utf-16LE带BOM编码,当参数指定”BE/LE”时,会变成相应不带BOM的编码;
②从第二组输出得出,用”utf-16”解码带BOM的utf-16{str}时,会根据BOM确定字节序完成解码;
③从第三组输出得出,用”utf-16”解码不带BOM的utf-16{str}时,会默认当做utf-16LE解码,所以BE的{str}会得到错误结果;
④从第四组输出得出,用指定”BE/LE”的参数解码带相应BOM的utf-16{str}时,BOM会被当成普通字符出现在解码结果中,这一般是大家都不想看到的;
⑤从第五组输出得出,用指定”BE/LE”的参数解码不带BOM的utf-16{str}时,可以正确解码,当然,前提是原编码跟参数指定的一致。
因此,utf-16的解编码一定要弄清楚原数据和需求,有没有BOM,要不要BOM,进而确定所用参数。
四、windows记事本中的utf
记事本可以保存为ansi、UTF-8、Unicode、Unicode Big Endian。Unicode其实是utf-16LE,Unicode Big Endian就是utf-16BE。在记事本中,它们和UTF-8都需要BOM才能保证正确识别。如果没有,记事本只能根据文档内容猜测可能的编码;如果猜测为utf的一种,当用户选择保存的时候,记事本会自动补上BOM。
因此当python读取记事本创建的文档时,要注意BOM的存在。