之前写在GitHub Pages上,发表于2017-02-06。这篇是总结删改了多次得到的,可能之后理解更深会进一步修改吧
在人们不仅仅满足于计算机只有计算器功能而想要用其表示世界万物的时候,有一群人决定用8个可以表示两种状态的晶体管组合到一起以表示256
(2^8=256)种不同的状态,每种状态表示一个信息的话,这些状态就可以用来描述世界上的万物。至于为何会选用8个一组而不是1,2,3,4,5,……呢?因为CPU的硬件原理(这里不讨论物理)决定了选用2的次方比较利于计算机的运算,所以选择只剩下2,4,8,16,32,……而选用7个作为一组的话就有128
(2^7=128)种不同的组合,已经完全够表示所有的文字和符号(碍于当时的各种局限性,美国人只考虑了拉丁文26个字母的大小写、数字和一些(可显示)符号,以及一些控制符(比如回车))了,于是就近选择了8个作为一组。这样的8个一组的组合被他们强制规定称为字节
。而这128个不同的组合表达了哪些不同的字节,这就是ASCII码所规定的内容了。
这群人把这128
种状态按前后顺序编了个号,又规定从编号0开始到编号31的总共32种状态分别表示一种特殊用途,一但终端、打印机等遇上这些约定好的字节被传过来时,就要做一些约定的动作。比如遇上0x10
(0x后接十六进制数是十六进制表示法,这里的0x10表示编号为16的字节), 终端就换行;遇上0x07
, 终端的蜂鸣器就滴滴滴的叫;遇上0x1b
, 打印机就打印反色的文字(就是交换前景色背景色后打印),或者终端就用彩色显示文字。这些规定用了一段时间后发觉挺好使,于是这规定就正式确立下来,并把这些0x20以下的字节(编号0~31)称为控制码
/控制符号
。
他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127
号(共128个),这样计算机就可以用不同字节来表示英语这种语言的文字了。以上规定经使用后反响很好,于是大家都把这个方案确定下来,并称之为美国国家标准学会(American National Standards Institute,ANSI)的美国信息互换标准代码
(American Standard Code for Information Interchange,ASCII),也就是俗称的ASCII编码
。当时世界上几乎所有的计算机都用这样的ASCII编码方案来保存英文文字。
计算机的运算、电子信息存储等优势展现出来以后,世界各地的都开始使用计算机,但是很多国家用的不是英文,他们的文字里有许多是ASCII编码表里没有的,为了可以在计算机保存他们的文字,他们决定采用127
号(第128个)之后到255
号(第256个)之间没有使用的空位来表示自己母语需要的字母、符号,同时还加入了很多画表格时需要用到的横线、竖线、交叉等形状的符号,一直把序号编到了最后一个字节——第255
号。从128到255这一部分的字符集被称扩展字符集
。
等到计算机起步较晚的中国人得到计算机时,已经没有可以利用的字节来表示汉字,况且常用汉字一再精简也仍有6000多个需要保存,哪怕256种字节全给咱也完全不够使。但是这难不倒智慧的中国人,咱们不客气地把那些127号之后的各种标准的符号们直接取消掉,并规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xB0(即十进制的176)到0xF7(即十进制的247)共72个,后面一个字节(低字节)从0xA1(即十进制的161)到0xFE(即十进制的254)共94个,这样就可以组合出6768(72*94=6768)个字节了(其中有5个空位是D7FA-D7FE),差不多常用简体汉字都够了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII
里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的全角
字符,而原来在127号以下的那些就叫半角
字符了。
这套标准中国人用起来很不错,于是就确立了这种编码方式,并称之为GB2312
。GB2312可以说是对ASCII的中文扩展。
但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这套标准里打出来。于是我们不得不继续把GB2312
没有用到的码位找出来老实不客气地用上。但后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要求第一个字节是大于127号就表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容(不管后面跟的是不是大于127号)。结果扩展之后的编码方案被称为GBK编码
(GuoBiaoKuozhan,“国标、扩展”)标准,GBK是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从8140至FEFE(剔除xx7F),共23940个码位,共收录了21003个汉字(包括繁体字)和一些符号。值得一提的是GBK与当时的国际标准UCS(ISO 10646)完全兼容,至于什么是UCS后文细说。
后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK扩成了GB18030
。从此之后,中华民族的文化就可以在计算机时代中传承了。中国的编码工作者们看到这一系列汉字编码的标准很好使,于是通称其叫做DBCS
(Double Byte Charecter Set,双字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。于是就有了早期的一个著名说法一个汉字算两个英文字符,一个汉字算两个英文字符。
在那个信息急需电子化的时代,由于没有统一的标准(其实是没有人认识到各自为政的弊端而不买统一标准的账),各个国家都像中国这样搞出一套自己的编码标准,结果各个语系名族谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用着同一种语言的兄弟地区,也分别采用了不同的DBCS编码方案:当时的大陆人想让电脑显示大陆的简体汉字,就必须装上一个“汉字系统”(其实就是改变编码),专门用来处理简体汉字的显示、输入的问题;而台湾人想要显示台湾的繁体汉字就必须改装另一套支持BIG5编码
的什么“倚天汉字系统”(其实又是改变编码)才可以用。装错了字符系统,显示就会乱了套!这怎么办?而且世界民族之林中还有那些暂时还没用上电脑的穷苦人民,他们的文字将来电子化又该怎么办?正在这时,一个叫ISO
(International Organization for Standardization,国际标谁化组织)的国际组织宣布决定着手解决这个问题(其实想搞国际统一标准的有好几个组织,争到中期还剩两家,最后又争了一段时间两家合作决定让ISO领头搞),他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算管它叫Universal Multiple-Octet Coded Character Set
(一说叫Universal Coded Character Set),简称UCS
,俗称UNICODE
。
UNICODE开始制订时,计算机的存储器容量极大地发展了(想当年能用钱买到的民用最大存储空间的硬盘不过29MB),存储空间再也不是重要瓶颈了。于是ISO就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ASCII里的那些“半角”字符,UNICODE保持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于“半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种“大气”的方案在保存英文文本时会多浪费一倍的存储空间!
这时候,从旧社会里走过来的程序员开始发现一个奇怪的现象:他们的strlen函数靠不住了,一个汉字不再是相当于两个字符了,而是一个!是的,从UNICODE开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的一个字符
,同时,也都是统一的两个字节
!请注意“字符”和“字节”两个术语的不同,“字节”是一个8位的物理存贮单元,而“字符”则是一个文化相关的符号最小单位。在UNICODE中,一个字符就是两个字节。一个汉字算两个英文字符的时代已经快过去了。
从前多种字符集存在时,那些做多语言软件的公司遇上过很大麻烦,他们为了在不同的国家销售同一套软件,就不得不在区域化软件时也增加对应地区的双字节字符集,不仅要处处小心不要搞错,还要把软件中的文字在不同的字符集中转来转去。更麻烦的是需要多语言工作环境的人就需要不停地在各种地区性内码表之间切换(DOS命令:chcp)。UNICODE对于他们来说是一个很好的一揽子解决方案,于是从Windows NT开始,MS趁机把它们的操作系统改了一遍,把所有的核心代码都改成了用UNICODE方式工作的版本,从这时开始,WINDOWS系统终于无需要加装各种区域化语言系统,就可以显示全世界上所有文化的字符了。
但是,UNICODE在制订时没有考虑与任何一种现有的编码方案保持兼容,比如对于GBK,这使得GBK与UNICODE在汉字的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从UNICODE编码和另一种编码进行转换,这种转换必须通过查表来进行。再加上前面提到的UNICODE字符集在保存英文文本时会多浪费一倍的存储空间,这不太能令人接受,尤其是后来计算机不再仅限于单机使用,而是需要信息交换的联网工作,但限于当时硬件制造技术水平的瓶颈,网线的传输速率很低,那么多用了一倍的存储空间就意味着至少(须知数据传输领域有个词叫“丢包”)多出一倍的传输时间,这是在传输速率低下的当时的人们绝对不能容忍的,于是大家都知道UNICODE前景很好,但是大家都不用它,UNICODE也就得不到推广。
如前所述,UNICODE是用两个字节来表示为一个字符,他总共可以组合出65536个不同的字符,这大概已经可以覆盖世界上所有文化的符号。如果还不够也没有关系,ISO已经准备了UCS-4
方案(其实之前的方案不仅可以叫做UCS,也可以称之为UCS-2),说简单点就是四个字节来表示一个字符,这样我们就可以组合出21亿个不同的字符出来(最高位有其它用途),这大概可以用到外星人来地球拜年的那一天吧!
前文说了,UNICODE限于浪费存储空间,UNICODE如何在网络上高效传输是一个必须解决的问题,于是面向传输的众多UTF(UCS Transformation Format)标准出现了。顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位传输数据。只不过为了传输时的可靠性,从UNICODE到UTF时并不是直接的对应,而是先要通过一些简单的规则来进行转换。在UTF-8中,0-127号的字符用1个字节来表示,使用与ASCII相同的编码。这意味着1980年代写的文档用UTF-8打开一点问题都没有。只有128号及以上的字符才用2个,3个或者4个字节来表示。因此,UTF-8被称作可变长度编码。它非常完美的解决了UNICODE一统世界标准却不便于传输的问题,于是UNICODE立刻遍地开花,春光灿烂。
学习过网络编程的程序猿们都知道,在网络里传递信息时有一个很重要的问题,就是对于数据高低位的解读方式,即字节序问题。一些计算机是采用低位字节被存在前面的方式,称之为低字节序
(Little Endian),例如Intel架构的微处理器;而另一些是采用高位字节被存在前面的方式,称之为高字节序
(Big Endian),例如Motorola架构的微处理器。在网络中数据交换时,为了核对双方对于高低位的规则是否是一致的,采用了一种很简便的方法——文本流前添加BOM(Byte Order Mark,字节顺序标记),就是在文本流的开始时添加一个标志符来表示该文本的字节序。具体规则是如果该文本是高字节序,BOM就为FEFF
,反之,则为FFFE
。
BOM尽管很有用,但并不是很简洁。还有一个类似的概念,称作「魔术字」(Magic Byte),很多年来一直被用来表明文件的格式。BOM和魔术字间的关系一直没有被清楚的定义过,因此有的解释器会搞混它们。
讲到这里,顺便说说一个很著名的奇怪现象:当在Windows的记事本里新建一个文件,输入联通
两个字,保存关闭后再次打开,会发现这两个字已经消失了,代之的是几个乱码!
其实这是因为GB2312编码与UTF8编码产生了编码冲撞的原因。
上文提到,从UNICODE到UTF-8时并不是直接的对应,而是先要通过一些简单的规则来进行转换,那么这时候就必须要说说这个规则了。
从网上引来一段从UNICODE到UTF8的转换规则:
UNICODE字符集范围 | UTF-8编码规则 |
---|---|
0000 0000 ~ 0000 007F | 0xxxxxxx |
0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx |
0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000 ~ 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
例如”汉”字的UNICODE编码是6C49
(没有原因,就是规定,后文会详细解释UNICODE)。6C49在0000 0800 ~ 0000 FFFF
这个区间里,所以要用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将6C49写成二进制是0110 1100 0100 1001
(为便于阅读去掉了前导0),将这个比特流按三字节模板的分段方法分为0110 110001 001001
,依次代替模板中的x,得到1110-0110 10-110001 10-001001
,再转为十六进制即E6 B1 89
,这就是其对应的UTF-8编码。而当在Windows中新建一个文本文件时,记事本的编码默认是ANSI, 如果你在ANSI的编码下输入汉字,那么他实际就是DBCS里的GB系列的编码方式,在这种编码下,”联通”的内码是C1 AA CD A8
,换算成二进制即1100 0001 1010 1010 1100 1101 1010 1000
,观察发现第一二个字节、第三四个字节的起始部分的都是110
和10
,正好与UTF-8规则里的第二个模板规则,即两字节模板是一致的,于是再次打开记事本时,而记事本默认会先判断它是不是UTF,如果不是再用ANSI编码。而第一步一判断发现其与UTF-8的第二个模板规则,即二字节模板吻合,于是就认为这是一个UTF-8编码的文件。让我们把这个长得像却不是UTF-8的数据当做UTF-8解读一下:把第一个字节的110和第二个字节的10去掉,我们就得到了---0 0001 --10 1010
,再把各个比特位对齐,补上前导的0,就得到了0000 0000 0110 1010
,换算成十六进制就是006A
,最后拿UNICODE字符集一对照,U+006A是小写的字母j
,而之后的两字节用UTF-8解码之后是0368
,U+0368这个字符什么也不是,就会变成乱码。这就是只有”联通”两个字的文本没有办法在记事本里正常显示的原因。
而如果你在联通
之后多输入几个字,其他的字的编码不见得又恰好是110和10开始的字节,这样再次打开时由于第一步判断不满足,于是记事本就不会坚持认为这是一个UTF-8编码的文件,而会用ANSI的方式解读,这时乱码又不出现了。
如果你要测试”abc汉字”这个串的长度,在没有n前缀的数据类型里,这个字串是7个字符的长度,因为一个汉字相当于两个字符。而在有n前缀的数据类型里,同样的测试串长度的函数将会告诉你是5个字符,因为一个汉字就是一个字符。
UNICODE并不涉及字符是怎么在字节中表示的,它仅仅指定了字符对应的数字,仅此而已。
关于UNICODE的其它误解包括:UNICODE支持的字符上限是65536个,UNICODE字符必须占两个字节。告诉你这些的人应该去换换脑子了。
UNICODE只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节。
UNICODE字符是怎样被编码成内存中的字节这是另外的话题,它是被UTF(UNICODE Transformation Formats)定义的。
总结:UNICODE只是一个字符与数字的映射关系,官方术语是码位(Code Point),总是用U+开头,如字母A是U+0061。即UNICODE(不是编码方案)是字符集,UTF-8、UTF-16等才是编码方案。
Content-Type
或者meta
标签的charset
属性来显式指定你的文档的编码。这样浏览器就不需要猜测你使用的编码了,他们会准确的使用你指定的编码来渲染文档。参考:
学点编码知识又不会死:Unicode的流言终结者和编码大揭秘