当前位置: 首页 > 后端技术 > Python

一篇文章把文字编码的那些事搞清楚

时间:2023-03-25 19:59:18 Python

长期以来,编码问题如鬼似鬼,让不少开发者深受其害。想象一下,你请求一条数据,得到的却是一堆乱码,张儿和尚想不通。有同事质疑你的数据是乱码。虽然你确定你通过了UTF-8,但是你仍然不能证明你的清白,更不能帮助你的同事调试。有时候,依靠百度和盲调的技巧,乱码也是可以解决的。即便如此,我还是很羡慕那些铁杆程序员。为什么他们每次都能一针见血地指出问题并迅速解决?原因是他们已经弄清楚了编码问题背后的所有来龙去脉。本文从ASCII代码开始,带您了解代码背后的内容。相信当你了解了编码的原理之后,你就不会再害怕任何编码问题了。说起ASCII码,现代计算机技术从英语国家兴起,首先遇到的就是英文文本。英文文本一般由26个字母、10个数字和若干个符号组成,总数只有100个左右。计算机中最基本的存储单位是字节(byte),它由8位(bit)组成,也称为八位位组(octet)。8位可以表示$2^8=256$个字符。好像用bytes来存英文字符就够了?计算机先驱也是如此。它们给每个英文字符编号,再加上一些控制字符,就形成了我们熟悉的ASCII码表。其实因为英文字符不多,所以他们只用了字节的后7位。根据ASCII码表,由01000001的8位组成的八位字节表示字母A。顺便说一句,位本身没有意义,位构成上下文中的信息。比如内存中的一个字节01000001,如果你把它当成一个整数,就是65;如果把它当成英文字符,就是字母A;你对待位的方式就是所谓的上下文。那么,猜猜下面的程序输出什么?#includeintmain(intargc,char*argv[]){charvalue=0x41;//作为数字,值为65或十六进制的0x41printf("%d\n",value);//作为ASCII字符,值是字母Aprintf("%c\n",value);return0;}latin1西欧人来了,主要使用拉丁字母语言。与英语类似,拉丁字母的数量也不多,大概有几十个。于是,西欧人萌生了ASCII码表未使用位(b8)的想法。你是否记得?ASCII码表一共定义了128个字符,范围从0到127,最高字节b8暂未使用。因此,西欧人将拉丁字母和一些辅助符号(如欧元符号)定义在128到255之间。这就构成了latin1,它是一个8位字符集,定义了如下字符:图中绿色部分是不可打印(unprintable)的控制字符,左半部分是ASCII码。所以,latin1字符集是ASCII码的超集:一个字节断成两半,欧美的兄弟二人各用一半。至此,欧美人玩得开心,那东亚人呢?由于中国文化对GB2312、GBK、GB18030的影响,东亚以汉字为主,所以我们以中文为例进行介绍。汉字有什么特点?——光是常用的汉字就有几千个,一个字节不够用。如果一个字节不够,就两个。虽然道理是这样,但操作起来未必那么简单。首先将需要编码的汉字和ASCII码组织成一个字符集,比如GB2312。为什么需要ASCII码?因为,在计算机世界里,不可避免地要和数字、英文字母打交道。至于拉丁字母,重要性就没那么大了,无所谓。GB2312字符集总共包含了6000多个汉字,两个字节就足以表示它们,但事情远没有那么简单。同样的数字字符在GB2312中占2个字节,在ASCII中占1个字节,这不是不兼容吗?电脑里涉及ASCII码的东西太多了,看一个http请求:GET/HTTP/1.1Host:www.example.com那么,如何兼容GB2312和ASCII码呢?没有无双的路,变长编码方案应运而生。变长编码方案,字符用不同长度的字节表示,有的字符只需要1个字节,有的需要2个字节,有的甚至需要更多字节。GB2312中的ASCII码和原来一样,仍然用一个字节表示,解决了兼容性问题。在GB2312中,如果一个字节的最高位b8为0,则该字节为单字节码,即ASCII码。如果该字节的最高位b8为1,则为双字节编码的第一个字节,与后面的字节一起表示一个字符。变长编码方案的目的是为了兼容ASCII码,但也带来了一个问题:由于字节编码长度不同,定位到第N个字符只能通过遍历来实现,时间复杂度退化为$O(1)$到$O(N)$。好在这种操作场景很少见,所以影响可以忽略不计。GB2312收录的汉字数量只有常用的6000多个,遇到生僻字还是束手无策。所以后来又推出了GBK和GB18030字符集。GBK是GB2312的超集,完全兼容GB2312;而GB18030是GBK的超集,完全兼容GBK。因此,要解码中文编码文本,指定GB18030是最稳健的:>>>raw=b'\xfd\x88\xb5\xc4\xb4\xab\xc8\xcb'>>>raw.decode('gb18030')《龙的传人》指定GBK或GB2312只是运气问题,GBK大概没问题:>>>raw.decode('gbk')'龙的传人'GB2312经常没商量就崩溃了:>>>raw.decode('gb2312')Traceback(最近调用last):文件“”,第1行,在UnicodeDecodeError:'gb2312'编解码器无法解码位置0中的字节0xfd:非法多字节sequencechardet是一个nicetextencoding检测库用起来很方便,但是对中文编码的支持不是很好。经常输入中文编码的文本,检测出来的结果是GB2312,但是一用GB2312解码就跪了:>>>importchardet>>>raw=b'\xd6\xd0\xb9\xfa\xc8\xcb\xca\xc7\xfd\x88\xb5\xc4\xb4\xab\xc8\xcb'>>>chardet.detect(raw){'encoding':'GB2312','confidence':0.99,'language':'Chinese'}>>>raw.decode('GB2312')Traceback(mostrecentcalllast):File"",line1,inUnicodeDecodeError:'gb2312'codeccan'tdecodebyte0xfdin位置8:非法多字节序列掌握了GB2312、GBK和GB18030的关系后,我们可以做一个小计算。如果chardet检测到结果是GB2312,那就用GB18030解码,大概率成功!>>>raw.decode('GB18030')'中国人是龙的传人'UnicodeGB2312、GBK、GB18030都是中文编码字符集。GB18030虽然也包括日韩表意字符,算是国际字符集,但毕竟以中文为主,无法适应全球应用。在计算机发展的早期,不同的国家推出了自己的字符集和编码方案,彼此之间互不兼容。中文编码的文字无法在日文编码的系统上显示,给国际交流带来障碍。这时,英雄出现了。UnicodeConsortium站出来说要开发一个通用的字符集,包括世界上所有的字符。这是统一码。经过多年的发展,Unicode已经成为世界上最通用的字符集和计算机科学领域的行业标准。Unicode已经收录了13万多个字符,每个字符占用2个多字节。由于常见的编程语言一般没有24位的数字类型,所以一个字符一般用32位的数字来表示。这样,同一个英文字母在ASCII中只需要占用1个字节,而在Unicode中却需要占用4个字节!英美人民都快哭了,想象一下当你的磁盘上的文件大小增加了4倍时是什么感觉!为了兼容ASCII,优化UTF-8的文本空间占用,我们需要一种变长字节编码方案,这就是大名鼎鼎的UTF-8。与GB2312等中文编码一样,UTF-8使用不固定的字节数来表示字符:ASCII字符的Unicode码位从U+0000到U+007F,用1个字节编码,最高位为0;码点表示为字符从U+0080到U+07FF用2个字节编码,第一个字节以110开头,其余字节以10开头;码点从U+0800到U+FFFF的字符用3个字节编码,第一个字节以1110开头,其余字节也以10开头;4到6字节编码的情况类似;如图,0开头的字节为单字节编码,共有7个有效编码位。编码范围为U+0000到U+007F,刚好对应ASCII码中的所有字符。110开头的字节为双字节编码,共有11个有效编码位,最大值为0x7FF,所以编码范围为U+0080到U+07FF;以1110开头的字节为三字节编码,共16个有效编码位,最大值为0xFFFF,所以编码范围为U+0800到U+FFFF。根据开头的不同,UTF-8流中的字节可以分为以下几类:最高有效字节类别有效位0单字节编码710多字节编码非首字节110双字节编码首字节111110三字节编码的首字节为1611110四字节编码的首字节为21111110五字节编码的首字节为261111110六字节编码的首字节为31至此,我们有读取UTF-8编码字节流的能力,如果你不相信我,让我们来看一个例子:概念回顾很长一段时间以来,字符集和编码这两个词一直被交替使用。现在,我们终于可以弄清楚两者的关系了。字符集,顾名思义,就是一定数量的字符的集合,每个字符在集合中都有唯一的编号。上面提到的ASCII、latin1、GB2312、GBK、GB18030、Unicode无一例外都是字符集。计算机存储和网络通信的基本单位是字节,因此文本必须以字节序列的形式存储或传输。那么,字符数如何转换成字节呢?这就是编码要回答的问题。在ASCII和拉丁文中,字符编号和字节是一一对应的,是一种编码方式。GB2312使用变长字节,这是另一种编码方式。而Unicode有多种编码方式,除了最常用的UTF-8编码外,还有UTF-16等。事实上,UTF-16的编码效率比UTF-8高,但由于不兼容ASCII,其应用范围受到很大限制。最佳实践既然我们了解了文本编码的前世今生,那么如何避免编码问题呢?是否有一些最佳实践?答案是肯定的。在开始编码选择项目之前,需要选择一种适应性广的编码方案。UTF-8是首选,优点很多:Unicode是编码字符数量最多的行业标准,天然支持国际化;UTF-8完全兼容ASCII码,是一种刚性索引;UTF-8是目前使用最广泛的;如果由于历史原因不得不使用中文编码方案,则首选GB18030。该标准最新、涵盖的字符最多、适应性最强。尽量避免使用GBK,尤其是GB2312等旧编码标准。编程习惯如果你使用的编程语言,字符串类型支持Unicode,那么问题就简单了。由于Unicode字符串肯定不会出现乱码等编码问题,所以只需要注意输入输出环节即可。例如,从Python3开始,str是一个Unicode字符串,bytes是一个字节序列。因此,在Python3程序中,核心逻辑应该统一使用str类型,避免使用bytes。文本编码和解码操作统一在程序的输入层和输出层进行。假设你正在开发一个API服务,数据库数据编码是GBK,但是用户使用的是UTF-8编码。然后在程序输入层,从数据库中读取GBK数据后,解码并转换成Unicode数据,然后进入核心层进行处理。在程序的核心层,数据以Unicode进行处理。由于核心层的处理逻辑可能比较复杂,统一采用Unicode可以减少问题的发生。最后,数据在程序的输出层以UTF-8编码返回给客户端。整个过程的伪代码大致如下:#input#从数据库中读取gbk数据并解码为unicodedata=read_from_database().decode('gbk')#core#processunicodedataonlyresult=process(data)#output#将unicode数据编码成utf8response_to_user(result.encode('utf8'))的程序结构看起来像一个三明治,很形象:当然,现在还有很多编程语言字符串还不支持Unicode。Python2中的str对象,就像Python3中的字节一样,只是字节序列;C中的字符串甚至更原始。没关系,良好的编程习惯是相通的:程序核心层统一使用某种编码,输入输出层负责编码转换。至于核心层使用的编码,主要看程序中使用哪种编码最多,一般应该和数据库编码保持一致。附录更多Python技术文章,请查看:Python语言小册子,前往原文以获得最佳阅读体验。订阅更新,获取更多学习资料,请关注我们的微信公众号: