Python 系列学习之二:字符编码(Encode)和译码(Decode)

前言

打算写一系列文章来记录自己学习 Python 3 的点滴;本篇将会重点介绍 python 有关字符串的编码和译码理论知识和相关操作,注意,为了阐述清楚相关内容,本文同时包含了 Python 2 和 3 相关的内容;

本文为作者的原创作品,转载需注明出处;

编码

编码的诞生

我们知道,计算机只能处理 0 和 1;所以,面临的问题便是,我们如何将一串 0、1 数值表达为有意义的人类能够识别的符号呢?那么,这个时候,我们就需要对由 0、1 组成的这一串数值进行编码,使其映射到人类能够识别的符号上;也就产生一系列的编码方式,最早的计算机编码是 ASCII 码;那么我们就来看看 ASCII 是如何进行编码的?

举个例子,字母A对应的二进制编码为01000001,什么意思呢?也就是说,当计算机采用 ASCII 编码并识别到一串二进制数值01000001的时候,它就会通过 ASCII 码表进行映射,得到人类可以识别的符号,既是字母A;看如下 ASCII 码表的片段;

可见,字母A对应的二进制编码为01000001,八进制编码为101,十进制编码为65,十六进制编码为41;通过这样的码表映射,就可以将原本无序的二进制数值变为计算机可以识别,可以映射的数值,其映射结果就是人类可以识别的符号;

ASCII 编码是最早的计算机字符编码,是在计算机诞生的时候,就设计出来了;当时主要是为了能够通过二进制数值来识别出英文字母、数字以及一些常规的符号;

混乱的伊始

编码的诞生小节我们知道,ASCII 编码主要是用来将一串二进制数值映射为英文字母、数字等基础符号;但是它并不能对中文、日文、韩文等进行编码;所以呢,各国就在 ASCII 编码的原理基础上,各自添加与本国语言相关的编码表,比如,中国有GBKGB2312编码,日本有SHIFT_JIS编码,韩文有EUC_KR编码;有什么问题呢?为什么本小节称为混乱的伊始呢?

举个例子,二进制数值11010110 1010000(十六进制为\xd6\xd0)通过中文GBK编码表映射得到汉字,但是,该二进制通过通过日文SHIFT_JIS编码表映射得到的却是ヨミ,翻译成中文是阅读的意思;也就是说,如果同一个二进制串,在通过中文编码表GBK映射得到的是,在通过日文编码表SHIFT_JIS得到的却是阅读;于是,混乱便产生了,两者的编码互相不认识,同一个二进制数值,在两套完全不同的编码体系下,得到的完全是两个不同的字符,也就是说我在中文中不能写日文,日文中不能写中文,否则在中文编码GBK的格式下不能识别输入的日文,同理日文编码SHIFT_JIS中也不能识别出中文,而当这种情况发生以后,输出的都是乱码(不能识别的字符,或者是言不达意的字符),归根结底就是互不兼容;而这个在国际化形式下,一篇论文可能会出现好几种语言的大趋势下,是不符的;于是,为了解决这个问题,UNICODE 便诞生了;

UNICODE 诞生

于是乎,为了解决各自为政、各个国家为各自的语言建立自己的编码而导致不能识别它国语言的混乱局面,于是乎 ISO 出手了,建立了全球所有语言的同一个编码表,这就是 UNICODE,其编码规定,任何字符均使用两个字节来表示;UNICODE的目标就是,统一编码全球的所有的不同国家,不同语言的字符,使得每一个字符对应一个唯一的二进制数值;更多有关 UNICODE 的介绍参考 https://zh.wikipedia.org/wiki/Unicode

那么 UNICODE 是如何做到统一的呢?我们来看看 UNICODE 的编码表,

字符 UNICODE
A 00000000 01000001
01001110 00101101
00110000 01100111

备注, 翻译成中文就是

这样呢,通过 UNICODE 的编码方式,全世界各国的文字通过统一的 UNICODE 编码,都有了自己唯一的二进制数值来唯一映射各国的文字;

注意,UNICODE 用两个字节来包罗所有国家的字符编码,也就是说,由它所能包含的字符充其量最多也就是 65536 个;而我们知道,如果真的把所有的汉字全部归纳起来,可能也要接近 2 万个吧,那么 UNICODE 怎么可能收录全球所有的文字编码呢?是的,如果要收录所有的文字,对所有的文字都做唯一的二进制编码,那么 UNICODE 是绝对做不到的,因此,UNICODE 只收录常用的汉字;而收录的常用的中文简体字也就几千个而已;所以,与其说是 UNICODE 收录了所有的文字,倒不如说成是,UNICODE 只是收录了全世界常用的文字;中文在 UNICODE 中的范围区间 4E00-9FBF

用途:Java、Python 3.0 语言的编译和运行的环境使用的就是UNICODE编码;

UTF-8

UTF-8 编码是对 UNICODE 编码的一种转换方式,两者之间是一一映射的;

有了 UNICODE 编码,为什么还需要 UTF-8 编码呢?其实就是西方人觉得,简单的英文字母也需要使用两个字节是极大的浪费,因为要额外使用一倍的存储空间来存储英文字母;所以,“可变长编码”的UTF-8编码就诞生了;UTF-8 编码将 UNICODE 编码映射到 1-4 个字节上,常用的英文字母仅需要 1 个字节即可;可是,中文,对中文,变长了,需要 3 个字节了,这个是由 UTF-8 的编码格式所决定的,后面会对其展开描述;下面,让我们先来看看 UNICODE 与 UTF-8 以及 ASCII 码之间的对应关系;

字符 ASCII UNICODE UTF-8
A 01000001 00000000 01000001 01000001
没有映射关系 01001110 00101101 11100100 10111000 10101101

可以看到,英文字母的编码通过 UTF-8 的映射得到的结果和 ASCII 编码是一致的,那么如果对纯英文的文本内容进行存储,使用 UTF-8 编码格式比 UNICODE 编码格式节约了整整一半的空间;而如果要存储中文的话,就要比使用 UNICODE 多了 1 个字节的空间;呵呵,本来 UTF-8 可变长空间的编码初衷是为了节省空间的,结果,只是对英文字母节省了空间,但是对其它语言却大量的增加了存储空间;

那么 UTF-8 是如何对 UNICODE 进行映射的呢?映射规则如下,

bytes 有效的位数 有效的范围 UTF-8
1 7 \u0000 - \u007F 0xxxxxxx
2 11 \u0080 - \u07FF 110xxxxx 10xxxxxx
3 16 \u0800 - \uFFFF 1110xxxx 10xxxxxx 10xxxxxx
4 21 \u10000 -\u10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  • bytes:表示转换后 UTF-8 的字节数
  • 有效位数:表示 UNICODE 中除去开始的0以后剩下的位数;
  • 有效范围:指转换成 UTF-8 编码以后对应的 UNICODE 原有的编码范围
  • UTF-8: 转码以后的形式;

备注,该映射规则总结自 https://en.wikipedia.org/wiki/UTF-8

其实转换规则非常的简单,笔者大致总结如下,

  1. 如果当前的 UNICODE 编码的有效位数 <= 7,那么采用上述规则 1 进行转换成 UTF-8 编码;
    所以,A的 UNICODE 的有效位数 <= 7,那么转换成 UTF-8 以后,就是 01000001
  2. 如果当前的 UNICODE 编码的有效位数大于 7 且小于等于 11,那么将会采用规则 2 进行转换;
  3. 如果当前的 UNICODE 编码的有效位数大于 11 且小于等于 16,那么将会采用规则 3 进行转换;
    所以,的 UNICODE 的有效位数为 15,满足此规则,那么转换以后,得到的就是 11100100 10111000 10101101,另外,开头的 3 个 1 既是表示要采用多少个字节;
  4. 如果当前的 UNICODE 编码的有效位数大于 16 且小于等于 21,那么将会采用规则 4 进行转换;

总结

  • UNICODE
    正如 UNICODE 诞生 小节我们知道,UNICODE 为全世界不同的文字定义了统一的编码方案;
  • UTF-8
    UTF-8 是 UNICODE 的另外一种编码形式,通过相应的映射规则与之一一映射;其作用主要是对 UNICODE 字符进行重新编码以用于序列化、存储以及在网络上传输;其主要目的就是为了节约英文字符所占的存储空间;

字符串编码(Encode)和解码(Decode)

本章节主要描述在 python 环境中是如何对字符串进行编码(Encode)和解码(Decode)的;

UNICODE

无论是 Python 2.x 还是 3.x,在其编译和运行时刻,在其内部,凡是参与运算的字符(或字符串)统统采用的是 UNICODE 编码形式;什么意思呢?就是说,如果我们要对相关字符进行运算,比如,我们要比对两两字符(或字符串)的内容是否相同,判断字符串的长度,对字符串进行截取操作等等,首先都要将不同编码的字符串转换为统一的 UNICODE 来进行比对和运算,而这个操作就是后面所要讲到的 Decode 部分的内容;其实道理也很简单,如果两个字符串的编码不同,那么比对的基础当然也就没有了,要知道,计算机比对的是二进制串,所以,必须是按照某种约定好的编码形式的二进制串,两两之间才有比较的基础;

为了加深理解,举个例子,字符,一个是通过 GBK 编码,得到的二进制串为\xd6\xd0(为了直观,这里使用十六进制表示);另外一个则通过 UTF-8 编码,得到的二进制串为\xe4\xb8\xad,可以看到,如果直接拿来比对,\xd6\xd0\xe4\xb8\xad不相等的,而,我们所期望的比对结果是相等的,因为无论你的编码格式是什么,我们比对的都是文字,它们必须相等;这也就是为什么在对字符(或字符串)操作以前,必须将其转换(既是 Decode) 为 UNICODE;

在操作系统中,维护了这么一张表,既其它所有编码对 UNICODE 编码的映射表,因此,其它编码可以通过这张表映射为对应的 UNICODE,相应的转换过程就叫做 Decode,而相反,UNICODE 也可以通过这张表映射到其它编码,相应的转换过程叫做 Encode;

8 Bit Strings

构成任意一个字符的最基本单位是 Byte 既 8 Bit,一个 ASCII 码字符由一个 Byte 构成,一个 Unicode 字符由两个 Byte 构成;8 Bit Strings 就是由这些 Byte 所构成的字符序列(一个由二进制所构成的序列)既 Bytes;每个合法的 Bytes 一定对应某种编码格式;比如,一个 Unicode 字符串就是由一组 Unicode 编码的 Bytes 所构成的;

简而言之,8 Bit Strings 就是表示由某种编码格式所构成的 Bytes;

举几个例子,来看看 8 Bit Strings 是如何输入 python 程序的;

  1. 通过终端输入中文原始编码
    笔者使用的是 MacOS 的环境,所以这里是通过其终端应用 Terminal 来进行演示的,首先,必须保证 Terminal 的编码格式支持中文输入,通过 Terminal 的 Preferences 进入 Profile;

    这里笔者将其设置为 UTF-8 的编码格式,当然,读者感兴趣的话,也可以将其设置为 GBK 或者 GB2312 编码,总之要能识别中文的编码;如果设置为了 ASCII 编码或者是其它语言的编码,那么会导致输入的中文不能识别;下面笔者就带领大家来演示一下如何通过终端给 Python 输入 8 Bit Strings

    首先,进入 python 客户端,这里笔者使用的是 Python 2.x 的环境,

    1
    2
    $ python
    Python 2.7.10 (default, May 4 2016, 20:37:30)

    再次,直接输入中国并赋值到变量m

    1
    2
    3
    >>> m = "中国"
    >>> m
    '\xe4\xb8\xad\xe5\x9b\xbd'

    '\xe4\xb8\xad\xe5\x9b\xbd'是什么东西?这就是 UTF-8 编码格式的 8 Bit Strings ,备注,如果是 Python 3.x 的环境,这里输出的内容为b'\xe4\xb8\xad\xe5\x9b\xbd',多了一个前缀 b 表示的是二进制串;从 Unicode 小节我们知道,Python 在运行时刻中所有用于计算的字符都应转换成统一编码 UNICODE,所以下面,我们就来看看,如何将 UTF-8 转换为 UNICODE,

    1
    2
    3
    >>> s = m.decode("UTF-8")
    >>> s
    u'\u4e2d\u56fd'

    这样,我们就通过 decode 将 UTF-8 的 8 Bit Strings 转换为了 UNICODE u'\u4e2d\u56fd';注意,在 python 的输出中,如果有前缀u则表示该二进制串是 UNICODE 的编码格式,\u则表示由十六进制所组成的 UNICODE 二进制串;那么,我们是否可以再次将 UNICODE 编码为 UTF-8 呢?答案当然是可以的,实际上,还可以将该 UNICODE 字符串编码(encode)为 GBK 或者其它任意编码的字符串;下面,笔者就为大家来演示一下,

    • 编码为 UTF-8

      1
      2
      >>> s.encode('UTF-8')
      '\xe4\xb8\xad\xe5\x9b\xbd'
    • 编码为 GBK

      1
      2
      >>> s.encode('GBK')
      '\xd6\xd0\xb9\xfa'

    这里笔者要额外重点提示的是,上述测试用例均是在 Python 2.x 环境中执行的,如果上述用例在 Python 3.x 的环境中运行,会有部分测试用例会出现错误,那是因为 str 对象在 Python 3.x 中已经固定为 UNICODE 编码了,详情参考后续小节 Python 3.x str 对象固定为 UNICODE 编码

  2. 通过文件输入 8 Bit Strings
    这次,我们依然以中文中国为例,来演示如何通过文件来输入 8 Bit Strings ,首先,新建文本,输入中国,保存的时候,这次我么使用GBK编码;

    1
    2
    3
    >>> f = open('/Users/mac/tmp/cngb.txt', 'rb')
    >>> f.read()
    '\xd6\xd0\xb9\xfa'

    注意,参数 rb 表示的是以二进制读取文本的内容;

备注,严格意义而言,8 Bit Strings 一定是经过某种编码所产生的二进制串,注意,不存在没有经过编码的二进制字符串;归纳起来,编码(Encoding)就是字符串能够以二进制的形式输入计算机的基础,因为编码就是格式定义;

编码和译码简介

  • 因为 Python 的运行时采用的是 UNICODE 编码标准,那么必然就会产生将其它编码格式解码或称作译码(Decode)为 UNICODE 的过程;
  • 同样,若要输出字符串,就必须按照规定的编码格式进行输出,因此必然就会有将 Unicode 编码(Encode) 为其它编码的过程;

下面笔者就 Encode 和 Decode 分析进行说明;

Encode

通过上述详细的说明,我们可以知道,Python 中的 Encode 就是将 UNICODE 字符串转换为其它编码格式的字符串;看一个例子,

首先,定义一个 UNICODE 编码格式的字符串中国

1
2
3
>>> u = u'中国'
>>> u
u'\u4e2d\u56fd'

其次,将其编码为 UTF-8 格式的二进制串

1
2
>>> u.encode('UTF-8')
'\xe4\xb8\xad\xe5\x9b\xbd'

备注,如果是

再次,将其编码为 GBK 格式的二进制串

1
2
>>> u.encode('GBK')
'\xd6\xd0\xb9\xfa'

Decode

将其它编码格式的字符串解码或译码为 UNICODE 格式的字符串;备注,其它编码格式的字符串也就是前文所述的 8 Bit Strings 既 Python 输入的 8 Bit Strings

有关 decode 过程中需要注意的地方,有时候需要用到 ‘unicode_escape’,参考:https://stackoverflow.com/questions/13837848/converting-byte-string-in-unicode-string

Python 3.x str 对象固定为 UNICODE 编码

另外这里要特别特别注意的是 Python 2.x 和 3.x 之间的某些区别,下面的测试用例均是在 Terminal 且是在 UTF-8 编码环境下执行的;

Python 2.x 中

1
2
>>> '中'.decode('UTF-8')
u'\u4e2d'

得到了正确的 UNICODE 编码 u'\u4e2d',也就是说这里的字符是 UTF-8 编码的;

Python 3.x 中

1
2
3
4
>>> '中'.decode('UTF-8')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'decode'

相同的操作,在 3.x 中却报错,错误信息是 str 对象不允许 decode,且压根没有该属性了…

1
2
>>> '中'.encode('UTF-8')
b'\xe4\xb8\xad'

可见,Python 3.x 在这里做了极大的变动,字符在 decode 方法执行以前已经是 UNICODE 编码了;这里的原因就是,Python 3.x 中,内置 str 对象的编码固定为 UNICODE 编码了,而是一个str对象,所以上述命令执行以前,在 python 3.x 编译阶段,自动的将根据当前的编码 UTF-8 转换(既 DECODE)为了 UNICODE 编码,创建并返回一个匿名的str对象;

再来看一个例子,下面笔者写了一个极其简单的 test.py

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: GB2312 -*-

import sys

def test():

a = "中国" # 赋值以后,就是自动根据当前文本编码 GB2312 decode 为 UNICODE 了
print(a.encode("GB2312"))

if __name__=='__main__':
test()

执行该代码,

1
2
$ python3 test.py
b'\xd6\xd0\xb9\xfa'

可见,在中国赋值给 str 对象的变量 a 以前,已经译码为了 UNICODE 了;注意,这里的译码过程是,获取源码的 GB2312 编码,然后根据编码格式自动的译码为 UNICODE;

最后,笔者再要重点提示的是,Python 3.x 仅仅是对 str 做了此种改动,如果 8 Bit Strings 是以二进制的方式读取的,和 python 2.x 是一致的;看这样的一个例子,创建一个文件,输入汉字中国,然后设置编码格式为 GB2312,

1
2
3
4
>>> f = open('/Users/mac/tmp/cngb.txt', 'rb')
>>> b = f.read()
>>> b
b'\xd6\xd0\xb9\xfa'

可见,在 Python 3 的环境中,通过读取文本二进制流获取的就是 8 Bit Strings ,既是 8 Bit Strings;继续验证,译码为 UNICODE

1
2
3
>>> u = b.decode("GB2312")
>>> u
'中国'

再转换为 GB2312

1
2
>>> u.encode('GB2312')
b'\xd6\xd0\xb9\xfa'

嗯,挺好玩的…..

总结,在使用字符串的时候,唯一需要注意的是 Python 3.x 中的 str 类型对象,其固定编码为 UNICODE,其它的与 Python 2.x 没什么变化,至少,目前笔者所知的是这个样子;

其它有关字符串编码和解码的操作

ord

ord 函数返回其 code point,也就是在当前编码下该字符对应的二进制数值的十进制表示;注意其参数必须是 UNICODE 格式的字符;

下面,我们就以两个用例来理解一下,确保当前 Terminal 环境是 UTF-8 编码;

ord 接受

1
2
>>> ord('A')
65

下面,我们来测一测中文字,

python 3.x

1
2
>>> ord('中')
20013

注意,因为这里的中文字自动的转换为了 UNICODE,所以可以直接使用;自动转换的过程参考 Python 3.x str 对象固定为 UNICODE 编码

python 2.x

1
2
>>> ord(u'中')
20013

注意,如果在 python 2.x 使用 python 3.x 中的写法,这里是会报错的,因为在 Python 2.x 中并不会自动的将字符转换为 UNICODE;

chr

该方法正好与 ord 相反,一个是通过 UNICODE 字符得到对应的 code point,一个是从 code point 得到相关的 UNICODE 字符;下面,我们就来演示一下,如何通过 chr 函数从 code point 得到相关的 UNICODE 字符吧,

1
2
3
4
>>> chr(20013)
'中'
>>> chr(65)
'A'

不过,如果是在 Python 2.x 环境中

1
2
3
4
>>> chr(20013)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: chr() arg not in range(256)

看来,Python 2.x 只支持 ASCII 码的 chr 操作;

chardet

目前 python 中比较好的一款猜测 8 Bit Strings 编码的一款工具;参考 http://chardet.readthedocs.io/en/latest/usage.html

安装

1
$ pip install chardet

例子

1
2
3
4
5
>>> import urllib
>>> rawdata = urllib.urlopen('http://yahoo.co.jp/').read()
>>> import chardet
>>> chardet.detect(rawdata)
{'encoding': 'EUC-JP', 'confidence': 0.99}

可以看到,从读取的字节流中判断得出其的编码为EUC-JP;注意,上述例子是在 Python 2.x 的环境中执行的;

References

unicode history intro: https://docs.python.org/2/howto/unicode.html
unicode encode and decode: http://pythoncentral.io/python-unicode-encode-decode-strings-python-2x/