Python编码

Posted dongye95

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python编码相关的知识,希望对你有一定的参考价值。

由:1.https://www.zhihu.com/question/31833164,2.https://www.cnblogs.com/vipchenwei/p/6993788.html 整理


 

一、什么是编码?

1.1 ASCII

  计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。

  在英语世界里,字符的个数非常有限,26个字母(大小写)、10个数字、标点符号、控制符,也就是键盘上所有的键所对应的字符加起来也不过是一百多个字符而已,这在计算机中用一个字节的存储空间来表示一个字符是绰绰有余的,因为一个字节相当于8个比特位,8个比特位可以表示256个符号。于是聪明的美国人就制定了一套字符编码的标准叫ASCII(American Standard Code for Information Interchange),每个字符都对应唯一的一个数字,比如字符A对应的二进制数值是01000001,对应的十进制就是65。最开始ASCII只定义了128个字符编码,包括96个文字和32个控制符号,一共128个字符只需要一个字节的7位就能表示所有的字符,因此 ASCII 只使用了一个字节的后7位,最高位都为0。

1.2 GBK

  当计算机进入中国后不得不面临的一个问题就是字符编码,常见的汉字就有成千上万,这已经大大超出了 ASCII 编码所能表示的字符范围了,于是中国人自己弄了一套编码叫GB2312,又称GB0,1981由中国国家标准总局发布。GB2312 编码共收录了6763个汉字,同时他还兼容 ASCII,GB 2312的出现,基本满足了汉字的计算机处理需要。后来为了兼容一些罕见的字和繁体字,就在GB2312的基础上创建了一种叫 GBK 的编码,GBK 不仅收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。同样 GBK 也是兼容 ASCII 编码的,对于英文字符用1个字节来表示,汉字用两个字节来标识。

1.3 Unicode

  还有欧洲、亚洲其他国家的文字诸如日文、韩文全世界各地的文字加起来估计也有好几十万。如此庞大的字符库究竟用什么方式来表示好呢?于是统一联盟国际组织提出了Unicode编码,Unicode的学名是"Universal Multiple-Octet Coded Character Set",简称为UCS。

  Unicode有两种格式:UCS-2和UCS-4。UCS-2就是用两个字节编码,一共16个比特位,这样理论上最多可以表示65536个字符,不过要表示全世界所有的字符显示65536个数字还远远不过,因为光汉字就有近10万个,因此Unicode4.0规范定义了一组附加的字符编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)。理论上完全可以涵盖一切语言所用的符号。世界上任何一个字符都可以用一个Unicode编码来表示,一旦字符的Unicode编码确定下来后,就不会再改变了。

Unicode 起到了2个作用:

  1. 直接支持全球所有语言,每个国家都可以不用再使用自己之前的旧编码了,用unicode就可以了。(就跟英语是全球统一语言一样)
  2. unicode包含了跟全球所有国家编码的映射关系,

Unicode 导致的问题:

  如果统一成Unicode编码,乱码问题从此消失了。但是,如果你写的文本基本上全部是英文的话,用Unicode编码比ASCII编码需要多一倍的存储空间,在存储和传输上就十分不划算。

 1.4 UTF-8

  所以,本着节约的精神,又出现了把Unicode编码转化为“可变长编码”的UTF-8编码。UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符,用UTF-8编码就能节省空间:

字符ASCIIUnicodeUTF-8
A 01000001 00000000 01000001 01000001
x 01001110 00101101 11100100 10111000 10101101

  对于多字节(n个字节)的字符,第一个字节的前n为都设为1,第n+1位设为0,后面字节的前两位都设为10。剩下的二进制位全部用该字符的unicode码填充。

总结:UTF 是为unicode编码 设计 的一种 在存储 和传输时节省空间的编码方案。

二、编码的转换 

  虽然有了unicode and utf-8 , 但是由于历史问题,各个国家依然在大量使用自己的编码,比如中国的windows,默认编码依然是gbk,而不是utf-8。

  基于此,如果中国的软件出口到美国,在美国人的电脑上就会显示乱码,因为他们没有gbk编码。
  若想让中国的软件可以正常的在 美国人的电脑上显示,只有以下2条路可走:

  1. 让美国人的电脑上都装上gbk编码
  2. 把你的软件编码以utf-8编码

  第1种方法几乎不可能实现,第2种方法比较简单。 但是也只能是针对新开发的软件。 如果你之前开发的软件就是以gbk编码的,上百万行代码可能已经写出去了,重新编码成utf-8格式也会费很大力气。

  针对已经用gbk开发完毕的项目,以上2种方案都不能轻松的让项目在美国人电脑上正常显示,难道没有别的办法了么?
  有, 还记得我们讲unicode其中一个功能是其包含了跟全球所有国家编码的映射关系,意思就是,你写的是gbk的“路飞学城”,但是unicode能自动知道它在unicode中的“路飞学城”的编码是什么,如果这样的话,那是不是意味着,无论你以什么编码存储的数据 ,只要你的软件在把数据从硬盘读到内存里,转成unicode来显示,就可以了。
  由于所有的系统、编程语言都默认支持unicode,那你的gbk软件放到美国电脑 上,加载到内存里,变成了unicode,中文就可以正常展示啦。

总结一点:

  1. unicode:简单粗暴,所有字符都是2Bytes,优点是字符----->数字的转换速度快,缺点是占用空间大。
  2. utf-8:精准,对不同的字符用不同的长度表示,优点是节省空间,缺点是:字符->数字的转换速度慢,因为每次都需要计算出字符需要多长的Bytes才能够准确表示。

  因此,内存中使用的编码是unicode,用空间换时间(程序都需要加载到内存才能运行,因而内存应该是尽可能的保证快);硬盘中或者网络传输用utf-8,网络I/O延迟或磁盘I/O延迟要远大与utf-8的转换延迟,而且I/O应该是尽可能地节省带宽,保证数据传输的稳定性。

  所有程序,最终都要加载到内存,程序保存到硬盘不同的国家用不同的编码格式,但是到内存中我们为了兼容万国(计算机可以运行任何国家的程序原因在于此),统一且固定使用unicode,这就是为何内存固定用unicode的原因,你可能会说兼容万国我可以用utf-8啊,可以,完全可以正常工作,之所以不用肯定是unicode比utf-8更高效啊(uicode固定用2个字节编码,utf-8则需要计算),但是unicode更浪费空间,没错,这就是用空间换时间的一种做法,而存放到硬盘,或者网络传输,都需要把unicode转成utf-8,因为数据的传输,追求的是稳定,高效,数据量越小数据传输就越靠谱,于是都转成utf-8格式的,而不是unicode。

搞清楚了ASCII、Unicode和UTF-8的关系,我们就可以总结一下现在计算机系统通用的字符编码工作方式:

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

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

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

所以你看到很多网页的源码上会有类似<meta charset="UTF-8" />的信息,表示该网页正是用的UTF-8编码。

三、py2的string编码

  在py2中,有两种字符串类型:str类型和unicode类型。

  由于Python创始人在开发初期认知的局限性,其并未预料到python能发展成一个全球流行的语言,导致其开发初期并没有把支持全球各国语言当做重要的事情来做,所以就轻佻的把ASCII当做了默认编码。 当后来大家对支持汉字、日文、法语等语言的呼声越来越高时,Python于是准备引入unicode,但若直接把默认编码改成unicode的话是不现实的, 因为很多软件就是基于之前的默认编码ASCII开发的,编码一换,那些软件的编码就都乱了。所以Python 2 就直接 搞了一个新的字符类型,就叫unicode类型,比如你想让你的中文在全球所有电脑上正常显示,在内存里就得把字符串存成unicode类型。

我们来看一下,在python 2 上写字符串

#coding:utf8
 
s1=\'\'
 
print type(s1) # <type \'str\'>
print repr(s1) #\'\\xe8\\x8b\\x91
 
s2=u\'\'
print type(s2) # <type \'unicode\'>
print repr(s2) # u\'\\u82d1\'

  内置函数repr可以帮我们在这里显示存储内容。

  python2的字符串其实更应该称为字节串。 通过存储方式就能看出来, 但python2里还有一个类型是bytes呀,难道又叫bytes又叫字符串? 嗯 ,是的,在python2里,bytes == str , 其实就是一回事。
  除此之外呢, python2里还有个单独的类型是unicode , 把字符串解码后,就会变成unicode。str和unicode分别存的是字节数据和unicode数据;那么两种数据之间是什么关心呢?如何转换呢?这里就涉及到编码(encode)和解码(decode)了
s1=u\'\'
print repr(s1) #u\'\\u82d1\'

b=s1.encode(\'utf8\')
print b
print type(b)  #<type \'str\'>
print repr(b)  #\'\\xe8\\x8b\\x91\'

s2=\'苑昊\'
u=s2.decode(\'utf8\')
print u        # 苑昊
print type(u)  # <type \'unicode\'>
print repr(u)  # u\'\\u82d1\\u660a\'

#注意
u2=s2.decode(\'gbk\')
print u2  #鑻戞槉
print len(\'苑昊\') #6

无论是utf8还是gbk都只是一种编码规则,一种把unicode数据编码成字节数据的规则,所以utf8编码的字节一定要用utf8的规则解码,否则就会出现乱码或者报错的情况。

py2编码的特色:

#coding:utf8
 
print \'苑昊\' #  苑昊   
print repr(\'苑昊\')#\'\\xe8\\x8b\\x91\\xe6\\x98\\x8a\'
 
print (u"hello"+"yuan")
 
#print (u\'苑昊\'+\'最帅\')   #UnicodeDecodeError: \'ascii\' codec can\'t decode byte 0xe6
                         # in position 0: ordinal not in range(128)

  Python 2 悄悄掩盖掉了 byte 到 unicode 的转换,只要数据全部是 ASCII 的话,所有的转换都是正确的,一旦一个非 ASCII 字符偷偷进入你的程序,那么默认的解码将会失效,从而造成 UnicodeDecodeError 的错误。py2编码让程序在处理 ASCII 的时候更加简单。你复出的代价就是在处理非 ASCII 的时候将会失败。

四、py3的string编码

  时间来到2008年,python发展已近20年,创始人龟叔越来越觉得python里的好多东西已发展的不像他的初衷那样,开始变得臃肿、不简洁、且有些设计让人摸不到头脑,比如unicode 与str类型,str 与bytes类型的关系,这给很多python程序员造成了困扰。
  龟叔再也忍不了,像之前一样的修修补补已不能让Python变的更好,于是来了个大变革,Python3横空出世,不兼容python2,python3比python2做了非常多的改进,其中一个就是终于把字符串变成了unicode,文件默认编码变成了utf-8,这意味着,只要用python3,无论你的程序是以哪种编码开发的,都可以在全球各国电脑上正常显示,真是太棒啦!

  PY3 除了把字符串的编码改成了unicode, 还把str 和bytes 做了明确区分, str 就是unicode格式的字符, bytes就是单纯二进制啦。

  python3 renamed the unicode type to str ,the old str type has been replaced by bytes.

   py3也有两种数据类型:str和bytes;  str类型存unicode数据,bytse类型存bytes数据,与py2比只是换了一下名字而已。

import json

s=\'苑昊\'
print(type(s))       #<class \'str\'>
print(json.dumps(s)) #  "\\u82d1\\u660a"

b=s.encode(\'utf8\')
print(type(b))      # <class \'bytes\'>
print(b)            # b\'\\xe8\\x8b\\x91\\xe6\\x98\\x8a\'


u=b.decode(\'utf8\')
print(type(u))       #<class \'str\'>
print(u)             #苑昊
print(json.dumps(u)) #"\\u82d1\\u660a"


print(len(\'苑昊\')) # 2

py3的编码哲学:

  Python 3最重要的新特性大概要算是对文本和二进制数据作了更为清晰的区分,不再会对bytes字节串进行自动解码。文本总是Unicode,由str类型表示,二进制数据则由bytes类型表示。Python 3不会以任意隐式的方式混用str和bytes,正是这使得两者的区分特别清晰。你不能拼接字符串和字节包,也无法在字节包里搜索字符串(反之亦然),也不能将字符串传入参数为字节包的函数(反之亦然)。

#print(\'alvin\'+u\'yuan\')#字节串和unicode连接 py2:alvinyuan
print(b\'alvin\'+\'yuan\')#字节串和unicode连接 py3:报错 can\'t concat bytes to str

注意:

1.无论py2,还是py3,与明文直接对应的就是unicode数据,打印unicode数据就会显示相应的明文(包括英文和中文)

2.为什么在py3里,把unicode编码后,字符串就变成了bytes格式? 你直接给我直接打印成gbk的字符展示不好么?我想其实py3的设计真是煞费苦心,就是想通过这样的方式明确的告诉你,想在py3里看字符,必须得是unicode编码,其它编码一律按bytes格式展示。
 

五、字符编码的使用

  不管是哪种类型的文件,只要记住一点:文件以什么编码保存的,就以什么编码方式打开.

  下面我们来看看python中关于编码出现的问题:

  如果不在python文件指定头信息#-*-coding:utf-8-*-,那就使用默认的python2中默认使用ascii,python3中默认使用utf-8

  读取已经加载到内存的代码(unicode编码的二进制),然后执行,执行过程中可能会开辟新的内存空间,比如x="hello"

  内存的编码使用unicode,不代表内存中全都是unicode编码的二进制,在程序执行之前,内存中确实都是unicode编码的二进制,比如从文件中读取了一行x="hello",其中的x,等号,引号,地位都一样,都是普通字符而已,都是以unicode编码的二进制形式存放与内存中的.但是程序在执行过程中,会申请内存(与程序代码所存在的内存是俩个空间),可以存放任意编码格式的数据,比如x="hello",会被python解释器识别为字符串,会申请内存空间来存放"hello",然后让x指向该内存地址,此时新申请的该内存地址保存也是unicode编码的hello,如果代码换成x="hello".encode(\'utf-8\'),那么新申请的内存空间里存放的就是utf-8编码的字符串hello了。

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

  如果服务端encode的编码格式是utf-8, 客户端内存中收到的也是utf-8编码的二进制。

  python解释器执行py文件的原理 ,例如python test.py

    第一阶段:python解释器启动,此时就相当于启动了一个文本编辑器。

    第二阶段:python解释器相当于文本编辑器,去打开test.py文件,从硬盘上将test.py的文件内容读入到内存中。python2默认ASCII,所以如果不在开头声明使用utf-8,就不能使用中文等。python3默认使用utf-8,所以可以不声明。

    第三阶段:python解释器解释执行刚刚加载到内存中test.py的代码。此时已经为unicode。

    python解释器执行py文件分为两个步骤:1.将文件读到内存,2.解释执行内容。

5.1 在python2中有两种字符串类型str和unicode

   str类型

   当python解释器执行到产生字符串的代码时(例如s=\'林\'),会申请新的内存地址,然后将\'林\'编码成文件开头指定的编码格式,这已经是encode之后的结果了,所以s只能decode。再次encode就会报错。

#_*_coding:gbk_*_
#!/usr/bin/env python

x=\'\'
# print x.encode(\'gbk\') #报错
print x.decode(\'gbk\') #结果:林

  在python2中,str就是编码后的结果bytes,str=bytes,所以在python2中,unicode字符编码的结果是str/bytes。

#coding:utf-8
s=\'\' #在执行时,\'林\'会被以conding:utf-8的形式保存到新的内存空间中

print repr(s) #\'\\xe6\\x9e\\x97\' 三个Bytes,证明确实是utf-8
print type(s) #<type \'str\'>

s.decode(\'utf-8\')
# s.encode(\'utf-8\') #报错,s为编码后的结果bytes,所以只能decode

  Unicode类型

  当python解释器执行到产生字符串的代码时(例如s=u\'林\'),会申请新的内存地址,然后将\'林\'以unicode的格式存放到新的内存空间中,所以s只能encode,不能decode.

s=u\'\'
print repr(s) #u\'\\u6797\'
print type(s) #<type \'unicode\'>


# s.decode(\'utf-8\') #报错,s为unicode,所以只能encode
s.encode(\'utf-8\') 

  特别说明:

  当数据要打印到终端时,要注意一些问题.

  当程序执行时,比如:x=\'林\';print(x) #这一步是将x指向的那块新的内存空间(非代码所在的内存空间)中的内存,打印到终端,而终端仍然是运行于内存中的,所以这打印可以理解为从内存打印到内存,即内存->内存,unicode->unicode.对于unicode格式的数据来说,无论怎么打印,都不会乱码.python3中的字符串与python2中的u\'字符串\',都是unicode,所以无论如何打印都不会乱码.在windows终端(终端编码为gbk,文件编码为utf-8,乱码产生)

#分别验证在pycharm中和cmd中下述的打印结果
s=u\'\' #当程序执行时,\'林\'会被以unicode形式保存新的内存空间中


#s指向的是unicode,因而可以编码成任意格式,都不会报encode错误
s1=s.encode(\'utf-8\')
s2=s.encode(\'gbk\')
print s1 #打印正常否?
print s2 #打印正常否


print repr(s) #u\'\\u6797\'
print repr(s1) #\'\\xe6\\x9e\\x97\' 编码一个汉字utf-8用3Bytes
print repr(s2) #\'\\xc1\\xd6\' 编码一个汉字gbk用2Bytes

print type(s) #<type \'unicode\'>
print type(s1) #<type \'str\'>
print type(s2) #<type \'str\'>

六、文件从磁盘到内存的编码(******)

  抛开执行执行程序,在文本编辑器上编辑内容的时候,不管是中文还是英文,计算机都是不认识的,那么在保存之前数据是通过什么形式存在内存的呢?yes,就是unicode数据,为什么要存unicode数据,这是因为它的名字:万国码!解释起来就是无论英文,中文,日文,拉丁文,世界上的任何字符它都有唯一编码对应,所以兼容性是最好的。

  好,那当我们保存了存到磁盘上的数据又是什么呢?

  答案是通过某种编码方式编码的bytes字节串。比如utf8---一种可变长编码,很好的节省了空间;当然还有历史产物的gbk编码等等。于是,在我们的文本编辑器软件都有默认的保存文件的编码方式,比如utf8,比如gbk。当我们点击保存的时候,这些编辑软件已经"默默地"帮我们做了编码工作。

  那当我们再打开这个文件时,软件又默默地给我们做了解码的工作,将数据再解码成unicode,然后就可以呈现明文给用户了!所以,unicode是离用户更近的数据,bytes是离计算机更近的数据。

  先明确一个概念:py解释器本身就是一个软件,一个类似于文本编辑器一样的软件!

  现在让我们一起还原一个py文件从创建到执行的编码过程:

  打开pycharm,创建hello.py文件,写入

ret=1+1
s=\'苑昊\'
print(s)

      当我们保存的的时候,hello.py文件就以pycharm默认的编码方式保存到了磁盘;关闭文件后再打开,pycharm就再以默认的编码方式对该文件打开后读到的内容进行解码,转成unicode到内存我们就看到了我们的明文;

      而如果我们点击运行按钮或者在命令行运行该文件时,py解释器这个软件就会被调用,打开文件,然后解码存在磁盘上的bytes数据成unicode数据,这个过程和编辑器是一样的,不同的是解释器会再将这些unicode数据翻译成C代码再转成二进制的数据流,最后通过控制操作系统调用cpu来执行这些二进制数据,整个过程才算结束。

  那么问题来了,我们的文本编辑器有自己默认的编码解码方式,我们的解释器有吗?

  当然有啦,py2默认ASCII码,py3默认的utf8,可以通过如下方式查询

import sys
print(sys.getdefaultencoding())

大家还记得这个声明吗?

#coding:utf8

  是的,这就是因为如果py2解释器去执行一个utf8编码的文件,就会以默认地ASCII去解码utf8,一旦程序中有中文,自然就解码错误了,所以我们在文件开头位置声明 #coding:utf8,其实就是告诉解释器,你不要以默认的编码方式去解码这个文件,而是以utf8来解码。而py3的解释器因为默认utf8编码,所以就方便很多了。

 

  注意:我们上面讲的string编码是在cpu执行程序时的存储状态,是另外一个过程,不要混淆!

七、常见的编码问题

7.1 cmd下的乱码问题

#coding:utf8
print (\'苑昊\')

文件保存时的编码也为utf8。

思考:为什么在IDE下用2或3执行都没问题,在cmd.exe下3正确,2乱码呢?

      我们在win下的终端即cmd.exe去执行,大家注意,cmd.exe本身也一个软件;当我们python2 hello.py时,python2解释器(默认ASCII编码)去按声明的utf8编码文件,而文件又是utf8保存的,所以没问题;问题出在当我们print\'苑昊\'时,解释器这边正常执行,也不会报错,只是print的内容会传递给cmd.exe用来显示,而在py2里这个内容就是utf8编码的字节数据,可这个软件默认的编码解码方式是GBK,所以cmd.exe用GBK的解码方式去解码utf8自然会乱码。

py3正确的原因是传递给cmd的是unicode数据,cmd.exe可以识别内容,所以显示没问题。

明白原理了,修改就有很多方式,比如:

print (u\'苑昊\')

改成这样后,cmd下用2也不会有问题了。

7.2 open()中的编码问题

创建一个hello文本,保存成utf8:

苑昊,你最帅!

同目录下创建一个index.py

f=open(\'hello\')
print(f.read())

为什么 在linux下,结果正常:苑昊,在win下,乱码:鑻戞槉(py3解释器)?

因为你的win的操作系统安装时是默认的gbk编码,而linux操作系统默认的是utf8编码;

当执行open函数时,调用的是操作系统打开文件,操作系统用默认的gbk编码去解码utf8的文件,自然乱码。

解决办法:

f=open(\'hello\',encoding=\'utf8\')
print(f.read())

如果你的文件保存的是gbk编码,在win 下就不用指定encoding了。

另外,如果你的win上不需要指定给操作系统encoding=\'utf8\',那就是你安装时就是默认的utf8编码或者已经通过命令修改成了utf8编码。

注意:open这个函数在py2里和py3中是不同的,py3中有了一个encoding=None参数。

 

以上是关于Python编码的主要内容,如果未能解决你的问题,请参考以下文章

使用非utf-8编码在Python中解析XML

从 XML 声明片段获取 XML 编码:部分内容解析不支持 XmlDeclaration

Python之如何优雅的重试

《安富莱嵌入式周报》第279期:强劲的代码片段搜索工具,卡内基梅隆大学安全可靠C编码标准,Nordic发布双频WiFi6 nRF7002芯片

常用python日期日志获取内容循环的代码片段

python 有用的Python代码片段