python编码问题大终结

 

一、了解字符编码的知识储备

  1. 文本编辑器存取文件的原理(nodepad++,pycharm,word)

     打开编辑器就打开了启动了一个进程,是在内存中的,所以在编辑器编写的内容也都是存放与内存中的,断电后数据丢失,因而需要保存到硬盘上,点击保存按钮,就从内存中把数据刷到了硬盘上。在这一点上,我们编写一个 py 文件(没有执行),跟编写其他文件没有任何区别,都只是在编写一堆字符而已。

    即: 在没有点击保存时, 我们所写的内容都是写入内存。注意这一点,很重要!!当我们点击保存,内容才被刷到硬盘。

    上面做了两件事:写内容到内存, 从内存将内存刷到硬盘。这是两个过程。

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

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

    第二阶段:python 解释器相当于文本编辑器,去打开 test.py 文件,从硬盘上将 test.py 的文件内容读入到内存

    第三阶段:python 解释器解释执行刚刚加载到内存中 test.py 的代码

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

二、字符编码简介

  要搞清楚字符编码,首先要解决的问题是:什么是字符编码?

  我们都知道,计算机要想工作必须通电, 也就是说‘电’驱使计算机干活, 而‘电’的特性,就是高低电平 (高低平即二进制数 1, 低电平即二进制数 0), 也就是说计算机只认识数字 (010101). 如果我们想保存数据, 首先得将我们的数据进行一些处理,最终得转换成 010101 才能让计算机识别。

  所以必须经过一个过程:

  字符 --------(翻译过程)-------> 数字 

  这个过程实际就是一个字符如何对应一个特定数字的标准,这个标准称之为字符编码。

  那么问题就来了?作为一种编码方案,还得解决两个问题:

    a. 字节是怎么分组的,如 8 bits 或 16 bits 一组,这也被称作编码单元。

    b. 编码单元和字符之间的映射关系。例如,在 ASCII 码中,十进制 65 映射到字母 A 上。

  ASCII 码是上个世纪最流行的编码体系之一,至少在西方是这样。下图显示了 ASCII 码中编码单元是怎么映射到字符上的。

  

三、字符编码的发展史

阶段一:现代计算机起源于美国,最早诞生也是基于英文考虑的 ASCII

  随着计算机越来越流行,厂商之间的竞争更加激烈,在不同的计算机体系间转换数据变得十分蛋疼,人们厌烦了这种自定义造成的混乱。最终,计算机制造商一起制定了一个标准的方法来描述字符。他们定义使用一个字节的低 7 位来表示字符,并且制作了如上图所示的对照表来映射七个比特的值到一个字符上。例如,字母 A 是 65,c 是 99,~ 是 126 等等, ASCII 码就这样诞生了。原始的 ASCII 标准定义了从 0 到 127 的字符,这样正好能用七个比特表示。

  为什么选择了 7 个比特而不是 8 个来表示一个字符呢?我并不关心。但是一个字节是 8 个比特,这意味着 1 个比特并没有被使用,也就是从 128 到 255 的编码并没有被制定 ASCII 标准的人所规定,这些美国人对世界的其它地方一无所知甚至完全不关心。其它国家的人趁这个机会开始使用 128 到 255 范围内的编码来表达自己语言中的字符。例如,144 在阿拉伯人的 ASCII 码中是گ,而在俄罗斯的 ASCII 码中是ђ。ASCII 码的问题在于尽管所有人都在 0-127 号字符的使用上达成了一致,但对于 128-255 号字符却有很多很多不同的解释。你必须告诉计算机使用哪种风格的 ASCII 码才能正确显示 128-255 号的字符。

  总结:ASCII,一个 Bytes 代表一个字符(英文字符 / 键盘上的所有其他字符),1Bytes=8bit,8bit 可以表示 0-2**8-1 种变化,即可以表示 256 个字符,ASCII 最初只用了后七位,127 个数字,已经完全能够代表键盘上所有的字符了(英文字符 / 键盘的所有其他字符),后来为了将拉丁文也编码进了 ASCII 表,将最高位也占用了。

阶段二: 为了满足中文,中国人定制了 GBK

  GBK:2Bytes 代表一个字符;为了满足其他国家,各个国家纷纷定制了自己的编码。日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr

阶段三:万国码 Unicode 编码

  后来,有人开始觉得太多编码导致世界变得过于复杂了,让人脑袋疼,于是大家坐在一起拍脑袋想出来一个方法:所有语言的字符都用同一种字符集来表示,这就是 Unicode。

Unicode 统一用 2Bytes 代表一个字符,2**16-1=65535,可代表 6 万多个字符,因而兼容万国语言. 但对于通篇都是英文的文本来说,这种编码方式无疑是多了一倍的存储空间(英文字母只需要一个字节就足够, 用两个字节来表示, 无疑是浪费空间). 于是产生了 UTF-8,对英文字符只用 1Bytes 表示,对中文字符用 3Bytes.UTF-8 是一个非常惊艳的概念,它漂亮的实现了对 ASCII 码的向后兼容,以保证 Unicode 可以被大众接受。

在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 US-ASCII 相同的编码。这意味着 1980 年代写的文档用 UTF-8 打开一点问题都没有。只有 128 号及以上的字符才用 2 个,3 个或者 4 个字节来表示。因此,UTF-8 被称作可变长度编码。于是下面字节流如下:

    0100100001000101010011000100110001001111

  这个字节流在 ASCII 和 UTF-8 中表示相同的字符:HELLO

  至于其他的 UTF-16,这里就不再叙述了。

  总结一点:unicode:简单粗暴,所有字符都是 2Bytes,优点是字符 -----> 数字的转换速度快,缺点是占用空间大。

       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。

 四、字符编码的使用

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

  下面我们来看看 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 编码的二进制

五、Python2 与 python3 编码区别

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

   str 类型

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

#_*_coding:gbk_*_
2 #!/usr/bin/env python
3 
4 x=''
5 # print x.encode('gbk') #报错
6 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'>

  2. 在 python3 中也有两种字符串类型 str 和 bytes

  str 类型变为 unicode 类型

#coding:utf-8
s='' #当程序执行时,无需加 u,'林' 也会被以 unicode 形式保存新的内存空间中,

#s 可以直接 encode 成任意编码格式
s.encode('utf-8')
s.encode(
'gbk')

print(type(s)) #<class 'str'>

  bytes 类型

#coding:utf-8
s='' #当程序执行时,无需加 u,'林' 也会被以 unicode 形式保存新的内存空间中,

#s 可以直接 encode 成任意编码格式
s1=s.encode('utf-8')
s2
=s.encode('gbk')

print(s) #
print(s1) #b'\xe6\x9e\x97' 在 python3 中,是什么就打印什么
print(s2) #b'\xc1\xd6' 同上

print(type(s)) #<class 'str'>
print(type(s1)) #<class 'bytes'>
print(type(s2)) #<class 'bytes'>