Python字符编码之理解

在从普通程序员进阶到优秀程序员的路上,字符编码是一个不得不跨过去的坎,我们几乎所有的程序都会涉及到字符处理,如果跨不过这个坎,那么几乎注定会面对一些坑。
本篇文章试图通过实际的例子来阐释字符编码解码的过程,从而能够更加清晰地认识程序到底是怎样处理字符的。在进入正文之前,你需要先了解字符集和字符编码的区别,需要知道什么是Unicode,什么是UTF-8,GBK等基本概念,如果你不了解,请移步下面的几篇文章:
字符编码详解
字符编码笔记
之后,我们试想一下,程序处理字符的过程是怎样的?我想最开始一定是先打开一个编辑器,把程序写出来,然后将程序保存为一个源文件(Python中就是.py文件),所以我们先从文件的存储开始说起。

源文件的存储

我们在编辑器中写的代码都是字符形式存在的,而当我们要将这些字符存储到硬盘时,必须有一个编码过程,因为计算机只能认识0/1序列,所以这些字符就必须通过一些编码规则转化成二进制序列,然后再存储到硬盘。比如我们写了下面一段程序

s = '你好'
print repr(s), s

当我们存储该文件时,如果是以GB2312编码方式进行存储的,那么文件的二进制表示是这样的

➜  testProgram hexdump -C gb2312encodingfile.py
00000000  73 20 3d 20 27 c4 e3 ba  c3 27 0a 70 72 69 6e 74  |s = '....'.print|
00000010  20 72 65 70 72 28 73 29  2c 20 73 0a              | repr(s), s.|
0000001c

这里73代表s
20代表空格
3d代表=
27代表'
c4 e3代表
ba c3代表,以此类推
在这里可以看出汉字在GB2312中是用两个字节来表示的。
我们再使用utf-8来存储同样的一段代码,看看其二进制表示是什么样子

➜  testProgram hexdump -C utf8encodingfile.py
00000000  73 20 3d 20 27 e4 bd a0  e5 a5 bd 27 0a 70 72 69  |s = '......'.pri|
00000010  6e 74 20 72 65 70 72 28  73 29 2c 20 73 0a        |nt repr(s), s.|
0000001e

同样的这里73代表s
20代表空格
3d代表=
27代表'
但是你好汉字是用三个字节来表示的
e4 bd a0代表
e5 a5 bd代表

现在源文件已经以二进制码流存储到了硬盘,那么源代码又是如何执行的呢?

源代码执行

源代码执行的时候,Python解释器首先会将源文件load进内存当中,然后一行行开始读取文件并解释执行。

但是这里需要注意的是,如果是str字符串,python解释器只会读取其二进制码流,假设我们使用的是gb2312encodingfile.py,那么s指向的字符串你好读进内存后的表示就是c4 e3 ba c3, 当我们使用print打印的时候,如果是在Windows的console上执行,则可以正确执行,显示如下

➜  testProgram python gb2312encodingfile.py
'\xc4\xe3\xba\xc3' 你好

但是在Linux上或者mac上无法正确执行,显示如下:

➜  testProgram python gb2312encodingfile.py
'\xc4\xe3\xba\xc3' ���

这是由于Windows console默认是GBK编解码的(GB2312的扩展),所以可以将\xc4\xe3\xba\xc3正确解码显示成汉字你好,但是在Linux或者Mac上,console的默认编解码方式是UTF-8,所以也就无法将\xc4\xe3\xba\xc3正确显示出来。

另外一个小插曲是,如果代码中有汉字,需要在文件开头声明编码方式(#-*- coding: utf-8 - 或者# coding=utf8),否则解释器默认使用ASCII编码方式去打开源文件,这样就会报错,如下

➜  testProgram python gb2312encodingfile.py
  File "gb2312encodingfile.py", line 1
SyntaxError: Non-UTF-8 code starting with '\xc4' in file gb2312encodingfile.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

但是如果我们的字符串是unicode对象的字符串,那么Python解释器会将字符串的字节序列先进行解码,然后再将解码后的字节序列的引用赋给s,可以更改utf8encodingfile.py代码如下:

#-*- coding: utf-8 -*-
s = '你好'
print repr(s), s

u = u'你好'
print repr(u), u

保存后,使用hexdump查看其二进制编码如下:

➜  testProgram hexdump -C utf8encodingfile.py
00000000  23 2d 2a 2d 20 63 6f 64  69 6e 67 3a 20 75 74 66  |#-*- coding: utf|
00000010  2d 38 20 2d 2a 2d 0a 73  20 3d 20 27 e4 bd a0 e5  |-8 -*-.s = '....|
00000020  a5 bd 27 0a 70 72 69 6e  74 20 72 65 70 72 28 73  |..'.print repr(s|
00000030  29 2c 20 73 0a 0a 75 20  3d 20 75 27 e4 bd a0 e5  |), s..u = u'....|
00000040  a5 bd 27 0a 70 72 69 6e  74 20 72 65 70 72 28 75  |..'.print repr(u|
00000050  29 2c 20 75 0a                                    |), u.|
00000055

仔细观察会发现两个你好字符串都编码成了e4 bd a0 e5 a5 bd
然后在mac上执行,结果如下:

➜  testProgram python utf8encodingfile.py
'\xe4\xbd\xa0\xe5\xa5\xbd' 你好
u'\u4f60\u597d' 你好

可以看出s指向的字节序列是\xe4\xbd\xa0\xe5\xa5\xbd,而u指向的字节序列是\u4f60\u597d (也就是将e4 bd a0 e5 a5 bd 解码成了\u4f60\u597d)

但是如果我们更改的是gb2312encodingfile.py,并使用gb2312编码保存,再执行这个程序看看会是什么结果。
结果直接报错:

➜  testProgram python gb2312encodingfile.py
  File "gb2312encodingfile.py", line 5
    u = u'���'
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xc4 in position 0: invalid continuation byte

这是由于Python解释器尝试用声明的utf-8编码方式去解码gb2312编码的字节序列,所以造成了这样的错误。

至此我们已经知道了Python如何读写源文件的,那么Python执行的时候又是如何读写外部文件的呢?

文件读写

现在我们使用如下代码尝试将字符串写到文件当中,注意源码保存使用utf-8, 文件名为utf8encodingfile_write.py

#-*- coding: utf-8 -*-
s = '你好'
with open('stroutput.txt', 'w') as f:
    f.write(s)

u = u'你好'
with open('unicodeoutput.txt', 'w') as f:
    f.write(u)

在mac上执行,结果如下:

➜  testProgram python utf8encodingfile_write.py
Traceback (most recent call last):
  File "utf8encodingfile_write.py", line 8, in 
    f.write(u)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)说明系统在写文件编码的时候尝试使用ASCII来进行编码,可是我们明明已经声明了使用utf-8啊。

原来文件头声明使用utf-8,只是用于解释器去解释源码文件的时候使用,当我们调用write去写一个文件的时候,会调用系统默认的编码设置来进行编码。我们来看下系统默认的编码是什么:

>>> import sys
>>> sys.getdefaultencoding()
'ascii'

果然,系统默认就是ascii编码方式。
解决这个问题有两种方式,一种是修改系统默认的编码方式,另一种是在open的时候指定编码方式,其中第二种显然更加优雅一些。

# 通过修改系统默认编码方式来实现utf-8编码
import sys
reload(sys) # 这里必须reload一下才能找到setdefaultencoding method
sys.setdefaultencoding('utf-8')


# 通过在codecs.open中设置编码方式
import codecs

with codecs.open("filename", "w", encoding="utf-8") as f:
    f.write(u)

同样的,当我们读取一个文件的时候,也可以通过codecs.open来设定编解码方式,但是首先我们需要知道这个要读取的文件的编码方式,假设文件是以utf-8的方式进行编码的,读取的时候就可以如下:

import codecs

with open("somefile", "r", encoding="utf-8") as f:
    content = f.read()
    

另外,有时我们并非从文件中读取,而是直接使用了一个非标准字符,这是就需要使用decode先解码

# if not decode, will raise exception: 'ascii' codec can't
# decode byte 0xe2 in position 0: ordinal not in range(128)
dash = '–'.decode("utf8")
if dash in title:
    title = title.split(dash)[0]

至此,关于Python编码就讲完了,如果你有收获,就请点个赞鼓励下吧!

你可能感兴趣的:(Python字符编码之理解)