Python 编码

都说”人生苦短,我用 Python”,但是不了解 Python 编码的问题绝对会陷入痛苦深渊。

问题

最近在开发公司后台模块的时候发现审计日志打出了乱码,经过排查发现写审计模块的哥们虽然考虑到了编码,但是对所有消息一股脑进行了编码。那些乱码就是二次编码造成的。当发现问题的时候,我才发现很多人包括自己对 Python 的编码都是似懂非懂。所以,本文针 Python 编码进行讨论。

编码

计算机存储的是二进制,0和1

因此,无论是英文字母还是中文,在计算机存储的时候都要转化成0和1组成的二进制串。

老美的 ASCII

学过 C 语言的对 ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)一定不陌生。

在老美的世界里,字符非常有限,52个字母(大小写)、10个数字、标点符号和控制符等加起来不过一百多,给每个编个号,8个比特位就表示了。

image-20190429213753384

ASCII 的局限性

随着计算机的发展和普及,各个国家在发展计算机的同时发现自己的语言用 ASCII 码不能完成编码。以中国为例,汉语光常见的汉字就成千上万,ASCII 码的设计只能表达256个字符,完全不够用。因此,西欧设计了基于 ASCII 的 EASCII 码,中国设计了大家都熟悉的 GB2312,又在此基础上设计了 GBK。

多种编码的问题

多种编码带来的问题就像同一个项目,每个人对同一个功能都开发了自己专有的接口,那么在最后功能集成的时候必须识别消息是从哪个模块来的,对全世界的信息大互联造成了困扰。

因此统一联盟国际组织提出了 Unicode 编码,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode 有两种格式:USC-2 和 USC-4。其中,USC-2用两个字节编码,也就是16个比特位;而 USC-4用4个字节编码,也就是32个比特

但是 Unicode 也带来一些问题。

比如字符”A”,使用 ASCII 进行编码明明可以用一个字节,但是用Unicode 就要2个字节。也就是说,如果我为了通用性使用 Unicode在网络上传输一部原本用 ASCII 码编码 只有 1MB 的英文小说,网络流量会有 2MB 。

另一方面,Unicode 只规定了如何编码,没有规定如何传输、保存这个编码,那么对于一个二进制的串,计算机也一脸懵逼,我是把他按 ASCII 编码还是 GBK 编码呢?

UTF-8

UTF-8 是一种针对 Unicode 的、广泛应用于互联网的变长字符编码。其最主要的优点就是根据具体情况用1-4个字节来表示一个字符,以节省空间。

对于 UTF-8 我们需要记住以下几点:

  1. UTF-8 是对 Unicode 的一种编码:Unicode 是一种十六进制编码,UTF-8 是二进制编码;
  2. UTF-8 可以表示ASCII 码,ASCII 是 UTF-8 的子集。

编码之间的关系

我们需要把 Unicode 看做各种编码的桥梁

不同的字符编码集如UTF-8、GBK、ISO8859-1等等的字符串通过一个中间桥梁unicode来转换。

Python 编码

一个常见错误

由于 Python 的诞生时间比 Unicode 早,因此 Python 的默认编码是 ASCII 码:

1
2
3
>>> import sys
>>> sys.getdefaultencoding()
'ascii'

所以,我们在 Python 源代码文件中如果不显式指定编码的话,将出现语法错误

1
2
#test.py
print "你好"

运行 python test.py 就会出现如下错误:

1
File “test.py”, line 1 yntaxError: Non-ASCII character ‘\xe4′ in file test.py on line 1, but no encoding declared; see http://www.python.org/ ps/pep-0263.html for details

因此,一般学习 Python 都会在第一节课要求你在文件中加入以下编码格式:

1
2
3
# coding=utf8
或者
# -*- coding: utf-8 -*-

Python 字符串

在 Python 中,和字符串相关的数据类型分别是 strunicode, 他们都是 basestring 的子类,可见他们是两种不同类型的字符串对象,他们之间以 encodedecode两种方法进行转换。

其区别在于:

  1. str 是以 UTF-8/ASCII 等编码格式编码后的表示;
  2. unicode 是 str 以指定编码方式解码后的表示。

image-20190429222425672

1
2
3
4
#从str类型的字符串转换到unicode
s.decode(encoding) =====><type 'str'> to <type 'unicode'>
#从str类型的字符串转换到unicode
u.encode(encoding) =====><type 'unicode'> to <type 'str'>

问题:现在假设我想要把一个gbk字符编码的str对象转换为utf-8的str对象,该如何转换呢?

现在默认我是在windows下操作,操作系统的字符编码默认是gbk的:

1
2
3
4
5
6
>>> s="你好"
>>> s
'\xc4\xe3\xba\xc3'
>>> s1 = s.decode('gbk').encode('utf-8')
>>> s1
'\xe4\xbd\xa0\xe5\xa5\xbd'

str 和 unicode 的用处

计算机系统通用的字符编码工作方式:在计算机内存中,统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为 UTF-8 编码。

例如,用记事本编辑的时候,从文件读取的 UTF-8 字符被转换为 Unicode 字符到内存里,编辑完成后,保存的时候再把 Unicode 转换为UTF-8保存到文件。

image-20190429222033548

再例如,浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:

image-20190429222828125

乱码

所以出现乱码的原因都是:字符经过不同编码解码,在编码的过程中使用的编码格式不一致。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
# encoding: utf-8
>>> a = '好'
>>> a
'\xe5\xa5\xbd'
>>> b=a.decode('utf-8')
>>> b
u'\u597d'
>>> c=b.encode('gbk')
>>> c
'\xba\xc3'
>>> print c
��

所以遇到乱码,是因为在设计时没有用统一的编码,考虑:

  1. 碰到问题,问一下自己,我现在是哪种编码;
  2. 同一种编码才能交互,那我应该是哪种编码。

在开发过程中养成以下习惯:

  • 确定一种内部编码;
  • 内部编码的选择优先级如下:程序必须使用的编码、第三方包使用的编码、你喜欢的编码、Unicode;
  • 在输出时再更改到特定的编码;

参考

  1. Python 编码为什么那么蛋疼?
  2. 字符串和编码
  3. 五分钟战胜 Python 字符编码
  4. 理解Python字符集编码