Python基础之字符编码
一 前言
编码是指信息从一种形式或格式转换为另一种形式或格式的过程。
在计算机中,简单来说,编码就是将人能够读懂的信息(通常称为明文)转换为计算机能够读懂的信息,也就是二进制(0&1)。解码则是编码的逆过程。
字符编码我们可以理解为将人类能够读懂的信息与二进制数建立一种一一对应关系的一种标准,一种准则。
还有一点这里提醒一下:计算机中CPU负责运行程序,而CPU需要从内存中读取程序的指令,然后解码执行,所以任何程序的运行,都需要事先加载到内存中。
二 字符编码的发展
阶段一:ASCII
由于计算机是美国人发明的,因此,最早只有127个字符被编码到计算机里(占用了7位),也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII编码(American Standard Code for Information Interchange,美国标准信息交换代码)。后来把拉丁文也编码进了ASCII表,将最后一位也用上了,称为扩展ANSI编码(了解知识)。ASCII码表如下:

阶段二:各国字符编码的发展
为了处理中文,中国国家标准总局1980年发布《信息交换用汉字编码字符集》提出了GB2312编码,用于解决汉字处理的问题。它生猛地将扩展的第八位对应拉丁文全部删掉,规定一个小于127的字符的意
义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样大约可以表示7000多个字符,具体为:6763个汉字和682个其它符号。GB2312 是对 ASCII 的中文扩展。
后来因为GB2312 无法满足博大精深的汉字文化,1995年又颁布了《汉字编码扩展规范》(GBK,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容)。GBK收录了21886个符号,分为汉字区和图形符号区。汉字区包括21003个字符。
2000年 GB18030取代GBK,该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。
从ASCII、GB2312、GBK 到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS Double-Byte Character Set)。
有的中文Windows的缺省内码还是GBK,可以通过GB18030升级包升级到GB18030。不过GB18030相对GBK增加的字符,普通人是很难用到的,通常我们还是用GBK指代中文Windows内码。
同时,其他各国为了处理各自的文字,也都搞了一套适合自己的编码,比如:日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr里,各国均有各国的标准
阶段三:Unicode
各国有各国的标准,就会不可避免地出现冲突,结果就是,在多语言混合的文本中,显示出来会有乱码。因此,Unicode应运而生,Unicode被称为统一码、万国码、单一码。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了
Unicode标准还在不断发展,但最常用的是用两个字节表示一个字符(如果要用到非常偏僻的字符,就需要4个字节)。现代操作系统和大多数编程语言都直接支持Unicode
Unicode 主要以下两个作用:
- 支持全球所有语言,每个国家都可以不用再使用自己之前的旧编码了,用Unicode就可以了
- Unicode包含了跟全球所有国家编码的映射关系
阶段四:UTF
将编码统一成Unicode编码,虽然,乱码问题从此消失了,但是,如果我们的文本都是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。此时,出现了Unicode Transformation Format,学术名UTF,即:对unicode中的进行转换,以便于在存储和网络传输时节省空间。
- UTF-8: 使用1~4个字节表示所有字符;优先使用1个字符、无法满足则使增加一个字节,最多4个字节。英文占1个字节、欧洲语系占2个、东亚占3个,其它及特殊字符占4个
- UTF-16: 使用2、4个字节表示所有字符;优先使用2个字节,否则使用4个字节表示。
- UTF-32: 使用4个字节表示所有字符;
说明:UTF 是为Unicode编码设计的一种在存储和传输时节省空间的编码方案,我们常常使用UTF-8
同一字符采用ASCII、GBK、Unicode和UTF-8存储表示如下:
从上面的表格还可以发现,UTF-8编码有一个额外的好处,就是ASCII编码实际上可以被看成是UTF-8编码的一部分,所以,大量只支持ASCII编码的历史遗留软件可以在UTF-8编码下继续工作。
Unicode&UTF-8
Unicode:简单粗暴,所有的字符都是2个字节,优点是字符<—>数字(0&1)的转换速度快;缺点是占用空间大
UTF-8:精准,可变长,优点是节省空间;缺点是转换速度慢,因为每次转换都需要计算出需要多长Byte才能够准确表示
三 字符编码转换
通过上面的介绍,我们知道了各字符编码的优缺点,下面让我们了解一下实际的字符编码转换
先提出一个问题:我们常用的文本编辑软件,比如:nodepad++,word,包括pycharm,数据在内存中以什么形式存在?以什么形式存储在硬盘中?
这里用word举例说明,当我们在word上编辑文字的时候,不管是中文字符、英文字符还是其他任何字符,计算机都是不认识的,那么在保存之前数据是通过什么形式存在内存的呢?答案是unicode数据,为什么是unicode数据?这是因为它是万国码,解释起来就是无论中文、英文还是其他任何字符它都有唯一编码对应,兼容性最好,不管是什么字符,都能够正确显示。这里可能我们会觉得为什么不用UTF-8呢?理论上确实是可行的,之所以不用是因为Unicode比UTF-8更高效(Unicode固定用2个字节编码,UTF-8则需要计算)。但是Unicode更浪费空间,没错,这就是用空间换时间的一种做法。
以什么形式存储在硬盘中?答案是通过某种编码方式编码的bytes字节串。比如UTF-8,一种可变长编码,很好的节省了空间;当然还有历史产物的GBK编码等等。其实我们的文本编辑器软件都有默认的保存文件的编码方式,比如:UTF-8、GBK等。当我们点击保存的时候,这些编辑软件已经"默默地"帮我们做了编码工作。
当我们再次打文件时,编辑软件又默默地做了解码的工作,将数据再解码成Unicode,然后就可以呈现明文给用户了!所以,Unicode是离用户更近的数据,bytes是离计算机更近的数据。
这样,如果我们用GBK编写的程序,需要运行在没有GBK编码的计算机上时,只要他的计算机支持Unicode(好消息是:现代操作系统和大多数编程语言都直接支持Unicode),只要给予正确的解码方式加载到内存中转换成Unicode,我们的代码都能够正常显示。
简图如下:
再比如:为了方便传输,我们在浏览网页的时候,服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器:
所以我们会看到很多网页的源码上会有类似<meta charset="UTF-8" />的信息,表示该网页正是用的UTF-8编码
总结:现在计算机系统通用的字符编码工作方式:在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码
四 py文件执行
先明确一个概念:py解释器本身就是一个软件,一个类似于文本编辑器一样的软件
现在让我们一起还原一个py文件从创建到执行的编码过程:
打开pycharm或者nodepad++,创建encode_test.py文件,写入:
print('关于py文件执行')
当我们保存的的时候,encode_test.py文件就以pycharm或者nodepad++设置的编码方式保存到了磁盘;关闭文件后再打开,pycharm或者nodepad++就再以默认的编码方式对该文件打开后读到的内容进行解码,转成unicode到内存我们就看到了我们的明文。
注意:文件以什么编码保存,就必须用什么编码打开,不然则会出现乱码
执行过程
第一阶段:python解释器启动,此时就相当于启动了一个文本编辑器
第二阶段:python解释器打开encode_test.py文件,然后按照指定编码或者默认编码解码存在于磁盘上的bytes数据为unicode数据并加载到内存中
第三阶段:python解释器执行刚刚加载到内存中的encode_test.py的代码(该阶段才会识别python的语法,执行到字符串时,会开辟内存空间存放字符串),具体为:解释器将Unicode数据翻译成C代码再转成二进制的数据流,最后通过控制操作系统调用cpu来执行这些二进制数据,完成代码的执行
python解释器VS文本编辑器
相同点:python解释器是解释执行文件内容的,因而python解释器具备读py文件的功能,这一点与文本编辑器一样
不同点:文本编辑器将文件内容读入内存后,是为了显示/编辑,而python解释器将文件内容读入内存后,是为了执行(识别python的语法)
那么问题来了,文本编辑器有自己默认的编码解码方式,我们的py解释器有吗?
当然有,py2默认ASCII码,py3默认utf-8,可以通过如下方式查询:
import sys print(sys.getdefaultencoding())
具体关于py解释器的字符编码接下来会详细介绍
五 py2中的字符编码
5.1 默认字符编码:ASCII
这也就是说Py2在处理数据时,如果没有指定编码类型,Py2默认将其当做ASCII码来进行处理。这个问题最直接的表现在当我们编写的python文件中包含有中文字符时,在运行时会提示出错。例如:创建一个encode_py2.py脚本文件,然后执行,会报错,报错界面如下图。
country = '中国' print(country)
这个问题出现的原因是:Py2会将整个python脚本中的内容当做ASCII码去处理,当脚本中出现中文字符时,比如这里的“中国”,ASCII码是不能够正常处理的,所以出现了这个错误。
解决的办法是:在文件头部加入一行编码声明,需要注意的是:脚本文件用什么编码编写的则需要声明相应的字符编码,否则依然会出现乱码。
# -*- coding: gbk -*- country = '中国' print(country)
注意:因为win默认字符编码为GBK,所以如果我们在终端显示UTF-8编码的数据,会出现乱码,当然,如果我们在Linux系统(默认字符编码为UTF-8)上显示GBK编码的数据,也会出现乱码。具体原因分析见:常见的编码问题
这里有一个问题,我们如果直接在终端执行,可以正常打印:
这是为什么呢?这是因为在终端直接执行代码时代码直接存在于内存中,根本不需要通过ASCII码解码,是unicode数据。当执行代码时,字符串会以系统默认编码,也就是GBK保存到新的内存空间中,当执行到print()时,自然就可正确显示了。
5.2 py2中的字符串类型
在py2中,有两种字符串类型:str类型和unicode类型,注意,这仅仅是python定义的两个名字,下面然我们看看这两种数据类型在程序运行时存在内存地址的是什么?
# -*- coding: utf-8 -*-
country = '中国'
print type(country) # <type 'str'>
print repr(country) # '\xe4\xb8\xad\xe5\x9b\xbd' utf8字节串
print(country) # 中国
uni_country = u'中国'
# uni_country = country.decode('utf8')
print type(uni_country) # <type 'unicode'>
print repr(uni_country) # u'\u4e2d\u56fd' unicode数据,对应在unicode编码表里对应的位置
print(uni_country) # 中国
内置函数repr()可以帮我们在这里显示存储内容。我们可以看到,str和unicode分别存的是字节数据和unicode数据
我们也可以在win终端中执行,如下:
>>> country = '中国' >>> country '\xd6\xd0\xb9\xfa' # gbk字节串 >>> type(country) <type 'str'> >>> print(country) 中国 >>> uni_country = u'中国' >>> uni_country u'\u4e2d\u56fd' >>> type(uni_country) <type 'unicode'> # unicode数据 >>> print(uni_country) 中国
需要注意的是:win默认字符编码为GBK(Linux操作系统默认为UTF-8编码),所以当我们在终端输入中文字符时,系统就会自动将这个中文字符以GBK的编码传递给Python。
从上面的实例我们可以发现,虽然返回了不同的字节串(utf-8或gbk、unicode),type返回了不同的数据类型(str、unicode),但print打印出了相同的值
这里我们提到了一个“字节串”的名称,字节串是指该字符串在python中的标准形式,也就是说无论一个字符串是什么样的编码,在python中都会有一串字节串来进行表示。字节串是没有编码的,对应的最终交给计算机处理的数据形式。
5.3 encode()与decode()方法
encode()方法将Unicode编码字符转化为其他编码字符,称为编码
decode()方法将其他编码字符转化为Unicode编码字符,称为解码
win终端实例
>>> country = '中国'
>>> country
'\xd6\xd0\xb9\xfa' # gbk字节串,因为win默认为字符编码为GBK
>>> type(country)
<type 'str'>
>>> print(country)
中国
>>> uni_country = country.decode('gbk') # 使用decode()解码,注意这里是gbk,其他方式不能正确解码
>>> uni_country
u'\u4e2d\u56fd' # unicode数据
>>> type(uni_country)
<type 'unicode'>
>>> print(uni_country)
中国
>>> utf_country = uni_country.encode('utf-8') # 使用encode()将unicode编码成utf-8
>>> utf_country
'\xe4\xb8\xad\xe5\x9b\xbd' # utf-8字节串
>>> type(utf_country)
<type 'str'>
>>> print(utf_country) # 出现乱码,在win中不能正确显示
涓浗
>>> import chardet
>>> chardet.detect(utf_country) # 利用chardet模块监测字符串编码
{'confidence': 0.7525, 'language': '', 'encoding': 'utf-8'}
上面的例子中,我们将gbk编码的country通过decode()方法转换为uni_country,然后通过encode()方法将uni_country转换为utf_country。这时我们再用print去输出utf-8编码的字符时会出现乱码。
py2中str和unicode编解码关系图如下:
5.4 py2中的bytes类型
上面的例子中,虽然我们print(country)输出的“中国”,但若直接调用变量country,看到的却是一个个的16进制表示的二进制字节,我们上面称之为“字节串”,在表示形式上它将2进制转成了16进制来表示,目的是为了让人们看起来更可读。其实这就是bytes类型,即字节类型, 8个二进制数为一组称为一个byte(字节),用16进制来表示。
这里想说明的是:py2的字符串其实更应该称为字节串,这点我们通过存储方式就能看出来, 但py2里还有一个类型是bytes类型,它对应的也是“字节串”,难道又叫bytes又叫字符串?是的,在py2里,bytes == str , 其实就是一回事,总结起来就是下面的样子:
写在最后--py2编码特色
看下面一个例子:
# -*- coding:utf-8 -*- country = '中国' print(country) # 中国 print(repr(country)) # '\xe4\xb8\xad\xe5\x9b\xbd' print(u"Hello" + "World") # HelloWorld # print(u'中国'+'你好') # 报错:UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
py2 悄悄掩盖掉了 byte 到 unicode 的转换,只要数据全部是 ASCII 的话,所有的转换都是正确的;一旦一个非 ASCII 字符偷偷进入程序,那么默认的解码将会失效,从而造成 UnicodeDecodeError 的错误。py2编码让程序在处理 ASCII 的时候更加简单,代价是在处理非 ASCII 的时候将会失败
六 py3中的字符编码
相对py2的字符编码,py3做了如下改善:
1. Py3的默认编码方式为UTF-8,所以在Py3中可以不用在py脚本中进行coding声明(前提是py文件为utf-8,如果为其他格式,还是得coding声明)
2. 字符串以unicode编码,这意味着,只要用py3,无论程序是以哪种编码开发的,都可以在全球各国电脑上正常显示,并且系统传递给python的字符不再受系统默认编码的影响,统一为unicode编码
3. 将字符串和字节序列做了区别,字符串str是字符串标准形式(与2.x中unicode类似)存unicode数据,bytes(类似2.x中的str有各种编码)存bytes数据。bytes通过解码转化成str,str通过编码转化成bytes
下面对比了一下py2和py3的执行情况,可以佐证上面的描述:
pycharm示例代码如下:
import json
country ='中国'
print(type(country)) # <class 'str'>
print(json.dumps(country)) # "\u4e2d\u56fd" Unicode数据
print(country) # 中国
utf_country = country.encode('utf8')
print(type(utf_country)) # <class 'bytes'>
print(utf_country) # b'\xe4\xb8\xad\xe5\x9b\xbd' bytes数据
u_country1 = utf_country.decode('utf8')
print(type(u_country1)) # <class 'str'>
print(json.dumps(u_country1)) # "\u4e2d\u56fd" Unicode数据
print(u_country1) # 中国
gbk_country = country.encode('gbk')
print(type(gbk_country)) # <class 'bytes'>
print(gbk_country) # b'\xd6\xd0\xb9\xfa' bytes数据
u_country2 = gbk_country.decode('gbk')
print(type(u_country2)) # <class 'str'>
print(json.dumps(u_country2)) # "\u4e2d\u56fd" Unicode数据
print(u_country2) # 中国
注意:上面我们通过json展示了unicode数据,而实际在py3中,是不能直接看到unicode数据,它会被直接显示为中文的‘中国’,这是因为py3中字符串默认为unicode编码,unicode数据将被直接处理为中文显示出来。
py3中str和bytes类型编解码关系图如下:
py3中对bytes类型的数据用带b前缀的单引号或双引号表示:
x = b'ABC'
要注意区分'ABC'和b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。
纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8、GBK编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。
在bytes中,无法显示为ASCII字符的字节,用\x##显示:
>>> '中国'.encode('utf8')
b'\xe4\xb8\xad\xe5\x9b\xbd'
>>> 'ABC'.encode('utf8')
b'ABC'
如果bytes中包含无法解码的字节,decode()方法会报错,如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:
>>> b'\xe4\xb8\xad\xab'.decode('utf-8', errors='ignore')
'中'
在操作字符串时,我们经常遇到str和bytes的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对str和bytes进行
写在最后--py3编码特色
py3最重要的新特性大概要算是对文本和二进制数据作了更为清晰的区分,不再会对bytes字节串进行自动解码,文本总是unicode,由str类型表示,二进制数据则由bytes类型表示(简单总结一下:想在py3打印字符,必须得是unicode编码,其它编码一律按bytes格式展示)。py3不会以任意隐式的方式混用str和bytes,这使得两者的区分特别清晰。我们不能拼接字符串和字节包,也无能在字节包里搜索字符串(反之亦然),也不能将字符串传入参数为字节包的函数(反之亦然)。
# print(u"Hello" + "Hello") # 字节串和unicode连接 py2:HelloWorld print(b"Hello"+"Hello") # 字节串和unicode连接 py3:报错 TypeError: can't concat bytes to str
七 unicode与gbk的映射表
由上我们可以知道“中国”的unicode编码的映射位置是 u'\u4e2d\u56fd',‘\u4e2d’ 就是“中,到表里搜一下。
“中国”对应的GBK编码是'\xd6\xd0\xb9\xfa' ,2个字节一个中文,"中" 的二进制 "\xd6\xd0"是4个16进制,正好2字节,拿它到unicode映射表里对一下, 发现是G0-5650,并不是\xd6\xd0,同样的,我们找到“国”,'\xb9\xfa'与 G0-397A也对不上,这是为什么呢?
下面我们将GBK编码和映射表里面关于“中”和“国”转换为二进制看看
# 中 GBK编码:d6d0 11010110 11010000 映射表:5650 01010110 01010000 # 国 GBK编码:b9fa 10111001 11111010 映射表:397A 00111001 01111010
有没有发现?当我们把GBK编码的两个字节的第一位都变成0时,就与映射表相同了。这又是为什么呢?
这是因为GBK的编码表示形式决定的。因为GBK编码在设计初期就考虑到了要兼容ASCII,即如果是英文,就用一个字节表示,2个字节就是中文,但如何区别连在一起的2个字节是代表2个英文字母,还是一个中文汉字呢?上面介绍GBK时也提到了,两个大于127的字符连在一起时,就表示一个汉字,也就是相当于128的那个2进制位如果是1,就代表这是个中文。
所以,这里我们可以理解为:unicode在映射表的表达上直接忽略了这点,但真正映射的时候,还是得用GBK真正的编码。
unicode与gbk的映射表地址:http://www.unicode.org/charts/
八 常见的编码问题
8.1 终端下的乱码问题
上面已经分析过了,这里再总结一下,创建一个名为code_test.py的文件,代码如下:
# -*- coding:utf-8 -*- country = '中国' print(country)
文件保存时的编码也为utf-8,为什么在IDE下用2或3执行都没问题,在win下的终端3正确,2乱码呢?
首先我们需要明确一点:cmd.exe本身也一个软件;当我们python2 code_test.py时,python2解释器(默认ASCII编码)去按声明的utf-8编码文件,而文件也是utf-8保存的,解释器执行没问题,也不会报错;当指执行到country = '中国'时,字符串会被以文件的编码方式保存到新的内存空间中,此时也就是utf-8字节串。接下来,当我们print()时,print的内容会传递给cmd.exe来显示,而此时的数据是utf-8编码的字节数据,而cmd这个软件默认的编码解码方式是GBK,所以无法正确解码,自然会乱码。
py3正确的原因是因为py3中字符串对应的就是unicode数据,将unicode传递给cmd,cmd.exe可以识别,所以显示没问题。
所以我们做如下修改后,cmd下用py2也不会有问题,代码如下:
# -*- coding:utf-8 -*- country = u'中国' # 程序执行时,'中国'以unicode形式保存新的内存空间中 print(country)
8.2 文件打开的乱码问题
创建一个open_test文本,内容为:“中国,你好”,用utf-8保存。同时在同目录下创建一个code_test.py文件,代码如下:
data = open('open_test').read()
print(data)
这段代码在Linux下可以正常执行,但在win下(py3解释器)报错:
UnicodeDecodeError: 'gbk' codec can't decode byte 0xad in position 2: illegal multibyte sequence
这是因为win的操作系统安装时是默认的GBK编码,而linux操作系统默认的是utf-8编码;
当执行open()函数时,调用的是操作系统打开文件,操作系统用默认的GBK编码去解码utf-8的文件,自然会出现异常。修改如下:
data = open('open_test',encoding='utf8').read()
print(data)
如果文件保存的是gbk编码,在win 下则不需要指定encoding,但Linux中则需指定。
注意:open()函数在py2里和py3中是不同的,py3中存在一个encoding=None参数
写在最后:
python出现编码错误主要需要注意以下几点,掌握了这几点,基本上就可以处理python编码问题了
- Python解释器的默认编码
- Python源文件文件编码
- Terminal使用的编码
- 操作系统的语言设置














浙公网安备 33010602011771号