完美解决Python2的编码问题

背景

计算机无论 存储信息 ,或者通过网络 交换信息基本单位 都是 八位字节 ( octet )。八位字节能够表示的字符数最多不超过 256 种:

[2^8 = 256]

这用来存储英文字母是绰绰有余,但是要满足全世界人民可就难了——汉字光常用的就几千个呢!怎么办呢?——用多个字节存储呗。这时,全世界人民齐头并进:

  • 中国人民使用 GB2312GBKGB18030
  • 日本人民使用 Shift JISEUC-JP
  • 韩国人民使用 EUC_KR
  • etc

各家自扫门前雪, 互不兼容 。直到有个老好人 Unicode 出来统一世界,收录人类所有字符。Unicode 字符集非常庞大,也需要多个字节存储,最常见的编码方式是 UTF-8 。因此,对于同一个汉字 ,按不同的编码存储后的字节是不一样的:

汉字与编码
汉字
Unicode25105
GBK两字节:0xce 0xd2
UTF-8三字节:0xe6 0x88 0x91

一个 GBK 编码的字节序列,如果以 UTF-8 来读,肯定是一坨狗屎。因此,知晓数据编码方式非常重要,直接关系到数据的解读。中文编码方式有好几套,互不兼容,因此中文世界里的乱码现象就不难理解了。

Python 2 ,字符分为两种,首先是 Unicode 字符:

  1. u'你好,世界!'

普通字符串可以理解成字符序列:

  1. '你好,世界!'

注解

代码中的普通字符串编码与代码文件相同;交互式模式中输入时与终端编码相同。

在两种字符串之间转换,需要 编码 或者 解码

  1. # 编码
  2. a = u'你好,世界!'
  3. b = a.encode('utf8')
  4.  
  5. # 解码
  6. b.decode('utf8')

文件流

文件存储 八位字节 ,不管待存数据是啥,最终要转化成字节。那么,如果直接写入 Unicode 字符串,会发生什么事情:

  1. >>> f = open('somefile.out', 'w+')
  2. >>> f.write(u'你好,世界!')
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in <module>
  5. UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)

毫不意外,提示默认的 ascii 编码器不能编码待写入的数据。

但注意到,我们在终端运行程序,却可以输出 Unicode 字符串。将以下代码保存成 say-hello.py 文件,并运行:

  1. print u'你好,世界!'
  1. $ python2 say-hello.py
  2. 你好,世界!

为啥呢?理论上, print 也是写到 sys.stdout 中,它也是一个文件对象呀!

然而, Python 初学者还经常被另一个问题困扰,将输出重定向到文件就又不行了?

  1. $ python2 say-hello.py > /dev/null
  2. Traceback (most recent call last):
  3. File "say-hello.py", line 16, in <module>
  4. print u'你好,世界!'
  5. UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-5: ordinal not in range(128)

还记得吗, Python 打开文件有两种不同的模式, 文本模式二进制模式

  1. # 文本模式
  2. f = open('somefile.out', 'w+')
  3.  
  4. # 二进制模式
  5. f = open('somefile.out', 'wb+')

既然默认是文本模式,可以非常合理地怀疑 sys.stdout 文件对象的编码设置。接下来,写一个程序 stdio-encoding.py 来检查在不同的运行方式下, 标准 IO 对象的编码方式:

  1. import sys
  2.  
  3. for name in ('stdin', 'stdout', 'stderr'):
  4. print name, getattr(sys, name).encoding

直接运行,继承终端输入输出,文件对象的编码方式是 UTF-8 (因环境而已):

  1. $ python2 stdio-encoding.py
  2. stdin UTF-8
  3. stdout UTF-8
  4. stderr UTF-8

标准错误重定向, stderr 文件对象编码方式变成了 None

  1. $ python2 stdio-encoding.py 2>/dev/null
  2. stdin UTF-8
  3. stdout UTF-8
  4. stderr None

将标准输出写到管道,行为也是一样的!

  1. $ python2 stdio-encoding.py | cat
  2. stdin UTF-8
  3. stdout None
  4. stderr UTF-8

为啥继承终端输入输出, Python 可以给标准 IO 文件对象设置正确的编码呢?这是因为 Python 可通过用户设置的环境变量获悉终端的编码方式:

  1. $ env | grep UTF
  2. LANG=en_US.UTF-8
  3. LC_ALL=en_US.UTF-8
  4. LC_CTYPE=UTF-8

至此,问题的症结已经对位到,解决方案也呼之欲出。

方法一:PYTHONIOENCODING 环境变量

第一种解决方法是,通过环境变量,明确告诉 Python 进行 IO 的编码方式:

  1. $ PYTHONIOENCODING=UTF-8 python2 say-hello.py
  2. 你好,世界!

还是原来的代码,设置 PYTHONIOENCODING 环境变量后程序成功运行了!

方法二:设置文件对象编码

第二种方法,则是将 sys.stdout 换掉:

  1. import codecs
  2. import sys
  3.  
  4. sys.stdout = codecs.getwriter('utf8')(sys.stdout)
  5.  
  6. print u'你好,世界!'
  1. $ python2 say-hello-utf8.py | cat
  2. 你好,世界!

看到没有,程序运行也正常了!

方法三:设置程序默认编码

这是一个无脑大招,几乎可以应对大部分编码问题!

方法很简单,只需将入一下若干代码,重新设置程序的默认编码方式即可:

  1. import sys
  2. reload(sys)
  3. sys.setdefaultencoding('UTF8')

下一步

订阅更新,获取更多学习资料,请关注我们的 微信公众号

../_images/wechat-mp-qrcode.png 小菜学编程

微信打赏