8

编码,还是编码!

python2的直钩——编码异常

当你用python打开一篇中文文档,准备读取里面的数据开始实验...
当你处理好你的数据,打算打印出易于阅读的结果给boss检查...
甚至当你刚刚开始编写自己的代码,就写了一句话...

text = '什么鬼'

只要你开始运行自己的代码,信心满满期待搞定回寝时

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)

以及

SyntaxError: Non-ASCII character '\xe5' in file test.py on line 3, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

于是你10点前回到寝室以及之后的一系列计划全部泡汤了,垂头丧气的坐下来,你看到两个动词格外亮眼——decodeencode。而他们的中文释义,就是python2对新手的最大陷阱——编码

当我们谈论编码时我们在谈论什么

python中有关编码问题的对象有basestring, str, unicode, 标准库有codecs等,在这篇文章里我们基本上不会提到标准库,而仅仅简单的对对象们进行分析。因为这就够了!

事实上,我们常犯的编码问题,从抛出异常的角度来说分为两种,很明显,本文一开头也列出这两种异常的打印情形,它们分别是

  • py文件编译时未指定文件字符集导致的解码异常

  • 字符串对象互相转换时使用默认编码导致的异常

之后将会分别对两类异常的处理方法做说明。实际上, 第一类错误本质上则是 python 自己运行时打开文件进行解码造成的异常, 就是第二类错误! 我们所犯的解码异常,就是

字符串对象互相转化时没有指定字符编码

黄金原则

本文章之所以比其他写编码的文章稍微多一点价值的原因,在于本文在这里——第一章的最后一小节——就用最大的字体写了处理这类异常的黄金原则

不要惊慌

以及在此之下的,你真正可以掌握的,避免这类异常的黄金原则

只有在IO的时候,才进行转换

这意味着

  1. 因为某些原因, python 打开流读取出的是str,所以用你知道的每一种编码把它解码成unicode

  2. 大概是因为同样的原因,python 的输出也是str, 但是任何一个unicode 只有到要输出的时候才编码成str

  3. 在此之间,放弃该死的str,忘了它,当你开始处理的时候,确保你的每一个字符串对象都是unicode

掌握了以上原则,会避免99%的编码异常发生。当然,正在阅读这篇文章的人中有80%肯定犯过了1000次以上这种错误,去避免剩下1%的发生,而还有20%的人刚开始准备写python,他们会在看完这篇文章后犯完100%的错误,本文的作者正在和80%的人一起微笑着等他们第二遍来看这篇文章。
顺便说一下,这篇文章到这里主要内容就结束了,如果你想找到解决方法和原因,上面已经说的清清楚楚了,接下来主要是各种重复和闲谈,帮助你了解这之后的内幕,不过第二次来看的同学们记得往下看哦

第一类异常

一点小trick

第一类异常是python 自己打开你写的源文件时抛出的解码异常,这句话被说了两遍说明它一定——很不重要,不过你也可以当做一个冷知识储备一下。

SyntaxError: Non-ASCII character '\xe5' in file test.py on line 3, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

所有的这类异常都是因为你在源文件写代码时中直接使用了国际化文本——也就是你没有办法在ascii码表里找到的字符。同时你“聪明”的没有做下面说的这一件事

在文件的开头使用注释声明文件编码

# coding:文件编码

pep263

如果你有审慎的阅读出错信息,你一定会注意到一个网址出现在其中。没错,那就是python社区的技术提案 PEP(python enhancement proposals), 涵盖了从版本更新特性至python格式指南的一切东西,如果你有一个昏昏欲睡的下午的话,可以浪费一点时间看看它。

pep263里,详细的介绍了某种异常发生的原因,以及它提出的一种声明注释的解决方案。接下来我们简要介绍的一些内容你都可以在上面找到,当然它是英文的

原因

自从pep263成为python标准后,python的编译器或者说是编码器在开始解释前,先要经过以下几个步骤:

  1. 读出文件内容

  2. 将内容根据文件编码解码成为unicode

  3. 分词标注

  4. 解释它,并把每一个直接写出的unicode(u'什么鬼')创建一个unicode对象,对str对象,将会从unicode按照文件编码再编码成为str对象

异常原因在于,python的默认文件编码,不是utf-8,不是gbk,而是 ascii

快出来看上帝

他们彼此商量说,来吧,我们要作砖,把砖烧透了。他们就拿砖当石头,又拿石漆当灰泥。

他们说,来吧,我们要建造一座城和一座塔,塔顶通天,为要传扬我们的名,免得我们分散在全地上。
耶和华降临,要看看世人所建造的城和塔。
耶和华说,看哪,他们成为一样的人民,都是一样的言语,如今既作起这事来,以后他们所要作的事就没有不成就的了。
我们下去,在那里变乱他们的口音,使他们的言语彼此不通。
于是,耶和华使他们从那里分散在全地上。他们就停工,不造那城了

本文作者之所以在这里引用一段旧约(某知道里的答案),完全是因为作者想展示一下自己的逼格。事实上,本章关于第一类异常的处理在第一小节就已经结束了,后面完全是杂谈,但其实也许是很重要的

上帝机智的搅乱了人类的语言的1000年后,本文作者觉得可能是上帝的第二次降临,人类中最聪明的一群人,也许也是最蠢的,程序员,开始想要在自己的处理对象里增加字符了。

考虑到转换的问题,很容易就想到,如果把每一个字母,每一个标点,每一个符号与计算机中特殊的一位一一对应的话,就能够实现对字符的处理了。那么,这里假设你已经有一定的计算机底层知识了,这样一个唯一的对应的编码至少需要多少位?

这里提供一些数据, 所有大小写字母一共52个,0~9数字需要10个,加上逗号,句号,感叹号...

答案是 7

ascii码,也就是美国信息交换标准码(American Standard Code for Information Interchange),1967年发布,7位字符编码中影响最大的一种。二进制取值范围0000000~1111111,十六进制表示00h~7fh

事实上当时ascii码主要是用于电传打字机的,但是现在已经基本上一统计算机的天下。但它的问题同样很严重,就在它的名字里,它实在太美国化了。阿拉伯文,日语,当然还有我们的中文,通通找不到自己的位置,于是出现无穷多种扩展ascii编码,它们的前7fh的编码与ascii保持一致,而使用自己的扩展位实现对其他语言及符号的编码

我们统称这一类为ANSI编码标准,在这里各国的程序员们就开始各自发挥了:

  • gb大家族,我朝官方认证出品的一系列字符集

  • latin大家族,主要是对拉丁字母及西欧一些国家的字母编码

  • Big 5,呆湾主要使用的针对繁体中文的编码
    ...

你可以想象这是有多么混乱,实际上都不用想象,现在还有无数人在求助,我的文档打开乱码怎么办

因此,Unicode响应时代的号召,横空出世。Unicode使用16位编码,编码范围0000h~ffffh,它对还在捉对厮杀的各国程序员说,别打了,我们一个字符集包括世界上所有字符就好啦

但是,Unicode只是给定了字符与编码的对应关系,它的实现方式还是有很多种,其中就有UTF大家族(其实是美帝的程序员发现它们要为一辈子都可能见不到的中文,把英文编码提高一个字节时,wtf!)

于是就有了UTF-8,使用一个字节表示英文,而三个字节表示中文的编码方式

注释声明

在一大段闲谈之后,我们简单的说明了各大字符集的由来,所以,现在问题来了,面对各国程序员的各种编码的文件,一门编程语言应该如何处理呢?

对于python,它的默认文件编码是ascii码,在遇到国际化文本,也就是其他编码字符集时,就会无法编码(老天,这个编码都超过ffh了!)

因此,呼应文章开头,pep263指出,python的程序员们都应该在文件的开头写上文件的默认编码,同时一个文件只能有一种编码!也就是:

# coding:文件编码

至于为什么与你平常所见到的模式:

# -*- coding: utf-8 -*-

不一样,本文作者会轻易告诉你-*-是装饰用的吗

第二类异常

Unicode会梦见小绵羊吗?

在python中,其实是python2中,与其他语言不同的是,有两个经常被用来实际操作的字符串对象

  • str

  • Unicode

要说明两者之间的关系,实在不是一个——很难的问题。我们可以非常非常非常——容易的得到对象的继承关系,如下图:

> object
  >> basestring
     >>> str
     >>> --
     >>> unicode

可以看到,unicode对象与str对象都继承自basestring。basestring是一个抽象类,字符串及其操作由子类str及unicode各自实现。所以

基本上所有str能进行的操作unicode都能进行

编码与解码

在python中,我们所说的编码encode,特指从unicode转换成指定编码的str对象

str = unicode.encode(字符编码)

而所说的解码decode,特指从指定编码的str对象转换为unicode对象

unicode = str.decode(字符编码)

如果你有好好的阅读来看上帝把那一节,就很容易理解这二者的转换,相当于我们把不同字符集中对字符的编码与Unicode全世界统一的编码互相转换。

而python2最大的直钩也在于此,它的默认编码是ascii

然而ascii早已看穿了一切

我们之所以要重复提ascii,是因为它真的很重要!理解它是python2默认编码将会让你真正理解第二类异常的原因:

进行编码解码时没有指定字符集编码,python默认使用ascii进行编码解码

因为ascii仅包含英文大小写及几十个常用符号,因此,当你的编码解码的对象里包含中文或者其他乱七八糟东西的时候

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)

Do you know your object?

在这一节,我们将会谈到何时会触发第二类异常,也就是所谓的情景检查。事实上,在本文作者看来,所有的第二类异常都在一种情形下发生:

程序员混用了unicode与str对象

一旦开始错误的使用unicode或者str,都将很有可能导致第二类异常。然而,遗憾的是,直到它抛出了异常,大部分没读过这篇文章的人依然没有意识到问题在哪里。其主要原因在于:

  1. str对象支持的方法与unicode基本完全一样

  2. str与unicode都是继承自basestring,大部分对字符串操作的方法只会检查是不是basestring类及其子类

  3. 任何一个类都来自object(这里指新类),都默认包含内建方法__str__,该方法用于将实例转换成str对象,换言之,你能够print任何一个对象,都因为默认使用内建方法转换了。各个类都可以改写这个内建方法,而unicode改写为使用默认编码解码

这就使得一个初学者的程序中,字符串对象既有unicode,也有str,而他完全没有意识到,当然也是由于大部分市面上的书在这一点上都及其不负责任。想象一下,当你以为自己的对象a是一个str,而实际上是一个unicode,你想当然的进行print输出时,就会默认调用unicode的__str__进行转化输出,在这里进行了默认编码ascii的解码,error!同理适用于当你把一个str当unicode用的时候

一旦你开始混用两种对象,在你不注意的地方,就会发生默认编码解码!

另外一种稍微可以谅解的情况是,python2关于文件流的封装实在太过坑爹,基本上所有文件流最终返回和写入的都是str对象。简单的举个例子,你打开一个文件,按行读取的每一行,都是一个str对象!那些只告诉你这样可以读,不告诉你返回类型(虽然写了你也不大可能注意)的技术博客都是在耍流氓!

    with open('data.txt', 'r') as f:
        for line in f:
            line 是一个str对象!

所以在看到这里的时候,请务必检查你的程序,检查你的每一个字符串对象,确定它是你想要的类型,要知道,我们所接触的大部分数据都会有中文,千万不要等到报错了才开始纠错

Do you know your object?
No!?
Go to know your object!

放过str,请找unicode

为什么我们要放弃str?

简单的理由,str不仅需要我们知道它的编码,还需要根据输出编码做转换。假设你有一个utf8编码的str对象,想要输出到gbk编码的控制台上,你要这么做:

  1. utf8解码成unicode

  2. unicode编码成gbk

为什么我们不从一开始对象处理的时候就用unicode!

粗暴的理由,python3里面已经没有str这种东西了!

请记住黄金原则

只有在IO的时候,才进行转换

这意味着

  1. 因为某些原因, python 打开流读取出的是str,所以用你知道的每一种编码把它解码成unicode

  2. 大概是因为同样的原因,python 的输出也是str, 但是任何一个unicode 只有到要输出的时候才编码成str

  3. 在此之间,放弃该死的str,忘了它,当你开始处理的时候,确保你的每一个字符串对象都是unicode

是不是在哪里看到过? 不要在意这些细节~

按照黄金原则编写能确保你的每一个进行处理的字符串对象都是unicode,同时只在io处进行转换确保你只有在这个时候才需要考虑编码的问题,也符合面向对象封装的概念,也是最pythonic的做法

如果你还不知道什么是pythonic,请直接运行以下python代码

import this

上面所说的是最正确的解决方法,当然有同学就会问啦,下面这种为什么不是最正确的呢?

import sys
sys.setdefaultencoding(字符编码)

这种方法是在饮鸩止渴,完全没有解决你的实际代码问题。它只是将python默认编码替换成了你想要的编码(utf-8之类),一旦有新的编码类型的str对象出现,你的程序就会重新开始报错。所以不推荐这种方法,它会掩盖掉你程序的大部分问题。

异常蛋疼的windows控制台

简单粗暴

就在不久前,本文作者在服务器上部署爬虫代码,就不得不在控制台输出(当然不是因为作者懒得用其他方式跑代码),结果是一连串的乱码,自认不是新手的作者完全不能忍了,于是心平气和的坐下来研究了下windows控制台的编码

事实上,windows的控制台的字符集编码不叫字符集编码,而叫代码页,多么古怪的名字!于是我们很直接的查到了utf-8的代码页是65001

然后再输出的时候发现,每log一行就在报一行的error,看输出信息是log的流往控制台写的时候报的错,不过既然能打印出log,本文作者决定忽略掉那些error

所以

  • 把代码页设置为65001 chcp 65001

  • 如果打印出了log,忽略那些错误把~

本小节是真的没有查资料,如有错误和更好的解决方法,请不吝指正


jiminhuang
122 声望5 粉丝