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 <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编码就讲完了,如果你有收获,就请点个赞鼓励下吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容