「码动四季·开源同行」python语言:字符编码

张开发
2026/4/21 2:42:29 15 分钟阅读

分享文章

「码动四季·开源同行」python语言:字符编码
一、理解字符编码理论储备知识1、字符编码说明字符编码这个知识点其实只是涉及到一行代码但是它非常重要据不完全统计现在软件30%的损失都是由于乱码问题所导致的这个问题是最容易被大家所忽视的因为使用的时候只是一行代码的问题但是它的里面包含很多的知识大部分人更加倾向于直接掌握结果而不考虑它内部的知识这就导致了一旦遇到字符编码的乱码问题就会手足无措。你之前可能看过一些相关的介绍正确与否我们先不做评论在这篇文章中我们会对字符编码进行全方位的介绍。字符编码的特点是理论非常多而结论非常少但是如果不知道理论结论可能永远也无法理解而且以后遇到字符编码问题就会不知所措。目前在网上几乎没有人能够清晰的把它说明白因为字符编码的发展史涵盖了整个计算机发展的过程我们不会直接拿出现成的结论或者猜测的结论而是会从客观展示出来的打印结果来论证我所讲的就是正确的。2、计算机运行应用软件原理在正式介绍字符编码之前我们需要先了解计算机的运行与哪些核心部件相关CPU运行程序内存临时存储数据一个软件要想运行必先加载到内存硬盘软件的数据要想永久存储一定要存入硬盘如下图所示一个应用软件的启动过程是1.应用软件存放于硬盘上2.应用软件程序从硬盘读入到内存3.CPU调用内存中与该应用软件相关的数据进行处理3、保存文件的原理接下里来我们再来看一下一个文本编辑器保存文件的过程你可以下载安装一个notepad编程软件1.如下图所示当我启动notepad程序之后就是把这个程序读入到内存2.我在notepad程序中输入“HelloWorld”之后这个内容临时保存到了内存中3.你现在看到的屏幕上的内容是notepad程序在你写入的同时返回的结果存在内存中的特点是只要一断电数据就会丢失所以我们要想永久的保存数据需要在notepad程序上执行一个操作把数据永久保存到硬盘notepad程序会给你附加一个自动保存到硬盘的功能这是为了提升用户的使用体验早期的程序没有这个功能。4、执行Python程序的原理接下来我们再来看一下Python解释器执行Python程序的原理也是分为三个阶段以Python3解释器和test.py文件为例1.先启动Python解释器把Python3解释器这个应用软件读入内存2.把test.py文件读入内存3.Python3解释器识别Python语法解释执行test.py程序注意我们写的Python代码如果没有解释器解释执行和写一个普通的文件没有任何区别这也就是说你可以使用Pycharm写Python代码也可以使用WPS写Python代码在编写Python程序时候没有语法的概念检测语法Pycharm给你附加的功能但其实本质也是一个文本编辑器编写的结果和编写一个普通文件是没有任何区别的只有在第三阶段执行Python程序的时候才会监测语法。二、字符编码介绍1、字符编码初次登场计算机是基于电工作的高电平用数字1表示低电平用数字0表示计算机也只能识别010101这种东西这就是二进制计算机的工作原理就是基于二进制工作的。我们平时在使用计算机的时候并不是使用二进制控制的使用的都是人类的字符中国人使用汉语美国人使用英语但是这些人类的字符计算机是看不懂的要想让计算机能够看得懂必先经历一个过程人类的字符翻译二进制数字我们在notepad程序中写了一个“你好”就把这个内容写入到了内存中计算机要想识别必先经历一个翻译的过程这个翻译肯定不能随便翻译的因为在取这个数据的时候仍需按照二进制数字与人类的字符一一对应的取出来所以必须要遵循一个标准这个标准就是字符编码表我们在notepad程序中写了一个“HelloWorld”在计算机内部已经事先存好了这张表每一个字母包含大写字母和小写字母和空格回撤标点符号这些东西都对应一个数字然后再把这些数字转化成二进制这样一个字符就会对应一组二进制数字也就完成了写入的过程当打印的时候再反过来一组二进制数字对应一个字符。所以我们就清楚了内存上应该有这样一张字符编码表早期的时候硬盘上保存的也是二进制所以硬盘上无需有字符编码表。2、字符编码发展史计算机起源于美国美国人说英语美国人当时设计的时候根本就没考虑过中国人有一天也能用的起计算机那么当时美国人设计的时候就只需要考虑计算能识别英文符号就可以了所以当时的字符编码表就只是英文字符与数字的对应关系所有这些加起来一共120多种就够了那么也就应该有120多个不同的数字来表示这些字符。计算机的数字是二进制的要想用010101这种东西表示出120多个数字应该用几位二进制数?一位二进制数只能表示0或者1两个数字除去0之外只能表示1个数字两位二进制数00011011能表示3个数字三位二进制数000001010011100101110111能表示7个数字一特四位二进制数能表示2**4-115个数字最开始设计的时候设计了八位二进制数最小的是00000000最大的是11111111它能够表示281255个数字不包含0但是最早期只是使用了后面的七位二进制位能够表示1127的数字就够了留下一位二进制位为了以后的扩展留一些余地。一个二进制位称为一个比特位8个比特位称为一个字节8bit1bytes美国人用8个比特位来表示一个英文字符所以一个英文字符占一个字节。这个字符编码表就是ASCⅡ表这也是最早的字符编码表如下图所示。后来中国人也开始使用计算机了ASCⅡI只有英文字符的对应关系而且8个比特位最多能表示200多个字符的对印关系如果有一个人说他认识200多个汉字估计这个人小学还未毕业。在这样的场景下中国人也定制出了自己的字符编码表叫做GBKgb2312为了表示出更多的数字GBK编码表用16个比特位来表示一个中文字符那么他所能表示的汉字个数就是216165535个这个基本上也就涵盖了所有我们常用的汉字。所以GBK使用两个字节来表示一个中文字符但是GBK来表示一个英文字符还是用一个字节为了和ASCII统一。不只是中国人能用电脑日本人和韩国人也能用电脑日本人规定了自己的Shift_JIS编码韩国人规定了自己的Euc-kr编码另外韩国人说计算机是他们发明的要求世界统一用韩国编码但世界人民没有搭理他们。到了这个阶段如果每个国家的东西都是自己国家的人看这当然没有问题但是显然我们有这样的需求这就出现了一个问题你的硬盘上可能有日本人和韩国人编码的软件但是内存中的字符编码表只有GBK那么就会出现乱码你不是秦始皇自然做不出他那么伟大是事情所以必须找到一种能够兼容万国语言的字符编码其实6万多就足够表示了各国语言虽然不同但是所用的符号都非常类似这个字符编码就是Unicode它使用16个比特位也就是两个字节来表示一个字符。这也就意味着任何国家的数据到了内存中都是Unicode编码这样内存中就不会出现乱码的问题了。除此之外还有一个很重要的问题之前用各国编码写的保存在硬盘上的文件不能废弃这是历史遗留问题所以Unicode还必须要有一个非常重要的功能把各国编码的文件转化成Unicode如下图所以。这样有了Unicode之后一方面可以兼容万国语言另一方面老的软件也可以在不同国家的机器上运行。至此Unicode字符编码已经解决的大部分的乱码问题但是还存在一个可以优化的空间内存中用Unicode编码硬盘上面有各国编码的软件以后写程序肯定是趋向于全部使用Unicode编码但是如果一篇文档当部分都是英文写一个同样的内容原来的ASCII一个英文字符占用一个字节而Unicode一个英文字符就会占用两个字节这样的话就会增加硬盘的占用并且加大了O操作的时间(IO操作暂且先理解为读写操作在第五阶段最后一章单独讲解IO操作。本着节约的精神又出现了把Unicode编码转化为“可变长编码”的uTF-8可变长全称unicodeTransformation Format编码。UTF-8编码把—个Unicode字符根据不同的数字大小编码成1-6个字节常用的英文字母被编码成1个字节汉字通常是3个字节只有很生僻的字符才会被编码成4-6个字节。如果你要传输的文本包含大量英文字符用UTF-8编码就能节省空间。所以现在硬盘上的字符编码一般是“UTF-8”内存中用的字符编码一般是Unicode以后肯定会趋向于内存和硬盘的存储都是用UTF-8这种字符编码但是现阶段都是被逼的Unicode编码还要负责转换历史遗留问题的编码。三、乱码问题的产生与解决1、乱码问题的成因接下来就是我们做实验的过程了内存中的编码都是Unicode如果忽略硬盘在内存中随便写什么编码都不会出现乱码但是因为硬盘的存在就会出现由内存向硬盘保存的时候你要指定一个字符编码比如说是GBK这时就是由Unicode转化称GBK当把这个硬盘文件重新在内存读取的时候你也要告诉计算机按照GBK编码来读取它才会对应的把数据由GBK编码反解成Unicode编码写入到内存。如果你在这时告诉你计算机用ASCII的标准来反解数据那么就无法反解出原来保存的数据内容计算机懵圈了呈现给你的就是它懵圈后的结果。2、保证不乱码的方案保证不出现乱码问题其实结论就只有一个文件用什么编码保存的就用什么编码读取注意我们能控制的只是文件由内存保存到硬盘的编码。3、人为制造乱码接下来我们会在notepad程序上演示人为制造乱码的过程首先把程序保存文件的编码改成日文的编码见参考notepad的编码问题-百度经验然在在程序里面先后写入中文的“你瞅啥”和日文的“あなたを見て”这是日文的“瞅你咋地”接下里来保存。注意在保存过程中其实计算机已经不能识别中文的“你瞅啥”了但是它不能报错呀他一定要硬存所以这个保存的过程并没有什么问题你现在看到的结果还是在内存中的。保存之后我们关掉再重新读取先试用ASCLL试一下会看到什么结果。全都是乱码那么我们再改成日文的编码试试日文“瞅你咋地”已经读取出来了但是我们存的汉字就读不出来了这时无论我们改成任何编码汉字都是读不出来的因为保存的时候计算机就已经无法识别了他并没有完成有效的数据保存。所以以后写程序写文件都应该用UTF-8编码来写这样就不会发生乱码了。四、Python中的字符编码问题1、解释器读取文件时字符编码异同我们现在用GBK编码在文件中写一个“你好”然后用Python解释器来解释执行不用Pycharm你会发现在Python2和Python3中分别提示以下错误信息如下图所示这就说明如果你不指定字符编码Python3解释器默认使用UTF-8编码来读Python2解释器默认使用ASCIl来读为了解决这个问题我们需要再写Python文件开头就指定好字符编码如下图所示再次调用解释器解释执行的时候就不会发生字符编码的错误了文件头的作用就是告诉Python解释器用指定的字符编码去读取文件内容。在Pycharm中当我指定好了读取文件的字符编码它会自动改变保存写入到硬盘的字符编码如下图所示很多人都知道在写代码的时候文件最上方写一行指定字符编码的文件头:# coding:utf-8但是可能并不知道原因在Pycharm中只要你这样写了就不会发生任何的字符编码问题但是如果换一个其他的IDE工具这一行代码代表的是读文件所使用的字符编码如果这个IDE工具默认保存写入到硬盘的字符编码是“GBK”而且他不会随着你的文件头而改变那么它保存的时候就是以GBK保存的而读的时候指定了UTF-8的字符编码计算机就会读不出来换了人来解决这个问题如果不清楚它的成因也可能会很懵逼。好就好在大部分的文本编辑器默认用的字符编码都是UTF-8一般人们写文件头一般也是指定UTF-8为读取的标准。所以大部分不懂字符编码的人出现错误可能性也比较低。2、解释器执行文件时字符编码异同Python中有一种数据类型会涉及到字符的概念这种数据类型就是字符串。我们在写程序时候如果不指定字符编码保存的时候默认就是UTF-8的字符编码而Python3解释器默认也是使用UTF-8来读取的所以它在Python3中运行是没有任何问题的但是Python2中就不行了所以为了执行Python程序的前两个阶段不出现问题我们统一的都会加上文件头指定UTF-8字符编码但是到了第三个阶段就会开始识别语法了我们在Pycharm中写入如下代码内容计算机在执行的时候会先把这些代码以Unicode编码读入内存然后Python解释器检测到代码中要保存一个“上”这个字符串变量解释器就会调用计算机申请内存空间把“上”这个字符存入到计算机内存中保存的方式自然还是二进制但是应该用什么字符编码保存呢?# coding:gbk x 上涉及到Python中的数据类型这是龟叔写的解释器自然是他说了算。Python2中的字符串分为两种类型 1 str 2 unicode在Python2中如果指定字符编码那么字符串的保存会按照你指定的字符编码来完成正如上面的代码我们使用GBK编码来指定那么在内存中就会用两个字节来保存一个汉字接下来我们就来验证一下如果我们直接打印的这个x的话从理论上讲x对应的就是一串二进制数而打印的时候Python2解释器帮你做了一个转化目的是为了让你能够更加直观的看到它的打印结果但是会出现乱码这个乱码问题我们最后再讲我们可以使用以下代码这种形式来进行打印就可以看到还没有转化之前的结果龟叔还没来得及在Python2中的做转化就写了Python3。# coding:gbk #解释器已经切换到Python2 x 上 # print(x) # Python2加括号也能打印 print([x ]) 输出 [\xc9\xcf] 我们从输出结果上来看“\x”代表的是十六进制“c9”和“cf”分别代表两个十六进制位一个十六进制位对应四个二进制位那么打印结果\xc9\xcf就是16个二进制位也就是16bit2bytes这也就证明了GBK编码保存中文用两个字节。接下来用同样的方式我们再来验证一下UTF-8保存常见汉字用3个字节代码如下。# coding:utf-8 #解释器已经切换到Python2 x 上 # print(x) # Python2加括号也能打印 print([x ]) 输出 [\xe4\xb8\x8a] 3、字符编码的转换通过前面的讲解我们已经清楚了unicode编码可以转换成GBK或者UTF-8相反也可以转换他们之间的转换过程如下unicode 编码encodeGBK/UTF-8 GBK/UTF-8 解码decodeunicode所以必然会有以下代码的执行# coding:utf-8 #解释器已经切换到Python2 x 上 # print(x.decode(gbk)) #使用utf-8编码使用gbk无法解码 print(x.decode(utf-8)) #使用utf-8编码使用utf-8解码 print([x.decode(utf-8),1) #在列表打印出龟叔没有转换之前的unicode编码 输出 上 [u\u4e0a # \u 代表unicode 我们来比较一下unicode字符编码表中“上”这个字符下图是unicode字符编码表中部分内容左侧4开头的如“4E05”就是unicode的十六进制表示形式很明显通过对比我们可以找到字符“上”对应unicode的十六进制就是“4e0a”。在unicode中你可以看到有6个像“上”一样的字符第一个指的是简体中文第二个指的是香港繁体字第三个是台湾繁体字第四个是日本字这些字符都是看起来类似的所以在unicode中统一都用“4e0a”来存储但是每一个不同编码的字符为了区分又添加了类似“494F”这样的标识unicode编码也是通过这种方式来完成与万国编码的转换过程。4、Python2两种字符串类型的区别# coding:gbk #解释器已经切换到Python2 x 上 print(type (x)) # str类型 print([x]) #以gbk编码保存 print([x.decode(gbk), ]) #解码后就是unicode与下面保存的字符编码一致 y u上 #定义字符串的时候前面加u print(type(y)) # unicode类型 print([y, ]) #以unicode编码保存5、字符编码的保存与取值原理我们以GBK编码为例保存一个字符“上”那么保存的结果应该是与上图unicode编码中对应的GBK编码“494F”相对应但是请看如下代码打印结果并不对应这又是为何呢# coding:gbk #解释器已经切换到Python2 x 上 print([xl) #以gbk编码保存 输出 [\xc9\xcf] GBK可以保存中文或者英文字符假如我们要保存一串字符“你a好”应该是使用8bit8bit8bit8bit8bit一共40个比特位来保存。这样保存的时候没有问题但是取值却成了问题计算机并不知道从那一个比特位开始到哪一个比特位结束是第一个字符所以这样连在一起无法取值韩愈 《师说》中写到“句读之不知”也是一样的道理这句话指的是不理解标点符号那么自然不理解句子的开始和结束也就无法读懂一句话。计算机在取值的时候也一定要有一个明确的开始和结束所以其实计算机在保存的时候并不是8个比特位都用来表示字符的这也是GBK表面上是保存65535个汉字其实并没有那么多它只能保存3万多个。GBK能表示两种字符分别是中文和英文其实它的第一个字符是用来区分中文和英文的所以要保存一个“你a好”字符串应该是如下保存方式(17bit)(17bit)(17bit)(17bit)(17bit)假如第一个比特位是1代表中文字符那么接下来他就会先读第一个字节第一个比特位代表中文接下来把这个字节读完了之后需要再一次读取第二个字节的第一个比特位刚好第一个比特位也是1那么他就把后面的7个比特位读完了就能准确地读出这个中字符同理如果一个字符的第二个比特位是0那么它代表一个英文字符那么它只需要把这个字节的内容读完就能准确地读出这个英文字符。计算机原本都是机器它之所以智能都是源自于人类的智慧。如果你使用GBK编码保存而是UTF-8编码去读取由于两种编码每个字符所占用的字节数不同那么关于每个字符的第一个字节的第一个比特位的标识也就不一致就可能会产生第一个字符没有读完而后面的比特位标识与前面刚读过的字符标识不一致那么每个字符都不能准确的读出来这就会产生乱码。既然已经清楚了它的存取原理那么现在我们再来看一下GBK编码保存一个中文字符“上”的过程如下图所示。这个过程主要分为四个阶段1.先“上”这个字符转化十六进制分别是“c”“9”“c”2.把“c”“9”“c”“f”分别转化成二进制对应的01010这些东西3.取出每个字节的第一个二进制位4.把剩余的每个十六进制位对应的多个二进制位再次转化成十六进制所以你看到的结果和unicode字符编码表无法对应是由于计算机的存取原理所导致的。6、字符编码总结最后再回到第二小节直接打印×会出现乱码的问题龟叔在转化的时候按照你在内存保存的时候的字符编码GBK来转化但是Pycharm默认使用的打印到屏幕的字符编码是UTF-8这个可以改但是不建议你修改所以直接在Pycharm打印x你会看到乱码如果是Windows系统的用户可以在Windows终端以Python2执行代码他就不会出现乱码因为Windows平台默认打印到终端的编码也是GBK如果是MacOS系统的用户在终端以Python2执行代码还是会出现乱码因为MacOS系统默认打印到到终端的编码是和Pycharm一样的UTF-8。所以很多写Python2程序的人都会在str前面加一个“u”可能他们自己也不知道这是为什么这就是希望能够把所有的字符串按照unicode字符编码来保存这样可以和任意编码转换。有的人可能会有疑问Python为什么要来两种字符串类型这不是给使用者徒增麻烦吗?你能想到的龟叔自然也能想到这里有一点关于时间先后的问题。Python语言写于1989年1991年Python2才正式诞生unicode是1990年开始研发1994年才真正诞生UTF-8的诞生就更加的晚了所以最开始的Python解释器一定是使用ASClII编码那时候还没有unicode字符编码。我们花费了大量的篇幅讲解了字符编码这里面有一定的理论知识但需要你记住的结论非常少其他的深入的东西没有人会问你详细知道的也没几个人在Python3中所有的字符串都是用unicode编码来保存不需要前面加“u”字符串的数据类型也只有一个就是str只要是用unicode来保存的那么所有的字符串在任何情况下都不会出现乱码在Python3中代码示例如下# coding:gbk #解释器已经切换到Python3 x 上 print(x) # unicode编码encodegbk code_gbk x.encode(gbk) code_utf8 x.encode(lutf-8) print(code_gbk) print(code_utf8) print(type (code_gbk) ) print(type (code_utf8) ) print(code_gbk.decode(gbk) ) print(code_utf8.decode(utf-8)) 输出 上 b\xc9\xcf b\xe4\xb8\x8a class bytes class bytes 上 上 从Python3中打印x.encode(gbk的结果中你可以看到是“\xc9\xcf’这正是Python2中的str类型的值,而在Python3是bytes类型在Python2中则是str类型他们数据类型虽然不一致但是存储的结果确是一致的他们他们之间必然存在一种关联就是Python2中的str类型就是Python3的bytes类型我们可以查看Python2的str类型的源码看到部分代码如下图所示编码之后的结果数据类型是bytes看起来像是字节其实是十六进制计算机会自动把十六进制转化成二进制你可以把bytes类型等同于二进制去看待网络传输也是基于二进制来传输的你在上网的过程中网速很慢的时候可以看到1b/s这就是指的每秒传输一个字节

更多文章