在从普通程序员进阶到优秀程序员的路上,字符编码是一个不得不跨过去的坎,我们几乎所有的程序都会涉及到字符处理,如果跨不过这个坎,那么几乎注定会面对一些坑。
本篇文章试图通过实际的例子来阐释字符编码解码的过程,从而能够更加清晰地认识程序到底是怎样处理字符的。在进入正文之前,你需要先了解字符集和字符编码的区别,需要知道什么是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 <module>
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编码就讲完了,如果你有收获,就请点个赞鼓励下吧!