在开发过程中,字符编码始终是程序猿和程序媛们绕不开的一个话题。这里针对题主的问题和建议,简要整理下有关字符编码的知识,供列位看官茶余饭后消遣:)
本回答尽量直观地介绍相关概念,不纠缠相关规定的细节,以使读者能对字符编码有着更直观的理解。当然,这样很容易挂一漏万,难免出现纰漏,也望各位批评指正。
众所周知,在计算机的世界,所有的信息都是0/1组合的二进制序列,计算机是无法直接识别和存储字符的。因此,字符必须经过编码才能被计算机处理。字符编码是计算机技术的基础,也是程序猿/媛需要的基本功之一。
在展开具体介绍之前,首先要强调一下几个概念,这对理解字符编码非常重要,也将贯穿在后文的介绍之中:
1)
理解字符集和字符编码的区别
2)
“系统/终端/文件/程序”不同概念上的编码
3)
常见操作系统、文本编辑器对字符编码的处理
1 、字符集与字符编码
如前所述,字符只有按照一定规则编码,最终表示为0/1二进制序列的形式,才能被计算机处理。那么,怎么定义这种编码映射呢?其实很简单,只要大家都按照相同的规则,规定好字符与二进制序列(表现为某个数值)之间的对应关系即可。比如,我们规定英语大写字母A对应数字65,那么我们只要将65的二进制形式(01000001)保存即可。
那么,问题又来了。如果大家使用的规则不一致怎么办,那岂不全乱套了?这时候,字符集就粉墨登场了。所谓字符集,直观上讲,就是人们统计预先规定好的一系列字符与二进制序列(数字)之间的映射关系。只要大家都遵循这个规则,并且计算机也按照这种方式处理,那么这个世界不就很美好了!然而,全世界的语言实在太多了,由于历史和地域的原因,也就形成了多套应用于不同场合、语言的字符集,如ASCII、GBK、Unicode等。
需要注意的是,我们规定好了字符与数字之间的对应关系,但这并不代表计算机一定要按照字符对应的数字将数字本身直接存储!有时候,我们按照一定的规则,将字符的码元再次处理,以更加适应计算机存储、网络传输的需要。字符编码便是规定了如何编码、存储这些字符对应的二进制序列。
因此,某种意义上,可以理解为,字符集是一种协议,而字符编码是对字符集的一种实现。当然,既然称之为“实现”,也说明对同一种字符集可能有不同的编码方式。可以想到,最直接的编码方式,便是直接使用字符对应的二进制序列,这就导致了字符集和字符编码看起来像是一个东东,长期受此思维影响,可能就会对Unicode和UTF的区别有些困惑。
2 、祖先:ASCII编码
再次回顾,在计算机内部,所有的信息最终都表示为一个二进制的序列。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256(2^8)种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态可以对应一个符号,就是256个符号,从0000_0000到1111_1111。
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定,这就是ASCII(American Standard
Code for Information Interchange,美国信息交换标准代码),一直沿用至今。
ASCII码一共规定了128个字符的编码,包含常见的英语字符和一些控制符号。比如空格(Space)对应32(二进制0010_0000),大写字母A对应65(二进制0100_0001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0,没有使用。
需要强调的是,ASCII便是字符集与字符编码相同的情况,直接将字符对应的8位二进制数作为最终形式存储。因此,当我们提及ASCII,既表示了一种字符集,也代表了一种字符编码,即常说的“ASCII编码”。
ASCII表如下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/8754fa30011bc363094f33913c64958c.png)
图
1 ASCII编码表(摘自百度)
3 、扩展:扩展ASCII
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。同时,1byte中我们不是还有最高位没有使用么?这能够再编码128个符号啊!于是,一些欧洲国家就决定,利用闲置的最高位编入新的符号。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。从128到255这些字符集被称“扩展字符集”。基于此,ISO 组织在ASCII码基础上又制定了一系列标准用来扩展ASCII编码,它们是ISO-8859-1~ISO-8859-15,其中ISO-8859-1(又称Latin-1)涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1仍然是单字节编码,它总共能表示256个字符。
但是,问题又来了。。。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。但是不管怎样,所有这些编码方式中,0-127表示的符号是一样的,不一样的只是多了128-255的这一段,因此它们都向下兼容ASCII编码。
同样,对于扩展ASCII,它也是既表示了字符集,也代表一种字符编码。
4 、中文:GB系列
字符型语言的字符数量较少,因此用一个byte(8bit)基本就够用了,这就难为了我们博大精深的中文汉字——中文常用字就有好几千呢!但这也难不倒我们勤劳勇敢的中国人,为此,我们设计了GB2312字符集,意气风发走进那新时代。
GB2312的思想其实很简单——既然1个byte不够用,那咱们用2个呀!正所谓,“没有1byte解决不了的问题,如果有,就2byte”。理论上,2个字节便可以表示2^16=65536的字符。不过,GB2312最初被设计时,只规定了中文常见字,很多特殊字符还没有包含。GB2312一共收录了7445个字符,包括6763个汉字和682个其它符号。除了GB2312,还有用于中文繁体的Big5。
人们逐渐发现,GB2312规定的字符太少,甚至有些国家领导人名字中的汉字都表示不出来,这还了得!于是1995年的汉字扩展规范GBK1.0(《汉字内码扩展规范》)收录了21886个符号,它分为汉字区和图形符号区。GBK编码是GB2312编码的超集,向下完全兼容GB2312,同时GBK收录了Unicode基本多文种平面中的所有CJK汉字。GBK还收录了GB2312不包含的汉字部首符号、竖排标点符号等字符。
2000年的GB18030是取代GBK1.0的正式国家标准。GB18030编码向下兼容GBK和GB2312。
GB18030收录了所有Unicode3.1中的字符,包括中国少数民族字符。GB18030虽然是国家标准,但是实际应用系统中使用的并不广泛。目前使用最广的仍是GBK编码。
GBK和GB2312都是双字节等宽编码,如果算上为与ASCII兼容所支持的单字节,也可以理解为是单字节和双字节混合的变长编码。GB18030编码是变长编码,采用单字节、双字节和4字节方案,其中单字节、双字节和GBK是完全兼容的,4字节编码的码位就是收录了CJK扩展A的6582个汉字。
从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理,区分中文编码的方法是高字节的最高位不为0。GB2312、GBK都属于双字节字符集 (DBCS)。
最后,不严格的说,GB系列编码也可以认为是具有字符集的意义,又有字符编码的意义。实际上,GB系列编码中有“区位码”的概念(由于采用两个数来编码汉字和中文符号。第一个数称为“区”,第二个数称为“位”,所以也称为区位码),实际上, 区位码更应该认为是字符集的定义,定义了所收录的字符和字符位置,而GB2312是实际计算机环境中支持这种字符集的编码。区位码和GB2312编码的关系有点像 Unicode和UTF-8,正是最开始介绍的字符集与字符编码的区分。
5、 统一:Unicode
介绍完ASCII、Latin和GBK,想必各位会有这样的想法:为什么不规定一种字符集、编码方式,直接能够囊括全世界所有语言文字的符号呢?
由于科技发展不同步等历史原因,多种编码方式并存是不可避免的现象,但大一统的工作实际上人们也在做,这就是大名鼎鼎的Unicode。
这里面也有段有趣的历史。实际上,最早是由两个独立的组织都在设计一种通用字符集,但两家很快意识到,这个世界并不需要两套不同的通用字符集存在,否则所谓的“通用”便失去了意义。于是,后期的Unicode工作被有组织的统一了起来。
Unicode是为了整合全世界的所有语言文字而诞生的,全称是Universal
Multiple-Octet Coded Character Set,它所规定的字符集也被称为Universal Character Set (UCS)。
再次提醒,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。也即,UCS规定了怎么用多个字节表示各种文字,而怎样存储、传输这些编码,则是由UTF (UCS
Transformation Format)规范规定的。UTF会在后文介绍。
5.1 UCS
UCS有两种不同的规定版本:UCS-2和UCS-4。顾名思义,UCS-2就是用2个字节编码,UCS-4就是用4个字节(实际上只用了31位,最高位必须为0)编码。因此,UCS-2有216=65536个码位,UCS-4有231=2147483648个码位。目前UCS-2已经足够用了,UCS-4估计都可以把Asgard和氪星的文字(传说都是英语?)也收录进来了……具体的符号对应表,可以查询http://unicode.org,或者相关的字符对应表。
UCS-4根据最高位为0的最高字节分成=128个Group,每个Group再根据次高字节分为256个Plane,每个Plane根据第3个字节分为256行 (Rows),每行包含256个Cells。同一行的Cells只是最后一个字节不同,其余都相同。
![](https://i-blog.csdnimg.cn/blog_migrate/920e3f6917c5cc804f2e38c1d4aa722f.png)
其中,Group 0的Plane 0被称作Basic Multilingual
Plane,即BMP,或者说UCS-4中,高两个字节为0的码位被称作BMP。将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2。在UCS-2的两个字节前加上两个零字节,就得到了UCS-4的BMP。而目前的UCS-4规范中还没有任何字符被分配在BMP之外。
那么,新的问题又来了:
1)
如何才能区别Unicode和ASCII?计算机怎么知道2个字节表示1个符号,而不是分别表示2个符号呢?
2)
我们已经知道,英文字母只用1个字节表示就够了(ASCII),如果Unicode统一规定,每个符号用2个或4个字节表示,那么每个英文字母前都必然有许多字节是0,这对于存储来说是极大的浪费,是无法接受的。
这些问题造成的结果是:
1)
出现了Unicode的多种存储方式,也就是说有许多种不同的格式,可以用来表示Unicode。
2)
Unicode在很长一段时间内无法推广,直到互联网(UTF-8)的出现。
5.2 UTF
UTF(Unicode/UCS
Transformation Format),即Unicode字符集的编码标准,可以理解为对Unicode字符集的具体使用/实现方式,主要有UTF-16、UTF-32和UTF-8。
UTF-16
UTF-16由RFC2781协议规定,它使用2个字节来表示1个字符。不难猜到,UTF-16是完全对应于UCS-2的(实际上稍有区别),即把UCS-2规定的代码点通过Big Endian(下文介绍)或Little Endian方式直接保存下来。UTF-16包括三种:UTF-16,UTF-16BE(Big Endian),UTF-16LE(Little Endian)。
UTF-16BE和UTF-16LE不难理解,而UTF-16就需要通过在文件开头以名为BOM(Byte Order Mark,下文介绍)的字符来表明文件是Big Endian还是Little Endian。BOM为\uFEFF这个字符。其实BOM是个小聪明的想法。由于UCS-2没有定义\uFFFE,因此只要出现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是\uFEFF,并且据此判断出是Big Endian还是Little Endian。
例:“ABC”这三个字符用各种方式编码后的结果如下:
![](https://i-blog.csdnimg.cn/blog_migrate/df0f789c080883820f0465c0e360b547.png)
另外,UTF-16还能表示一部分的UCS-4字符——\u10000~\u10FFFF,表示算法就不再详细介绍了。
总结来说,UTF-16以2、4字节存储一个Unicode编码,对于小于0x10000的UCS码,UTF-16编码就等于UCS码对应的16位无符号整数。对于不小于0x10000的UCS码,定义了一个算法。不过由于实际使用的UCS2,或者UCS4的BMP必然小于0x10000,所以就目前而言,可以认为UTF-16和UCS-2基本相同(前提是明白UTF和UCS的差别)。
UTF-32
UTF-32用4个字节表示字符,这样就可以完全表示UCS-4的所有字符,而无需像UTF-16那样使用复杂的算法。与UTF-16类似,UTF-32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样需要BOM字符。仍以“ABC”为例:
![](https://i-blog.csdnimg.cn/blog_migrate/f3e14149cfaf04b02e8d7559d7e178b1.png)
UTF-16和UTF-32的一个缺点就是它们固定使用2个或3个字节,这样在表示纯ASCII文件时会有很多零字节,造成浪费。RFC3629定义的UTF-8则解决了这个问题,下面介绍UTF-8。
UTF-8
随着互联网的普及,强烈要求出现一种统一的编码方式,UTF-8就是在互联网上使用最广的一种Unicode的实现(传输)方式。
UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1-4个字节表示一个符号,根据不同的符号而变化字节长度。为什么采用边长的机制,实际上和字符出现的概率分布有关,其中蕴含着Huffman编码的思想——最常出现的字符编码尽量的短。
UTF-8的编码规则很简单,如下:
1) 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2) 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。
下表总结了编码规则,字母x表示可用编码的位。
可见,ASCII字符(\u0000~\u007F)部分完全使用一个字节,避免了存储空间的浪费,而且UTF-8可以不再需要BOM字节。
另外,从上表中可以看出,单字节编码的第一字节为00-7F,双字节编码的第一字节为C2-DF,三字节编码的第一字节为E0-EF,这样只要看到第一个字节的范围就可以知道编码的字节数,可以大大简化算法(感兴趣的话可以自己实现编码、解码的算法)。
例:以汉字“严”为例。
已知“严”的Unicode(UCS-2)码是\u4E25(0100_1110_0010_0101),根据上表,可以发现\u4E25处在第三行的范围内(0000 0800 ~ 0000 FFFF),因此“严”的UTF-8编码需要3个字节,即格式是“1110xxxx 10xxxxxx
10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“1110010010111000
10100101”,转换成十六进制就是E4 B8 A5。
可以看到“严”的Unicode码是\u4E25,而UTF-8编码是E4 B8 A5,两者是不一样的。它们之间的转换可以通过程序或一些编辑器实现。
5.3 大端、小端与字节序标记
UTF-8以单字节为编码单元,没有字节序的问题。UTF-16以2个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚编码单元的字节序。例如,当我们收到6A7C时,它到底代表\u6A7C还是\u7C6A?
仍以汉字“严”为例,Unicode(UCS-2)码是\u4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是Big endian方式;25在前,4E在后,就是Little endian方式。
因此,第一个字节在前,就是“大头/大端方式”(Big endian),第二个字节在前就是“小头/小端方式”(Little endian)。那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?
Unicode编码规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做“零宽度非换行空格”(Zero Width No-Break
Space),它的编码是FEFF。其实FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中,UCS规范建议我们在传输字节流前,先传输字符"Zero Width
No-Break Space",这正好是2个字节,而且FF比FE大1。如果一个文本文件的头2个字节是FE FF,就表示该文件采用大头方式;如果头2个字节是FF FE,就表示该文件采用小头方式。字符"Zero Width
No-Break Space"又被称作BOM(Byte Order Mark,字节序标记)。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"Zero Width No-Break
Space"的UTF-8编码是EF BB BF,所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。Windows就是使用BOM来标记文本文件的编码方式的,但实际上并不建议UTF-8格式的文件使用BOM。
6 、各种环境下的编码
简单介绍完字符编码相关知识后,我们需要理解不同环境下的字符编码问题,这对程序猿/媛们来说更为重要。谈及具体环境下的编码,我们实际上有以下四个层次:
1)
操作系统默认编码方式。这是操作系统的内部属性,比如大多Linux系统、Mac OS默认UTF-8编码,中文版Windows系统默认GBK编码
2)
终端编码方式。终端包括cmd、shell、terminal等,在与终端交互时,字符是要在终端显示的,这必然涉及到终端采用的编码方式,事实上有不少bug是在这个层面产生的。对于单机系统而言,终端编码与操作系统编码一般是一致的,但在远程登录时,可能会遇到一些问题。
3)
文本文件的编码方式。这是我们接触最多的概念,即一个文本文件(如源代码文件)是以什么编码格式保存的。大多数编辑器可以显示文本的编码格式,以及更改编码方式重新存储。以下图中的Notepad++编辑器为例,在格式选项卡中显示了可以进行编码转换的选项。
4)
程序中的字符、字符串变量的编码方式。这与具体的编程语言相关,涉及到程序运行时变量在内存中的状态。Python典型的encode/decode就是这个鬼。
![](https://i-blog.csdnimg.cn/blog_migrate/67a6f978d1125ef1cdd6534bd339ea4b.png)
图
2 Notepad++中的编码选项
以Java和Python编程语言为例。
Java和Python3里,字符均采用Unicode编码(Java.lang.String 采用 UTF-16 编码方式存储所有字符),因此可以很好地支持中文。但是,Python2中Unicode不是字符默认编码格式(Python从2.2才开始支持Unicode),因此需要进行编码的转换。函数decode( char_set )可以实现其它编码到Unicode的转换,函数encode( char_set )可以实现Unicode到其它编码方式的转换,这里所讲的Unicode String是指 UCS-2或者UCS-4 编码的Code Points。
比如, (“你好”).decode(
“GB2312”) 将得到 u’\u4f60\u597d’,即“你”和“好"的 Unicode 码分别是0x4f60 和 0x597d,再用
(u’\u4f60\u597d’).encode(“UTF-8”) 就可以得到“你好”的UTF-8编码结果:’\xe4\xbd\xa0\xe5\xa5\xbd’。【请注意,这些运行结果跟具体的操作平台有关,读者可以思考为什么,相关解答可以参考知乎(Python2.7 中文字符编码,使用Unicode时,选择什么编码格式? - Kenneth 的回答)对此的解释,本小节也是受到该回答的启发整理而成】
![](https://i-blog.csdnimg.cn/blog_migrate/26f16b928598ae3d4b422381dc6641b3.png)
图 3 Unicode decode/encode
in Python (摘自Nature Language Processing
with Python, 2 edition)
实际上,Python 3和2很大区别就是Python本身改为默认用Unicode编码。字符串不再区分"abc"和u"abc",字符串"abc"默认就是Unicode,不再代表本地编码,由于有这种内部编码,与C#和Java类似,没有必要在语言环境内做类似设置编码,也因此Python 3的代码和包管理上打破了和2.x的兼容。
再次提醒:只有字符到字节或者字节到字符的转换才存在编码转码的概念。
7、 编码总结
总结一下上述内容:
1、编码界最初只有ASCII码,只用了1byte中的7bit(0~127);
2、欧洲人发现128个不够了,就把1byte中没用的最高位给用上了,出现了Latin系列(ISO-8859系列)编码;
3、中国人民通过对ASCII编码进行中文扩充改造,产生了GB2312编码,可以表示6000多个常用汉字;
4、汉字实在太多了,还有繁体、各种字符呀,于是加以扩展,有了GBK;
5、GBK还不够,少数民族的字还木有呀,于是GBK又扩展为GB18030;
6、每个国家、语言都有自己的编码,彼此无法交流,迫切需要大一统局面的出现;
7、Unicode诞生,可以容纳全世界的任何文字。Unicode分为UCS-2和UCS-4,分别是2字节和4字节,实际2字节就够用了;
8、为了Unicode能实际应用(存储、传输),制定了Unicode的编码方式,即UTF,有UTF-8、UTF-16、UTF-32,其中UTF-8应用广泛;
9、UTF-16、UTF-32均是多字节传输,存在字节顺序的问题,于是有了大头还是小头的概念,为了解决这个问题,引入了BOM。UTF-8是单字节传输,不存在这个问题,也就不需要BOM,但可以有,仅用来表明编码格式;
10、要从“环境/终端/文本/程序”等不同层次去理解编码,并尝试解决遇到的问题;
8 、最后的叨叨念
各种编码方式的不一致,环境平台的不一致,会导致在编程中遇到各种头疼的bug。如果对这些相关的概念有一定的了解,至少在解决问题时会从容很多。
为了最大程度上减少可能产生的问题,可以在平时多注意一些细节,一方面方便个人开发,另一方面减少对二次开发用户的干扰。比如:
1)
可以的话,文本尽量以UTF-8(无BOM)方式存储。对于Windows用户,强烈建议弃用默认的文本编辑器,采用一款更专业的编辑器,并将默认编码格式设置为UTF8(无BOM)。
2)
跨平台开发时,如果需要在terminal输出,注意terminal的编码方式,有时采用重定向到文件会是一个更加通用的选择。
3)
对于Python用户,如果不是相关依赖项只有py2版本,强烈建议采用py3开发,会减少很多中文相关的问题。
4)
路径、文件、变量等按照规范命名。比如,听说Java支持用中文命名变量了,因此可能会出现这样的代码,String 哈哈 = “2333333”; 为了减少后续可能遇到的问题,奉劝还是不要这样,不做死就不会死。
跋 · 示儿
死去元知字符崩,但悲不见编码同。
Unicode一统日,家祭无忘告乃翁。
参考资料
[1] Unicode和UTF-8的关系
(Unicode和UTF-8的关系)
[2]
编码格式简介(ANSI、GBK、GB2312、UTF-8、GB18030和 UNICODE) (编码格式简介(ANSI、GBK、GB2312、UTF-8、GB18030和 UNICODE))
[3]
UNICODE,GBK,UTF-8区别 (UNICODE,GBK,UTF-8区别)
[4]
谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词-CSDN论坛-CSDN.NET-中国最大的IT技术社区 (谈谈Unicode编码,简要解释UCS、UTF、BMP、BOM等名词)
[5]
常用字符集编码详解:ASCII 、GB2312、GBK、GB18030、UTF-8、unicode (常用字符集编码详解:ASCII 、GB2312、GBK、GB18030、UTF-8、unicode)
[6]
Python的编码注释# -- coding:utf-8 -- (Python的编码注释# -- coding:utf-8 --)
[7]
Java几种常见的编码格式 (JAVA几种常见的编码格式)
[8]
python3.3中文乱码与默认编码格式设定。
(python3.3中文乱码与默认编码格式设定)
[9]
字符集与编码的故事 (字符集与编码的故事)
转载:https://www.zhihu.com/question/20152853/answer/95576659