python编码问题

几个基本概念

bit
二进制位, 是计算机内部数据储存的最小单位,11010100是一个8位二进制数。一个二进制位只可以表示0和1两种状态(21);两个二进制位可以表示00、01、10、11四种(22)状态;三位二进制数可表示八种状态(2^3)……

Byte
字节,是计算机中数据处理的基本单位,计算机中以字节为单位存储和解释信息,规定一个字节由八个二进制位构成,即1个字节等于8个比特(1Byte=8bit)。八位二进制数最小为00000000,最大为11111111;通常1个字节可以存入一个ASCII码,2个字节可以存放一个汉字国标码。


在计算机中,一串数码作为一个整体来处理或运算的,称为一个计算机字,简称宇。字通常分为若干个字节(每个字节一般是8位)。在存储器中,通常每个单元存储一个字,因此每个字都是可以寻址的。字的长度用位数来表示。在计算机的运算器、控制器中,通常都是以字为单位进行传送的。

字长
字长:电脑技术中对CPU在单位时间内(同一时间)能一次处理的二进制数的位数叫字长。所以能处理字长为8位数据的CPU通常就叫8位的CPU。同理32位的CPU就能在单位时间内处理字长为32位的二进制数据。

字节和字长的区别:由于常用的英文字符用8位二进制就可以表示,所以通常就将8位称为一个字节。字长的长度是不固定的,对于不同的CPU、字长的长度也不一样。8位的CPU一次只能处理一个字节,而32位的CPU一次就能处理4个字节,同理字长为64位的CPU一次可以处理8个字节。

常见的字符编码

为什么Python使用过程中会出现各式各样的乱码问题,明明是中文字符却显示成“/xe4/xb8/xad/xe6/x96/x87”的形式?为什么会报错“UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)”?本文就来研究一下这个问题。

编解码过程

字符串在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成其他编码

整个过程如下,即
  decode      encode
str ---------> unicode --------->str

u = u'中文' #显示指定unicode类型对象u
str = u.encode('gb2312') #以gb2312编码对unicode对像进行编码
str1 = u.encode('gbk') #以gbk编码对unicode对像进行编码
str2 = u.encode('utf-8') #以utf-8编码对unicode对像进行编码
u1 = str.decode('gb2312')#以gb2312编码对字符串str进行解码,以获取unicode
u2 = str.decode('utf-8')#如果以utf-8的编码对str进行解码得到的结果,将无法还原原来的unicode类型

如上面代码,str\str1\str2均为字符串类型(str),给字符串操作带来较大的复杂性。

好消息来了,对,那就是python3,在新版本的python3中,取消了unicode类型,代替它的是使用unicode字符的字符串类型(str),字符串类型(str)成为基础类型如下所示,而编码后的变为了字节类型(bytes)但是两个函数的使用方法不变:
    decode        encode
bytes ---------> str(unicode) --------->bytes

u = '中文' #指定字符串类型对象u
str = u.encode('gb2312') #以gb2312编码对u进行编码,获得bytes类型对象str
u1 = str.decode('gb2312')#以gb2312编码对字符串str进行解码,获得字符串类型对象u1
u2 = str.decode('utf-8')#如果以utf-8的编码对str进行解码得到的结果,将无法还原原来的字符串内容

在文件读取的过程中:
假如我们读取一个文件,文件保存时,使用的编码格式,决定了我们从文件读取的内容的编码格式,例如,我们从记事本新建一个文本文件test.txt, 编辑内容,保存的时候注意,编码格式是可以选择的,例如我们可以选择gb2312,那么使用python读取文件内容,方式如下:

f = open('test.txt','r')
s = f.read() #读取文件内容,如果是不识别的encoding格式(识别的encoding类型跟使用的系统有关),这里将读取失败
'''假设文件保存时以gb2312编码保存'''
u = s.decode('gb2312') #以文件保存格式对内容进行解码,获得unicode字符串
'''下面我们就可以对内容进行各种编码的转换了'''
str = u.encode('utf-8')#转换为utf-8编码的字符串str
str1 = u.encode('gbk')#转换为gbk编码的字符串str1
str1 = u.encode('utf-16')#转换为utf-16编码的字符串str1

python给我们提供了一个包codecs进行文件的读取,这个包中的open()函数可以指定编码的类型:

import codecs
f = codecs.open('text.text','r+',encoding='utf-8')#必须事先知道文件的编码格式,这里文件编码是使用的utf-8
content = f.read()#如果open时使用的encoding和文件本身的encoding不一致的话,那么这里将将会产生错误
f.write('你想要写入的信息')
f.close()

代码中字符串的默认编码与代码文件本身的编码一致。

如:s='中文'
如果是在utf8的文件中,该字符串就是utf8编码,如果是在gb2312的文件中,则其编码为gb2312。这种情况下,要进行编码转换,都需要先用decode方法将其转换成unicode编码,再使用encode方法将其转换成其他编码。通常,在没有指定特定的编码方式时,都是使用的系统默认编码创建的代码文件。
如果字符串是这样定义:s=u'中文'
则该字符串的编码就被指定为unicode了,即Python的内部编码,而与代码文件本身的编码无关。因此,对于这种情况做编码转换,只需要直接使用encode方法将其转换成指定编码即可。

如果一个字符串已经是unicode了,再进行解码则将出错,因此通常要对其编码方式是否为unicode进行判断:
isinstance(s, unicode) #用来判断是否为unicode
用非unicode编码形式的str来encode会报错

  • 如何获得系统的默认编码
#!/usr/bin/env python
#coding=utf-8
import sys
print sys.getdefaultencoding()  
#!/usr/bin/env python  
#coding=utf-8  
s="中文"  
if isinstance(s, unicode):  
#s=u"中文"  
    print s.encode('gb2312')  
else:  
#s="中文"  
    print s.decode('utf-8').encode('gb2312')  

IDE和python2编码相关问题

在某些IDE中,字符串的输出总是出现乱码,甚至错误,其实是由于IDE的结果输出控制台自身不能显示字符串的编码,而不是程序本身的问题。

如在Sublime Text中运行如下代码:

s=u"中文"
print s

会提示:UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)。
而同样的 print u'中文' 代码在 Mac 的终端里却能正常打印出 “中文” 结果,没有任何报错。
这是因为Sublime Text在英文win7上的控制台信息输出窗口是按照ascii编码输出的(英文系统的默认编码是ascii),而上面代码中的字符串是Unicode编码的,所以输出时产生了错误。
若最后一句改为:print s.encode('utf8')

则输出:中文
unicode(str,'gb2312')与str.decode('gb2312')是一样的,都是将gb2312编码的str转为unicode编码
使用str.__class__可以查看str的编码形式

分析

Python 在向控制台 (console) print 的时候,因为控制台只能看得懂由 bytes(字节序列)组成的字符串,而 Python 中 "unicode" 对象存储的是 code points(码点),因此 Python 需要将输出中的 "unicode" 对象用编码转换为储存 bytes(字节序列)的 "str" 对象后,才能进行输出。

而在报错里看到 UnicodeEncodeError, 那就说明 Python 在将 unicode 转换为 str 时使用了错误的编码。而为什么是 'ascii' 编码呢?那是因为 Python 2 的默认编码就是 ASCII,可以通过以下命令来查看 Python 的默认编码:

import sys
print sys.getdefaultencoding()

ascii
所以此时在 Sublime Text 里运行 print u'中文',实际上等于是运行了:

print u'中文'.encode('ascii')

ASCII 编码无法对 unicode 的中文进行编码,因此就报错了。
那为什么同样的代码 print u'中文' 在 Mac 的终端里却能正常输出中文,难道是因为终端下的 Python 2 的默认编码不是 ASCII?非也,在终端下运行 sys.getdefaultencoding() 结果一样是 ascii。那同样是 ascii 为什么会有不同的结果?难倒这里 Python 用了另外一个编码来转换?

是的,其实 Python 在 print unicode 时真正涉及到的是另一组编码:stdin/stdout/stderr 的编码,也就是标准输入、标准输出和标准错误输出的编码。可以通过以下命令来查看,这里是在Sublime Text下运行的结果:

import sys
print sys.stdin.encoding
None
print sys.stdout.encoding
None
print sys.stderr.encoding
None

那么在这种 sys.stdout.encoding 为 None 情况下的 print unicode 怎么办呢?答案就是 Python 只能很无奈地使用 sys.getdefaultencoding() 的默认编码 ascii 来对 unicode 进行转换了。这样就出现了本文开头所说的那个 UnicodeEncodeError 问题。
在mac下他的这三种输出都是utf-8,实际上输出等于print u'中文'.encode('UTF-8'),所以输出正常。

python2 向控制台print输出是流程

总结一下 Python 2 向控制台 print 输出时的流程:

Python 启动时,当它发现当前的输出是连接到控制台的时候,它会根据一些环境变量,例如环境变量LC_CTYPE,来设法判断出 sys.stdin/stdout/stderr.encoding 编码值。
当 Python 无法判断出所需的编码时,它会将 sys.stdin/stdout/stderr.encoding 的值设置为None。
print 时判断字符串是否是 unicode 类型。
如果是的话,并且 sys.stdout.encoding 不为 None 时,就使用 sys.stdout.encoding 编码对 unicode 编码成 str 后输出。
如果 sys.stdout.encoding 为 None 的话,就使用 sys.getdefaultencoding() 默认编码来对 unicode 进行转换成 str 后输出。

if sys.stdout.encoding:
print unicode.encode(sys.stdout.encoding)
else:
print unicode.encode(sys.getdefaultencoding())

解决办法

解决办法一:
最不正确的解决方法:在头部文件加上

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

这种方法通过 dirty hack 的方式在 Python 刚启动时更改了 Python 的默认编码为 utf-8。此后:
print sys.getdefaultencoding()
utf-8
这个方法并不是真正地直接解决了问题。就如上述所说,Python 只是在sys.stdout.encoding 为 None 时才会使用默认编码来转换需要 print 的 unicode 字符串。那万一在sys.stdout.encoding 存在,但为 ascii 的情况下呢?这样即使更改了 Python 的默认编码,同样还是会出现 UnicodeEncodeError 报错。 所以对本问题来说,这个方法治标不治本。

解决办法二:
在 print 的时候显式地用正确的编码来对 unicode 类型的字符串进行 encode('正确的编码') 为 str 后, 再进行输出。
而在 print 的时候,这个正确的编码一般就是 sys.stdout.encoding 的值。但也正如上述所说,这个值并不是一直是可靠的,因此需要根据所使用的平台和控制台环境来判断出这个正确的编码。

而在 Mac 下这个正确的编码一般都是 utf-8,因此若不考虑跨环境的话,可以无脑地一直用 encode('utf-8') 和 decode('utf-8') 来进行输入输出转换。

解决办法三:
虽然解决方法 2 是最正确的方式,但是有时候在 Sublime Text 里调试些小脚本,实在是懒得再在每个print 语句后面写一个尾巴 .encode('utf-8')。那么有没有办法能让 Sublime Text 像在终端里一样直接就能 print u'中文' 呢?也就是说能不能解决 sys.stdin/stdout/stderr.encoding 为 None 的情况呢?

答案肯定是有的,一种方法是用类似更改默认编码的方法一样,用 dirty hack 的方式在 Python 代码中去显式地更改 sys.stdin/stdout/stderr.encoding 的值。一样是不推荐,我也没尝试过,在这里就不详说了。
另一种方法则是通过设置 PYTHONIOENCODING 环境变量来强制要求 Python 设置 stdin/stdout/stderr 的编码值为我们想要的,这是一个相对比较干净的解决方法。
在 Mac 下对全局 GUI 程序设置环境变量的方法是:使用 launchctl setenv <<key> <value>, ...>命令对所有 launchd 启动的未来子进程设置环境变量。
而 Sublime Text 提供了一个设置 Build System 环境变量的方法,这个方法各平台的 Sublime Text 都适用。

设置 Sublime Text 的 Python Build System 环境变量的步骤如下:

将 Sublime Text 默认的 Python Build System 的配置文件 Python.sublime-build(找到这个文件的最好方法是安装插件 PackageResourceViewer)复制一份到 Sublime Text 的 /Packages/User 文件夹下(在 Mac 和 Sublime Text 3 下这个路径是 ~/Library/Application Support/Sublime Text 3/Packages/User)。
打开编辑新复制来的 Python.sublime-build 文件,如下加上一行设置 PYTHONIOENCODING 环境变量为 UTF-8 编码的内容,并保存:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+   "env": {"PYTHONIOENCODING": "utf8"},
    "selector": "source.python"
}

这样一来终于在这么长的文章后能在 Sublime Text 里直接运行 print u'中文',而不用再出现万恶的UnicodeEncodeError 了。
既然都研究到这了,不妨我们试试把 PYTHONIOENCODING 设置成其它编码看看会出现什么情况,例如设置成简体中文 Windows 的默认编码 cp936:"env": {"PYTHONIOENCODING": "cp936"}

import sys
print sys.stdout.encoding
print u'你好'

cp936
[Decode error - output not utf-8]
[Finished in 0.1s]
[Decode error - output not utf-8],这就是 Sublime Text 在 Windows 下可能会出现的问题。这是因为 Sublime Text 的 Build System 默认是用 utf-8 编码去解读运行的输出的,而我们指定了让 Python 用 cp936 编码来生成 str 字符串进行输出,那么就会出现 Sublime Text 无法识别输出的情况了。
解决办法之一就是同样在 Python.sublime-build 文件里设置 "env": {"PYTHONIOENCODING": "utf8"}来使得输出统一为 utf-8。

或者是更改 Sublime Text 的 Build System 所接受的输出编码,将其改为一致的 cp936 编码,同样也是更改 Python.sublime-build 文件,加入一行:

{
    "shell_cmd": "python -u \"$file\"",
    "file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+   "encoding": "cp936",
    "selector": "source.python"
}

这里要注意,"env": {"PYTHONIOENCODING": "cp936"}和"encoding": "cp936",是两个不同的概念,PYTHONIOENCODING是表示读取和输出时进行解码编码的格式。"encoding"表示的是,python的build system所接受的输出编码。
这里要注意,PYTHONIOENCODING和encoding要一致,这样输出控制台才行。详细的资料,参考这篇文章

【已解决】Python字符串处理出现错误:UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xe6 in position 0: ordinal not in range(128)

注意到错误提示中的“ordinal not in range(128)”,意思是,字符不在128范围内,即说明不是普通的ASCII字符,超出处理能力了。所以感觉是str类型的变量,无法处理超过ASCII之外的字符。所以想到去将对应原始字符转换为unicode:
gVal[``'newPostPatStr'``] ``= unicode``(gVal[``'newPostPatStr'``]);
然后再去调用上面的replace,结果此句执行结果,也出现和上面同样的错误,无法转换为unicode。
最后是通过,在最开始的时候,得到gVal[‘newPostPatStr’]的值之后,
调用unicode时候指定对应的编码:
gVal[``'newPostPatStr'``] ``= unicode``(gVal[``'newPostPatStr'``], ``"utf-8"``);

然后就可以强制转换为unicode了,然后之后的字符串处理,就都是可以正常的了。

【总结】

此处是最开始获得某字符串变量,没有通过指定编码为utf-8转换为unicode,然后接下来的操作,比如replace替换,就都无法处理包含了utf-8的,超出了128 range的字符,才会报UnicodeDecodeError错的。

所以,以后遇到UnicodeDecodeError方面的错误,那就先去看看,是不是由于没有指定合适的编码。如果指定了对应的编码后,字符串的一切操作(replace, re.sub等),一般来说,就都可以正常操作了。

推荐阅读更多精彩内容

  • 字符集和编码简介 在编程中常常可以见到各种字符集和编码,包括ASCII,MBCS,Unicode等字符集。确切的说...
    兰山小亭阅读 1,878评论 0 11
  • 什么是编码 任何一种语言、文字、符号等等,计算都是将其以一种类似字典的形式存起来的,比如最早的计算机系统将英文文字...
    随风化作雨阅读 495评论 1 2
  • 可以看我的博客 lmwen.top 或者订阅我的公众号 简介在刚玩python爬虫的那段时间,时常获取到乱码内...
    ayuLiao阅读 81评论 0 0
  • 继上一篇文章字符集和编码详解总结了常见字符编码后,这篇文章会对python中常见的编码问题进行分析和总结。由于py...
    __七把刀__阅读 1,745评论 0 6
  • 写python的过程中经常出现各种蛋疼的编码问题,于是通过上网查资料,自己做实验,想彻底搞清楚这个问题。 编码和解...
    allen哦阅读 84评论 0 1