前言
中文编程界的汉字编码问题,每一个小白的血泪史,每一个小白在处理中文文件时,都无数次想,如果这是一份纯英文的文件,可能现在早就下班在家洗澡了。
史前文明-ASCII码
每个程序员都知道,不论是你在手机上看到漂亮小姐姐,还是这篇博文中的文字,在计算机底层存储中都是又一串01编码组成的二进制文件。
虽然对于计算机来说高低电平来控制程序的执行逻辑是很方便的事情,但是人可读不懂复杂冗长的二进制文件,因此就需要制定一种编码规则实现二进制文件与人类文字的相互转换,ACSII码应运而生。
计算机最早是由美国人在使用,于是由他们制定了二进制编码中8bit为一个字节(0~255),划定了0-32的控制码、26个大小写英文字母以及若干个英文符号的二进制编码,将一个字节使用到了127号,因此在ASCII码中一个字节就是一个英文字母,很好计算。美国人将这种编码方式命名为 ANSI(American National Standards Institute,美国国家标准学会) 的ASCII码,全称American Startand Code for Information Interchange,美国信息互换标准码。
随后很多欧洲国家也开始使用计算机,进而占用了一个字节的剩余128~255的位置,称为 ISO 8859-1,International Organization for Standardization(国际标准组织)。
汉字编码的军阀混战-GB2312、GBK、BIG5、JIS
GB2312,中国大陆编码规范
随后计算机逐渐在中国日本等国家流行开来,和之前面临同样的问题,中国也迫切需要一种编码方式将二进制按照某种规则翻译为汉字显示出来。但是和只有26个的英文字母不同的是,中文的每一个字都是一种新的写法,因此只能对每个汉字映射到一个二进制编码。而常用汉字就有6000多个,之前美国人定义的8bit=1字节完全不够用。于是人们想到了对于ASCII码进行扩展,用两个字节表示。并且将这种编码方式命令为GB2312(GB,Guóbiāo,中华人民共和国国家标准,简称国标,即国标2312)。
计算机在使用这种编码时,判断当前字节是否在0~127(ASCII码范围),如果在,则表示当前为单字节的英文字母;否则,会联合后面的一个字节共同判断当前的汉字表示(注意,在GB2312中两个字节都要大于127)。
整个过程用伪代码表示如下:
def decode(stream):
byte the_byte = stream.next_byte()
if 0 <= the_byte and the_byte <= 127:
return decode_english(the_byte) # decode english
byte next_byte = stream.next_byte()
if the_byte > 127 and next_byte > 127:
return decode_chinese(the_byte, next_byte) # decode chinese
else:
return error # decode error.
此外,GB2312中还包含了很多数学符号、罗马希腊的字母、日文的假名,而且还包含了ASCII中定义的一些标点符号,例如本文中的逗号,这些符号都定义了双字节的版本,这种符号被称为全角符号,与之对应的概念即ASCII码中的半角符号。
GBK,GB2312的扩展,简体字常用编码
细心的读者可以发现,在GB2312标准中,只有两个相邻的字节都是128~255的编码值,才会将其解析为汉字,这其实完全没有必要,会浪费很多数字。因为其实只要读到一个字节是ASCII字符集以外的二进制,也就表明了这是一个汉字,其后的下一个字节完全可以取0~255中的任何一个数字,因此 国标2312 后又对其进行了字符集的扩展kuòzhǎn,也就诞生了我们耳熟能详的国标扩,GBK,这种双字节表示一个汉字的方式称为DBCS(Double Byte Charecter Set 双字节字符集)。这时候的解析伪代码如下:
def decode(stream):
byte the_byte = stream.next_byte()
if 0 <= the_byte and the_byte <= 127:
return decode_english(the_byte) # decode english
byte next_byte = stream.next_byte()
return decode_chinese(the_byte, next_byte) # decode chinese
GBK这个编码相信国内的程序员们并不陌生,因为历史原因,很多代码和数据存储依然使用着GBK编码,也就有了国内程序员经常碎碎念的 一个字母一个字节,一个汉字两个字节。
BIG-5,繁体字常用编码规范
台湾海峡隔海相望的另一边,同样使用着汉字的台湾人民也有着一套自己定制的汉字编码规范,称为大五码-BIG5,是繁体中文社区中最常用的编码方式,广泛使用于港澳台地区。
“大五码”(Big5)是由台湾财团法人信息产业策进会为五大中文套装软件所设计的中文共通内码,在1983年12月完成公告,隔年3月,信息产业策进会与台湾13家厂商签定“16位个人电脑套装软件合作开发(BIG-5)项目(五大中文套装软件)”,因为此中文内码是为台湾自行制作开发之“五大中文套装软件”所设计的,所以就称为Big5中文内码。五大中文套装软件虽然并没有如预期的取代国外的套装软件,但随着采用Big5码的国乔中文系统及倚天中文系统先后在台湾市场获得成功,使得Big5码深远地影响繁体中文电脑内码,直至今日。“五大码”的英文名称“Big5”后来被人按英文字序译回中文,以致现在有“五大码”和“大五码”两个中文名称。
说起BIG5编码,笔者就想起小时候刚有家庭PC机的时候,那个时候台湾的单机游戏可以说是百花齐放,当时国内盗版光盘泛滥,笔者当时最爱玩的一款游戏叫做《大富翁》,相信很多玩游戏的读者听到这个名字并不陌生,当时有很多台湾游戏厂商生产的单机游戏是我的童年挚爱。但是唯一的问题就是这该死的编码问题,当时笔者为了玩好某款游戏就不得不装一些字符集插件。否则的话游戏界面就会变成这样。
JIS,日语编码
除了以上两种中文编码方式外,日语中也同样有着汉字,日语的标准编码称为JIS,如JIS X 0201
日本产业标准(日语:日本産業規格/にほんさんぎょうきかく Nihon Sangyō Kikaku ?,英语:Japanese Industrial Standards,缩写JIS),日语又把 JIS 音译为ジス,是由日本产业标准调查会(JISC)组织制定和审议。JIS是日本国家级标准中最重要、最权威的标准。
规范统一 Unicode与UTF-8
从上面对中文编码的归纳中我们可以看出,随着互联网的逐渐发展,不同地区与文化之间的交流只会越来越紧密,不同编码之间的冲突成为了人与人之间自由交流沟通的阻碍。
为了解决不同地区编码的统一问题(尤其是中文编码问题),美国加州成立了一个Unicode组织,对现存的所有字符都进行了编码设置。
编码方式 -- Unicode的通用字符集UCS
通用字符集(英语:Universal Character Set, UCS)
- UCS-2,用2字节表示字符集,目前实际应用的统一码版本,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示2^16(即65536)个字符。基本满足各种语言的使用。
- UCS-4,未来扩展使用的字符集,理论上最多能表示2^31个字符,完全可以涵盖一切语言所用的符号
平面空间(Plane)
因为Unicode编码字符集实在太多了,于是人们将Unicode编码进行了分组,分成了若干个平面(plane),我们日常能接触到的中文、英文、日文、韩文等都在0号平面。可以看到,Unicode当前的所有范围为0-0x10FFFF。
平面 | 始末字符值 | 中文名称 | 英文名称 |
---|---|---|---|
0号平面 | U+0000 - U+FFFF | 基本多文种平面 | Basic Multilingual Plane,简称BMP |
1号平面 | U+10000 - U+1FFFF | 多文种补充平面 | Supplementary Multilingual Plane,简称SMP |
2号平面 | U+20000 - U+2FFFF | 表意文字补充平面 | Supplementary Ideographic Plane,简称SIP |
3号平面 | U+30000 - U+3FFFF | 表意文字第三平面 | Tertiary Ideographic Plane,简称TIP |
4号平面至13号平面 | U+40000 - U+DFFFF | (尚未使用) | |
14号平面 | U+E0000 - U+EFFFF | 特别用途补充平面 | Supplementary Special-purpose Plane,简称SSP |
15号平面 | U+F0000 - U+FFFFF | 保留作为私人使用区(A区)[1] | Private Use Area-A,简称PUA-A |
16号平面 | U+100000 - U+10FFFF | 保留作为私人使用区(B区)[1] | Private Use Area-B,简称PUA-B |
实现方式 -- UTF-8,Unicode的实现方式
Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。
试想,如果所有的符号都采用16号平面的三字节存储,那欧美国家的人肯定不答应,因为他们基本不常用非英文字母,一个字节完全可以存储一个字母,现在改用三字节完全是对存储空间和网络传输的浪费。为了解决这个问题,有了如下几种UTF的转换格式:
UTF-32
Code Point Segment | Encoding(binary) | description |
---|---|---|
0x000000 - 0x10FFFF | xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx | 简单粗暴 |
UTF-32,使用32位即4个字节表示每种字符,例如英文位数不够的就补充0,正如前文所述,这是一种对空间的浪费,一般不会使用。
UTF-16
Code Point Segment | Encoding(binary) | description |
---|---|---|
0x000000 - 0x00FFFF | xxxxxxxx xxxxxxxx | 基本平面编码(英文、日语、汉字、甚至emoji文字也在这里) |
0x010000 - 0x10FFFF | 110110yy yyyyyyyy 110111xx xxxxxxxx | 扩展平面编码(甲骨文) |
UTF-16,用2字节表示基本平面(即包含了英文、中文、日文等的0号平面),用4字节表示扩展平面(一般都是甲骨文等奇葩字符),通过特殊的编码位(称为代理)标识当前这2个字节是一个基本平面的字符还是半个扩展平面的字符,对于中文、英文字符都需要用两个字节来存储,只用英文的情况下存在对空间的浪费。
PS:UCS-2编码一般指的是没有执行扩展平面编码的UTF-16,如果你不是要显示甲骨文什么的,UCS-2=UTF-8
UTF-8
Code Point Segment | Encoding(binary) | description |
---|---|---|
0x000000 - 0x00007F | 0xxxxxxx | 英文 |
0x000080 - 0x0007FF | 110xxxxx 10xxxxxx | |
0x000800 - 0x00FFFF | 1110xxxx 10xxxxxx 10xxxxxx | 中文 |
0x010000 - 0x10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | emoji😀 |
- UTF-8是一种变长的编码,通过对字节的高位01的判断,按照一定的算法转换,使得英文字母还是0~127的1字节,其它字符会使用1-3个字节编码。常用的汉字通常位于 0x000800 - 0x00ffff 这一段,需要三个字节的存储,比起 UTF-16 的存储消耗要大一些。
例如汉字“我”,unicode编码为0x6211,在各种不同的实现方式下编码如下
Encoding | hex | dec (bytes) | dec | binary |
---|---|---|---|---|
UTF-8 | E6 88 91 | 230 136 145 | 15108241 | 11100110 10001000 10010001 |
UTF-16BE | 62 11 | 98 17 | 25105 | 01100010 00010001 |
UTF-16LE | 11 62 | 17 98 | 4450 | 00010001 01100010 |
UTF-32BE | 00 00 62 11 | 0 0 98 17 | 25105 | 00000000 00000000 01100010 00010001 |
UTF-32LE | 11 62 00 00 | 17 98 0 0 | 291635200 | 00010001 01100010 00000000 00000000 |
UTF-8转换为unicode的方式参考这里。
BOM(Byte Order Marker,字节顺序标记)
UTF Encoding | Byte Order Marker |
---|---|
UTF-8 | EF BB BF |
UTF-16 BE | FF FE |
UTF-16 LE | FE FF |
UTF-32 BE | FF FE 00 00 |
UTF-32 LE | 00 00 FE FF |
在文本文件的开头若干个字节用来标识下当前文件是以上哪种编码类型,该用哪种方式解码。
大端存储:两个字节12 34,低位的34放在高位地址中,令人头大
小端存储:两个字节12 34,低位的34放在低位地址中
锟斤拷
设备在显示字符的时候,会把不能显示的字符显示为符号�,即\ufffd
,参考这里。
这个unicode用UTF-8表示为EF BF BD
,当用GBK解码遇到两个��时,编码为EF BF BD EF BF BD
,GBK的2字节定长解码会将其识别为EF BF
+BD EF
+BF BD
,对应的汉字就是 锟斤拷。
windows文本文件中的编码
- ANSI,英文系统是ASCII,中文系统是GBK或BIG5
- UTF-16 LE,UTF-16 BE,分别是UTF-16小端存储和大端存储
- UTF-8,不用多提,老朋友了
- 带有BOM的UTF-8,BOM(byte order mark),字节顺序标记,它是插入到以UTF-8、UTF-16、UTF-32编码Unicode文件开头的特殊标记,用来识别Unicode文件的编码类型。识别这个编码到底是前面的哪一种。
urlcode
urlcode的存在主要是为了应对url中的非法字符,例如url中有特殊含义的保留字符=、/、空格等,还有一些ascii码不支持的字符(如中文)。
因此对于中文来说,所谓的url编码一般情况下其实就是UTF-8编码,如“中文”这两个汉字,UTF-8编码为0xE4 0xB8 0xAD 0xE6 0x96 0x87,经过Url编码之后得到"%E4%B8%AD%E6%96%87",将它放到baidu.com的传参中
中文编码使用实践
python的chardet
python库中有一个用来检测文件编码类型的工具,chardet,即charset detect,十分好用,会根据以上描述的不同编码方式的特征,尝试探测字符串的编码方式。
总结
最后,我们来对编码的发展史进行一下总结:
- 计算机存储的信息全都是01的二进制
- 最初发明计算机的美国人先定义了单字节的英文编码规范ASCII(0-127)
- 随后欧洲国家纷纷加入战场,对ASCII码进行了扩展,构成了ISO 8859-1(128-255)
- 随后中日韩加入战场,使用了双字节形式的编码,中国程序员最熟悉的是GBK(两字节),能够兼容纯ASCII码
- 最后国际制定了统一的编码Unicode,包含了世界上所有符号(0-0x10FFFF),但是Unicode的数字太大,于是有了多种压缩编码的方式
- UTF-32,就是完全不压缩的形式,一个英文4个字节,没有人用
- UTF-16,英、中、日等常用语言都是2字节,特殊字符4字节,纯英文存储时会有浪费
- UTF-8,变长字节,英文和ASCII完全一致的1字节,中文3字节,能够完全兼容纯ASCII码,是现在主流编码方式