1.6 字符编码

在Python 2.x中,字符编码是一个让人困惑的问题,这个问题在Python 3.x中自然解决了。由此可以说,未来是Python 3的。但是,由于前面已经分析过的原因,在一段时间内Python 2.x还不能完全丢弃,甚至不少工程项目还是以它为主。所以,还要将字符编码问题单独叙述。

如果一个字符串都是英文,就没有所谓编码问题。但在我们的环境中,中文是我们不得不用的。

  1. >>> name = '老齐'
  2. >>> name
  3. '\xe8\x80\x81\xe9\xbd\x90'

你在交互模式中遇到过上面的情形吗?这就是显示汉字的问题,英文就不这样了。

难道这是中文的错吗?看来投胎真的是一个技术活。是的,投胎是技术活,但上面的问题不是中文的错。

1.6.1 编码

什么是编码?这是一个比较玄乎的问题,也不好下一个普通定义。我看到有的教材中有定义,且不敢说其定义不对,但至少是不容易理解。

“古代打仗,击鼓进攻、鸣金收兵”这就是编码。把要传达给士兵的命令对应为一定的其他形式,比如命令“进攻”,经过信息传递,如图1-7所示。

1.6 字符编码 - 图1图1-7 信息传递

1)长官下达进攻命令,传令员将这个命令编码为鼓声。

2)鼓声在空气中传播,比传令员的嗓子吼出来的声音传播得更远,士兵听到后也不会有歧义,这就是“进攻”命令被编码成鼓声之后的优势所在。

3)士兵听到鼓声,就是接收到信息,如果接受过训练或者有人告诉过他们,他们就知道这是命令进攻,这个过程就是解码。所以,编码方案要有两套:一套在信息发出者那里,另外一套在信息接受者这里。经过解码之后,士兵明白了才行动。

以上过程比较简单,但真实的编码和解码过程比这个复杂。不过,原理都差不多。

举一个似乎遥远,其实不久前人们都在使用的东西做例子:电报(以下引用的内容来自《维基百科》)。

电报是通信业务的一种,在19世纪初发明,是最早使用电进行通信的方法。电报大为加快了消息的流通,是工业社会的一项重要发明。早期的电报只能在陆地上通信,后来使用了海底电缆,开展了越洋服务。到了20世纪初,开始使用无线电波发电报,电报业务基本上已能抵达地球上大部分地区。电报主要用作传递文字讯息,使用电报技术用作传送图片称为传真。

中国出现首条电报线路是1871年,由英国、俄国及丹麦敷设,从中国香港经上海至日本长崎,且是海底电缆。由于清政府的反对,电缆被禁止在上海登录。后来丹麦公司不理清政府的禁令,将线路引至上海公共租界,并在1871年6月3日起开始收发电报。至于中国首条自主敷设的线路,是由福建巡抚丁日昌在中国台湾所建,1877年10月完工,连接台南及高雄。1879年,北洋大臣李鸿章在天津、大沽及北塘之间架设电报线路,用作军事通信。1880年,李鸿章奏准开办电报总局,由盛宣怀任总办。并在1881年12月开通天津至上海的电报服务。李鸿章説:“五年来,我国创设沿江沿海各省电线,总计一万多里,国家所费无多,巨款来自民间。当时正值法人挑衅,将帅报告军情,朝廷传达指示,均相机而动,无丝毫阻碍。中国自古用兵,从未如此神速。出使大臣往来问答,朝发夕至,相隔万里好似同居庭院。举设电报一举三得,既防止外敌侵略,又加强国防,亦有利于商务。”天津官电局于庚子遭乱全毁。1887年,台湾巡抚刘铭传敷设了福州至台湾的海底电缆,是中国首条海底电缆。1884年,北京电报开始建设,采用“安设双线,由通州展至京城,以一端引入署中,专递官信,以一端择地安置用便商民”,8月5日,电报线路开始建设,所有电线杆一律漆成红色。8月22日,位于北京崇文门外大街西的喜鹊胡同的外城商用电报局开业。同年8月30日,位于崇文门内泡子和以西的吕公堂开局,专门收发官方电报。

为了传达汉字,电报部门准备由4位数字或3位罗马字构成的代码,即中文电码,采用发送前将汉字改写成电码发出,收电报后再将电码改写成汉字的方法。

注意:这里出现了电报中用的“中文电码”,这就是一种编码,将汉字对应成阿拉伯数字,从而能够用电报发送汉字。

1873年,法国驻华人员威基杰参照《康熙字典》的部首排列方法,挑选了常用汉字6800多个,编成了第一部汉字电码本《电报新书》。

电报中的编码被称为摩尔斯电码,英文是Morse Code。

摩尔斯电码(英语:Morse Code)是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母、数字和标点符号。是由美国人萨缪尔·摩尔斯在1836年发明。

摩尔斯电码是一种早期的数字化通信形式,但是它不同于现代只使用0和1两种状态的二进制代码,它的代码包括五种:点(.)、划(-)、每个字符间短的停顿(在点和划之间的停顿)、每个词之间中等的停顿以及句子之间长的停顿。

看来电报员是一个技术活,不同长短的停顿都代表了不同意思。哦,对了,有一个老片子叫《永不消逝的电波》,保证你看完之后才知道,里面根本就没有讲电报是怎么编码的。

摩尔斯电码在海事通信中被作为国际标准一直使用到1999年。1997年,当法国海军停止使用摩尔斯电码时,发送的最后一条消息是:“所有人注意,这是我们在永远沉寂之前最后的一声呐喊!”

1.6 字符编码 - 图2

我瞪着眼看了老长时间,这两行不是一样的吗?

不管这个了,总之,这就是编码。

1.6.2 计算机中的字符编码

抄一段维基百科对字符编码的解释:

字符编码(英语:Character Encoding),也称为字集码,是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数串行、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。常见的例子包括将拉丁字母表编码成摩斯电码和ASCII。其中,ASCII将字母、数字和其他符号编号,并用7比特的二进制来表示这个整数。通常会额外使用一个扩充的比特,以便于以1个字节的方式存储。

在计算机技术发展的早期,如ASCII(1963年)和EBCDIC(1964年)这样的字符集逐渐成为标准。但这些字符集的局限很快就变得明显,于是人们开发了许多方法来扩展它们。对于支持包括东亚CJK字符家族在内的写作系统的要求能支持更大量的字符,并且需要一种系统而不是临时的方法实现这些字符的编码。

在这个世界上,有好多不同的字符编码。但是,它们不是自己随便搞搞的,而是要有一定的基础,往往是以名叫ASCII的编码为基础。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以部分支持其他西欧语言,并等同于国际标准ISO/IEC 646。由于万维网使得ASCII广为通用,直到2007年12月,逐渐被Unicode取代。

上面的引文中已经说了,现在我们用的编码标准已经变成Unicode了(Python3.x就是用了Unicode),那么什么是Unicode呢?还是抄一段来自维基百科的说明:

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。

Unicode伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为7.0.0,已收入超过十万个字符(第十万个字符在2005年获采纳)。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。

听这名字:万国码,那就一定包含了中文。但是,光有一个Unicode还是不够用(可以访问《维基百科》网站查看相关说明),还要有其他的一些编码实现方式,Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF),于是乎有了一个我们在很多时候都会看到的utf-8。

什么是utf-8?还是看维基百科上怎么说的吧:

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件不需要或只做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中优先采用的编码。

是不是理解了呢?前面写程序的时候,曾经出现过coding:utf-8的字样,就是在告诉Python我们要用什么字符编码。

1.6.3 encode和decode

encode()和decode()是两个内置函数。

codecs.encode(obj[,encoding[,errors]]):Encodes obj using the codec registered for encoding.

codecs.decode(obj[,encoding[,errors]]):Decodes obj using the codec registered for encoding.

Python2默认的编码是ASCII,通过encode()可以将对象的编码转换为指定编码格式(称作“编码”),而decode是这个过程的逆过程(称作“解码”)。

做一个实验,才能理解:

  1. >>> a = "中"
  2. >>> type(a)
  3. <type 'str'>
  4. >>> a
  5. '\xe4\xb8\xad'
  6. >>> len(a)
  7. 3
  8.  
  9. >>> b = a.decode()
  10. >>> b
  11. u'\u4e2d'
  12. >>> type(b)
  13. <type 'unicode'>
  14. >>> len(b)
  15. 1

在做这个实验之前,或许还不是很迷茫(知道得越多越迷茫),实验做完了,自己也迷茫了。别急躁,对编码问题的理解要慢慢来,如果一时理解不了,就先按照要求做,做着做着就豁然开朗了。

变量a引用了一个字符串类型对象,但严格地讲是字节串,因为它是经过编码后的字节组成的序列。也就是你在上面的实验中看到的“中”这个字在计算机中编码之后的字节表示。(关于字节可以搜索一下)。用len(a)来度量它的长度,它是由三个字节组成的。

然后通过decode函数将字节串转变为字符串,并且这个字符串是按照Unicode编码的。在Unicode编码中,一个汉字对应一个字符,这时候度量它的长度就是1。

反过来,一个Unicode编码的字符串也可以转换为字节串。

  1. >>> c = b.encode('utf-8')
  2. >>> c
  3. '\xe4\xb8\xad'
  4. >>> type(c)
  5. <type 'str'>
  6. >>> c == a

关于编码问题先到这里点到为止吧。因为再扯,还会扯出问题来,读者肯定感到不满意,因为还没有知其所以然。

1.6.4 避免中文是乱码

“避免中文是乱码”是一个具有很强操作性的问题。

首先,提倡使用utf-8编码方案,因为它跨平台不错。

经验一,在开头声明:

  1. # -*- coding: utf-8 -*-

有朋友问我“-*-”有什么作用,那个就是为了好看,爱美之心人皆有,更何况程序员?当然,也可以写成:

  1. # coding:utf-8

经验二,遇到字符(节)串,立刻转化为unicode,不要用str(),直接使用unicode():

  1. unicode_str = unicode('中文', encoding='utf-8')
  2. print unicode_str.encode('utf-8')

经验三,如果对文件操作,打开文件的时候,最好用codecs.open替代open(关于文件的操作,请参阅后续内容。)

  1. import codecs
  2. codecs.open('filename', encoding='utf8')

最后,如果用Python3,这种编码的烦恼会少一点。