阅读 1091

☀️学习系列:字符串编码

Created By JishuBao on 2019-04-02 12:38:22
Recently revised in 2019-04-03 12:38:22

 

  欢迎大家来到技术宝的掘金世界,您的star是我写文章最大的动力!GitHub地址     

文章简介:

1、遇上emoji

2、字符编码的故事

3、字符编码详解

4、字符编码区别

5、谈谈emoji

一、遇上emoji

最近闲来无事,在看源码,发现一个很有意思的事情。

当时就震惊了,这个emoj表情好牛逼啊...直接在编辑器里面就有表情了,顿时就感觉不懂原理,各种字符编码都理解的云里雾里的,只知道一个utf-8,所以准备搜索大量资料准备把这个字符编码了解通透!

二、字符编码的故事

很久很久以前,有一群人,他们决定用8个可以开合的晶体管来组合成不同的状态,以表示世界上的万物。他们看到8个开关状态是好的,于是他们把这称为”字节“。再后来,他们又做了一些可以处理这些字节的机器,机器开动了,可以用字节来组合出很多状态,状态开始变来变去。他们看到这样是好的,于是它们就这机器称为”计算机“。

开始计算机只在美国用。八位的字节一共可以组合出256(2的8次方)种不同的状态。 他们把其中的编号从0开始的32种状态分别规定了特殊的用途,一但终端、打印机遇上约定好的这些字节被传过来时,就要做一些约定的动作:

遇上0×10, 终端就换行;

遇上0×07, 终端就向人们嘟嘟叫;

遇上0x1b, 打印机就打印反白的字,或者终端就用彩色显示字母。

他们看到这样很好,于是就把这些0×20以下的字节状态称为”控制码”。他们又把所有的空格、标点符号、数字、大小写字母分别用连续的字节状态表示,一直编到了第127号,这样计算机就可以用不同字节来存储英语的文字了。大家看到这样,都感觉 很好,于是大家都把这个方案叫做 ANSI 的”Ascii”编码(American Standard Code for Information Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。

后来,就像建造巴比伦塔一样,世界各地都开始使用计算机,但是很多国家用的不是英文,他们的字母里有许多是ASCII里没有的,为了可以在计算机保存他们的文字,他们决定采用 127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128 到255这一页的字符集被称”扩展字符集“。从此之后,贪婪的人类再没有新的状态可以用了,美帝国主义可能没有想到还有第三世界国家的人们也希望可以用到计算机吧!

等中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。中国人民看到这样很不错,于是就把这种汉字方案叫做 “GB2312“。GB2312 是对 ASCII 的中文扩展

但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,特别是某些很会麻烦别人的国家领导人。于是我们不得不继续把GB2312 没有用到的码位找出来老实不客气地用上。后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK包括了GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。 后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。 中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 “DBCS“(Double Byte Charecter Set 双字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。那时候凡是受过加持,会编程的计算机僧侣们都要每天念下面这个咒语数百遍: “一个汉字算两个英文字符!一个汉字算两个英文字符……

因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用着同一种语言的兄弟地区,也分别采用了不同的 DBCS 编码方案——当时的中国人想让电脑显示汉字,就必须装上一个”汉字系统”,专门用来处理汉字的显示、输入的问题,像是那个台湾的愚昧封建人士写的算命程序就必须加装另一套支持 BIG5 编码的什么”倚天汉字系统”才可以用,装错了字符系统,显示就会乱了套!这怎么办?而且世界民族之林中还有那些一时用不上电脑的穷苦人民,他们的文字又怎么办? 真是计算机的巴比伦塔命题啊!

正在这时,大天使加百列及时出现了——一个叫 ISO(国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号 的编码!他们打算叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “unicode“。

unicode开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 ISO 就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于ASCII里的那些“半角”字符,unicode包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于”半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

这时候,从旧社会里走过来的程序员开始发现一个奇怪的现象:他们的 strlen 函数靠不住了,一个汉字不再是相当于两个字符了,而是一个!是的,从unicode开始,无论是半角的英文字母,还是全角的汉字,它们都是统一的”一个字符“!同时,也都是统一的”两个字节“,请注意”字符”和”字节”两个术语的不同,“字节”是一个8位的物理存贮单元,而“字符”则是一个文化相关的符号。在unicode中,一个字符就是两个字节。一个汉字算两个英文字符的时代已经快过去了。

unicode同样也不完美,这里就有两个的问题,一个是,如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储空间来说是极大的浪费,文本文件的大小会因此大出二三倍,这是难以接受的

unicode在很长一段时间内无法推广,直到互联网的出现,为解决unicode如何在网络上传输的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。UTF-8就是在互联网上使用最广的一种unicode的实现方式,这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,当字符在ASCII码的范围时,就用一个字节表示,保留了ASCII字符一个字节的编码做为它的一部分,注意的是unicode一个中文字符占2个字节,而UTF-8一个中文字符占3个字节)。从unicode到utf-8并不是直接的对应,而是要过一些算法和规则来转换。

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

最后简单总结一下:

中国人民通过对 ASCII 编码的中文扩充改造,产生了 GB2312 编码,可以表示6000多个常用汉字。

汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。

中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码。

每个国家都像中国一样,把自己的语言编码,于是出现了各种各样的编码,如果你不安装相应的编码,就无法解释相应编码想表达的内容。

终于,有个叫 ISO 的组织看不下去了。他们一起创造了一种编码 UNICODE ,这种编码非常大,大到可以容纳世界上任何一个文字和标志。所以只要电脑上有 UNICODE 这种编码系统,无论是全球哪种文字,只需要保存文件的时候,保存成 UNICODE 编码就可以被其他电脑正常解释。

UNICODE 在网络传输中,出现了两个标准 UTF-8 和 UTF-16,分别每次传输 8个位和 16个位。于是就会有人产生疑问,UTF-8 既然能保存那么多文字、符号,为什么国内还有这么多使用 GBK 等编码的人?因为 UTF-8 等编码体积比较大,占电脑空间比较多,如果面向的使用人群绝大部分都是中国人,用 GBK 等编码也可以。

三、字符编码详解

1.为何需要编码

我们知道,所有的信息最终都表示为一个二进制的字符串每一个二进制位(bit)有0和1两种状态。当我们需要把字符'A'存入计算机时,应该对应哪种状态呢,存储时,我们可以将字符'A'用01000010(这个随便编的)二进制字符串表示,存入计算机;读取时,再将01000010还原成字符'A'。那么问题来了,存储时,字符'A'应该对应哪一串二进制数呢,是01000010?或者是10000000 11110101?说白了,就是需要一个规则。这个规则可以将字符映射到唯一一种状态(二进制字符串),这就是编码。而最早出现的编码规则就是ASCII编码,在ASCII编码规则中,字符'A'既不对应01000010,也不对应1000 0000 11110101,而是对应**01000001*(不要问为什么,这是规则)。

2.ASCII

这套编码规则是由美国定制,一共规定了128个字符的编码,比如空格"SPACE"是32(十进制)(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括 32个不能打印出来的控制符号),只占用了一个字节(8 bit)的后面7位,最前面的1位统一规定为0总共才有128个字符编码,一个字节都没有用完,这好像似乎有点太少了。于是乎,就开始压榨最高位,对其为1时也进行编码,利用最高位进行编码的方式就称为非ASCII编码,如ISO-8859-1编码。

3.ISO-8859-1

这套编码规则由ISO组织制定。是在 ASCII 码基础上又制定了一些标准用来扩展ASCII编码,即 00000000(0) ~ 01111111(127) 与ASCII的编码一样,对 10000000(128) ~ 11111111(255)这一段进行了编码,如将字符**§编码成 10100111(167)。ISO-8859-1编码也是单字节编码,最多能够表示256**个字符。Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。但是,即使能够表示256个字符,对中文而言,还是太少了,一个字节肯定不够,必须用多个字节表示。但是,由于是单字节编码,和计算机最基础的表示单位一致,所以很多时候,仍旧使用 ISO8859-1编码来表示。而且在很多协议上,默认使用该编码。比如,虽然"中文"两个字不存在ISO8859-1编码,以GB2312编码为例,应该是D6D0 CEC4两个字符,使用ISO8859-1编码的时候则将它拆开为4个字节来表示:D6D0 CEC4(事实上,在进行存储的时候,也是以字节为单位进行处理)。而如果是UTF编码,则是6个字节e4 b8 ad e6 96 87。很明显,这种表示方法还需要以另一种编码为基础才能正确显示。而常见的中文编码方式有GB2312、BIG5、GBK。

4.GB2312

GB2312其对所收录字符进行了"分区"处理,共94个区,区从1(十进制)开始,一直到94(十进制),每区含有94个位,位从1(十进制)开始,一直到94(十进制),共8836(94 * 94)个码位,这种表示方式也称为区位码,GB2312是双字节编码,其中高字节表示区,低字节表示位。各区具体说明如下:

01-09区收录除汉字外的682个字符,有164个空位(9 * 94 - 682)。
10-15区为空白区,没有使用。
16-55区收录3755个一级汉字(简体),按拼音排序。
56-87区收录3008个二级汉字(简体),按部首/笔画排序。
88-94区为空白区,没有使用。
复制代码

那么根据区位码如何算出GBK2312编码呢?区位码的表示范围为0101 - 9494(包含了空的区位码)。点击这里,查看中GB2312编码区位码。之后只需要按照如下规则进行转化即可。

  1. 将区(十进制)转化为十六进制。
  2. 将转化的十六进制加上A0,得到GB2312编码的高字节。
  3. 将位(十进制)转化为十六进制。
  4. 将转化的十六进制加上A0,得到GB2312编码的低字节。
  5. 组合区和位,区在高字节,位在低字节。
  6. 得到GB2312编码。

例如:'李'字的区位码为3278(表示在32区,78位)。

  1. 将32(区)转化为十六进制为20。
  2. 加上A0为C0。
  3. 将78(位)转化为十六进制为4E。
  4. 加上A0为EE。
  5. 组合区和位,为C0EE。
  6. 得到GB2312编码,即'李'字的GB2312编码为C0EE。

GB2312用两个字节编码,采用分区编码,总共编码的中文个数为6763(3755 + 3008)。这些汉字只是最常用的汉字,已经覆盖中国大陆99.75%的使用频率。但是,还有一些汉字在GB2312中没有被编码,如'镕'字,在GB2312中就没有被编码,这样就导致了问题,随之就出现了主流的GBK编码。在讲解GBK编码之前,我们另外讲解一下BIG5编码。

5.BIG5

BIG5采用双字节编码,使用两个字节来表示一个字符。高位字节使用了0x81-0xFE,低位字节使用了0x40-0x7E,及0xA1-0xFE。该编码是繁体中文字符集编码标准,共收录13060个中文字,其中有二字为重复编码,即“兀、兀”(A461及C94A)和“嗀、嗀”(DCD1及DDFC)。具体的分区如下:  

8140-A0FE 保留给使用者自定义字符(造字区)
A140-A3BF 标点符号、希腊字母及特殊符号。其中在A259-A261,收录了度量衡单位用字:兙兛兞兝兡兣嗧瓩糎。
A3C0-A3FE 保留。此区没有开放作造字区用。
A440-C67E 常用汉字,先按笔划再按部首排序。
C6A1-F9DC 其它汉字。
F9DD-F9FE 制表符。
复制代码

点击这里,查看BIG5编码。注意,BIG5编码与GBK编码没有什么关系。

6.GBK

GBK编码扩展了GB2312,完全兼容GB2312编码(如'李'字的GBK、GB2312编码均为C0EE),但其不兼容BIG5编码('長'字的BIG5编码为AAF8,GBK编码为E94C,'李'字的BIG5编码为A7F5 不等于C0EE),即如果使用GB2312编码,使用GBK解码是完全正常的,但是如果使用BIG5编码,使用GBK解码,会出现乱码。相比于GB2312编码,GBK编码了更多汉字,如'镕'字。GBK编码依然采用双字节编码方案,其编码范围:8140-FEFE,剔除xx7F码位,共23940个码位。能表示 21003 个汉字。点击这里,查看GBK编码。点击这里,可以查询中文的其他编码。在GBK之后又出现了GB18030编码,但是没有形成主流,故不做介绍,至此,中文编码的问题已经讲解完成。那么问题又来了,大陆网民与在海峡两岸网民交流时,若都使用GBK编码,则没有问题,若一方使用GBK编码,一方使用BIG5编码,那么就会出现乱码问题,这是在海峡两岸网民交流,如果漂洋过海进行交流呢?那就更容易出现乱码问题,这时候我们可能想,要是有一套全世界都通用的编码就好了,不要担心,这样的编码确实是存在的,那就是Unicode。

7.Unicode

有两个独立的, 创立单一字符集的尝试. 一个是国际标准化组织(ISO)的 ISO 10646 项目, 另一个是由多语言软件制造商组成的协会组织的 Unicode 项目. 在1991年前后, 两个项目的参与者都认识到, 世界不需要两个不同的单一字符集. 它们合并双方的工作成果, 并为创立一个单一编码表而协同工作. 两个项目仍都存在并独立地公布各自的标准, 但 Unicode 协会和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO 10646 标准的码表兼容, 并紧密地共同调整任何未来的扩展。

Unicode是指一张表,里面包含了可能出现的所有字符,每个字符对应一个数字,这个数字称为码点(Code Point),如字符'H'的码点为72(十进制),字符'李'的码点为26446(十进制)。Unicode表包含了1114112个码点,即从000000(十六进制) - 10FFFF(十六进制)。地球上所有字符都可以在Unicode表中找到对应的唯一码点。点击这里,查询字符对应的码点。Unicode将码空间划分为17个平面,从00 - 10(十六进制,最高两位),即从0 - 16(十进制),每个平面有65536个码点(2^16),其中最重要的是第一个Unicode平面(码位从0000 - FFFF),包含了最常用的字符,该平面被称为基本多语言平面(Basic Multilingual Plane),缩写为BMP,其他平面称为辅助平面(Supplementary Planes),在基本多文种平面內, 从D800到DFFF之间的码位区段是永久保留不映射到字符的, 因此UTF-16编码巧妙的利用了这保留下来的码位来对辅助平面内的字符进行编码,这点后面进行讲解。Unicode只是一个符号集,只规定的字符所对应的码点,并没有指定如何存储,如何进行存储出现了不同的编码方案,关于Unicode编码方案主要有两条主线:UCS和UTF。UTF主线由Unicode Consortium进行维护管理,UCS主线由ISO/IEC进行维护管理。

8.UCS

UCS全称为"Universal Character Set",在UCS中主要有UCS-2和UCS-4。

1.UCS-2

UCS-2是定长字节的,固定使用2个字节进行编码,从0000(十六进制)- FFFF(十六进制)的码位范围,对应第一个Unicode平面。采用BOM(Byte Order Mark)机制,该机制作用如下:

  1. 确定字节流采用的是大端序还是小端序。
  2. 确定字节流的Unicode编码方案。

2.UCS-4

UCS-4是定长字节的,固定使用4个字节进行编码。也采用了BOM机制。

9.UTF

UTF全称为"Unicode Transformation Format",在UTF中主要有UTF-8,UTF-16和UTF-32。

1.UTF-8

UTF-8是一种变长编码方式,使用1-4个字节进行编码。UTF-8完全兼容ASCII,对于ASCII中的字符,UTF-8采用的编码值跟ASCII完全一致。UTF-8是Unicode一种具体的编码实现。UTF-8是在互联网上使用最广的一种Unicode的编码规则,因为这种编码有利于节约网络流量(因为变长编码,而非统一长度编码)。关于Unicode码点如何转化为UTF-8编码,可以参照如下规则:

  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  • 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
Unicode符号范围 UTF-8编码方式
(十六进制) (十进制) (二进制
0000 0000-0000 007F (0-127) 0xxxxxxx
0000 0080-0000 07FF (128-2047) 110xxxxx 10xxxxxx
0000 0800-0000 FFFF (2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
001 0000-0010 FFFF (65536-1114111) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

说明:字符'A'的Unicode码点为65(十进制),根据上表,在第一行范围,则字符'A'的UTF-8编码为01000001,中文字符'李'的Unicode码点为26446(十进制),二进制为01100111 01001110,十六进制为674E。根据上表,在第三行范围,则将'李'二进制代码从低位到高位依次填入x中,不足的填入0。得到UTF-8编码为11100110 10011101 10001110,即E69D8E(十六进制)。

由上述编码规则可知,0000 0000 - 0000 FFFF(第一行到第三行)为Unicode第一个平面(基本多语言平面),而0001 0000 - 10 FFFF(第四行)为Unicode其他平面(辅助平面)。在基本多语言平面对应了绝大多数常用的字符。对于大于65535(十进制)的码点,即在辅助平面上的码点,需要使用4个字节来进行UTF-8编码。

2.UTF-16

UTF-8是不定长的编码,使用1、2、3、4个字节编码,而UTF-16则只使用2或4个字节编码。UTF-16也是Unicode一种具体的编码实现。关于Unicode如何转化为UTf-16编码规则如下:

  1. 若Unicode码点在第一平面(BPM)中,则使用2个字节进行编码。
  2. 若Unicode码点在其他平面(辅助平面),则使用4个字节进行编码。

关于辅助平面的码点编码更详细解析如下:辅助平面码点被编码为一对16比特(四个字节)长的码元, 称之为代理对(surrogate pair), 第一部分称为高位代理(high surrogate)或前导代理(lead surrogates),码位范围为:D800-DBFF. 第二部分称为低位代理(low surrogate)或后尾代理(trail surrogates), 码位范围为:DC00-DFFF。注意,高位代理的码位从D800到DBFF,而低位代理的码位从DC00到DFFF,总共恰好为D800-DFFF,这部分码点在第一平面内是保留的,不映射到任何字符,所以UTF-16编码巧妙的利用了这点来进行码点在辅助平面内的4字节编码。

说明:字符'A'的Unicode码点为65(十进制),十六进制表示为41,在第一平面。根据规则,UTF-16采用2个字节进行编码。那么问题又来了,知道了采用两个字节编码,并且我们也知道计算机是以字节为单位进行存储,这两个字节应该表示为00 41(十六进制)?或者是41 00(十六进制)呢?这就引出了一个问题,需要用到之前提及的BOM机制来解决。

表示为00 41意味着采用了大端序(Big endian),而表示为41 00意味着采用了小端序。那么计算机如何知道存储的字符信息采用了大端序还是小端虚呢?这就需要加入一些控制信息,具体是采用大端序,则在文件前加入FE FF,采用小端序,则在文件前加入FF FE。这样,当计算开始读取时发现前两个字节为FE FF,就表示之后的信息采用的是小端序,反之,则是大端序。

字符 (无法显示,只能截图显示),其Unicode码点为65902(十进制),十六进制为1016E,很显然,已经超出了第一平面(BMP)所能表示的范围。其在辅助平面内,根据规则,UTF-16采用4个字节进行编码。然而其编码不是简单扩展为4个字节(00 01 01 6E),而是采用如下规则进行计算。

  1. 使用Unicode码位减去100000(十六进制),得到的值扩展20位(因为Unicode最大为10 FF FF(十六进制),减去1 00 00(十六进制)后,得到的结果最大为0FFF FF(十六进制),即为20位,不足20位的,在高位加一个0,扩展至20位即可)。
  2. 将步骤一得到的20位,按照高十位和低十位进行分割。
  3. 将步骤二的高十位扩展至2个字节,再加上D800(十六进制),得到高位代理或前导代理。取值范围是D800 - 0xDBFF。
  4. 将步骤二的低十位扩展至2个字节,再加上DC00(十六进制),得到低位代理或后尾代理。取值范围是DC00 - 0xDFFF。

Unicode转UTF-16规则流程图如下:

按照这个规则,我们计算字符的UTF-16编码,我们知道其码点为1016E,减去10000得到016E,扩展至0016E,进行分割,得到高十位为00 0000 0000,十六进制为0000,加上D800为D800;得到低十位为01 0110 1110,十六进制为016E,加上DC00为DD6E;综合得到D8 00 DD 6E。即UTF-16编码为D8 00 DD 6E(也可为D8 0 DD 6E)。

而对于UTF-32是使用4个字节表示,也采用BOM机制,可以类比UTF-16,这里不再额外介绍。

四、字符编码区别

1. UCS-2 与 UTF-16区别

从上面的分析知道,UCS-2采用的两个字节进行编码。在0000到FFFF的码位范围内,它和UTF-16基本一致,为什么说基本一致,因为在UTF-16中从U+D800到U+DFFF的码位不对应于任何字符,而在使用UCS-2的时代,U+D800到U+DFFF内的值被占用。

UCS-2只能表示BMP内的码点(只采用2个字节),而UTF-16可以表示辅助平面内的码点(采用4个字节)。

我们可以抽象的认为UTF-16可看成是UCS-2的父集。在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的意思基本一致。但当引入辅助平面字符后,想要表示辅助平面字符时,就只能用UTF-16编码了。

2.UCS -4与 UTF-16的区别

在BMP上,UTF-16采用2个字节表示,而在辅助平面上,UTF-16采用的是4个字节表示。对于UCS-4,不管在哪个平面都采用的是四个字节表示。

3 为什么UTF-8编码不需要BOM机制

因为在UTF-8编码中,其自身已经带了控制信息,如1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx,其中1110就起到了控制作用,所以不需要额外的BOM机制。

五、谈谈emoji

终于可以说说emoji了。Emoji早先由日本企业发明,日文将其称为“絵文字”。后来随着智能手机的推广,全世界都在用,于是被Unicode收编了。截至Unicode 10.0,共有1144个emoji被收录。

🍃与

这个小标题可能会有乱码或者显示一半的情况,这是有意为之。大家不妨来看一张图:

图中是同一段数据在我司内部通讯工具和Mac版微信中的表现。

抛去格式不太友好之外,有一处特别的地方是emoji的两片飘叶显示不对。实际上除了微信,这个emoji在几乎哪都不显示。这就奇怪了,我特地把两个软件中的飘叶都复制出来,结果发现他们压根连码点都不同:

  • 🍃: U+1F343
  • : U+E447

emmmm...同一个emoji怎么会变码点呢?emoji的特殊规则?没听说啊。。

几经搜索,我终于找到了原因。原来iPhone中使用的emoji最早是软银的一套编码,飘叶在这套编码中的码点正是U+E447。这个码点位于用户可自定义的私有码位区域,因此现今以Unicode的角度来看这个字符,它是没有确切的字形的。现在绝大部分软件也都不再支持这套老emoji了。这个emoji来自于一位微信用户的昵称,恰好微信还支持这套老emoji,于是能够显示出来。

从图中可以看到,飘叶在这套编码里恰好是U+E447。

单色与彩色

如果emoji就这点玩法的话,也不至于写这么一章了。实际上emoji比我最初的认识还要复杂很多。Unicode的emoji除了可以使用大家平时见到的彩色来展示,还可以用单色来展示,以适应一些非常简单的显示设备。

怎么做呢?规则就是在普通的emoji码点之后,紧跟一个用来表示颜色版本的“变幻符”,这个变幻符有两个取值:VS15(U+FE0E)和VS16(U+FE0F)。其中VS15表示强制使用单色版,而VS16则表示强制使用彩色版。如果没有变幻符呢,每个emoji可以使用自己默认的展示。

举个例子来说,U+26A0这个emoji可以有两种样子:

  • ⚠︎(U+26A0 + U+FE0E)
  • ⚠️(U+26A0 + U+FE0F)

各位还记得Unicode当中的“组合字”么,异曲同工。

肤色

想必大家已经见过了,现在每个人类emoji都有很多种肤色版本。Unicode从8.0开始,为所有展示人或人体部位的emoji都增加了肤色控制。

在普通人物的emoji码点之后如果跟上一个肤色码点,那么这个emoji就会采用相应的肤色。举个例子:

👃🏿 = 👃(U+1F443) + 🏿(U+1F3FF)

实际上不管emoji中的人是否露出皮肤,都可以这样组合,画emoji的人可以去抉择如何去表现。无论如何,现在用emoji已经可以这么干了:

🎅🏿???

emoji里的全家福

上面的emoji组合,都是同一个emoji内部的事情,然而实际上多个独立的emoji也可以进行组合。

在Unicode中,存在一个特殊的码点,被称为零宽连接符(ZWJ),其码点为U+200D。这个零宽连接符在平时是不会显示出来的,不然也不会叫零宽连接符了。之前网上有人贴出“空字符串”几百个字节,就是用它了。

零宽连接符在emoji中的作用,就是可以i将多个emoji组合成一个更大的emoji。大家平时在网上看到的全家福emoji,正是这样做出来的:

👨‍❤️‍💋‍👨 = 👨(U+1F468) + ZWJ(U+200D) + ❤(U+2764) + ZWJ(U+200D) + VS16(U+FE0F) + ZWJ(U+200D) +💋(U+1F48B)+ ZWJ(U+200D) +👨(U+1F468)

可以说是很解耦了。。

当然也不是所有的软件现在都支持ZWJ这种玩法,对于不支持的软件,全家福就会被打散。

Unicode && JavaScript

那么作为一个前端,JavaScript对Unicode的支持是怎样的呢?事实是,JavaScript的字符串使用UTF-16来存储字符。在我印象里,有这么几个函数与Unicode关系最大:

String.prototype.charAt() //返回指定位置的字符 String.prototype.charCodeAt() //返回指定位置的UTF-16编码 String.prototype.codePointAt() //返回指定位置的Unicode码点 String.length //返回对象中字符串所占的UTF-16单元数量

干说没用,来考虑一下下面这段代码:

// U+1F4A9
const str = '💩';
console.log(str.charCodeAt(0));
console.log(str.charAt(0));
console.log(str.codePointAt(0));
复制代码

这是一坨屎无误,第一行log的情况是:

console.log(str.charCodeAt(0));
// 55357
// 0xD83D
复制代码

嗯?好像和码点对不上?答案是这样的

'💩' === '\u{1F4A9}'
'💩' === '\uD83D\uDCA9'
复制代码

哦,原来这坨屎已经超出了UTF-16最初支持的2字节表示,因此需要使用代理对来表示,这样一来就不是直接给出码点了。

这样的话,第二行log输出也就很好理解了:

console.log(str.charAt(0));
// ‘?'
复制代码

charAt比较简单,只是单纯地将UTF-16字符串按下标返回对应的字符,这里会打出一个代理对的一半,所以显示不出来。也就是说chartAt是不会完整吐出一个需要代理对的字来的。

第三个log给出了你可能最想要的:

console.log(str.codePointAt(0));
// 128169
// 0x1F4A9
复制代码

这是真正的Unicode码点无误了。那么console.log(str.length);的话,会输出什么呢?由于这个属性返回的是UTF-16单元的数量,而这坨屎需要2个UTF-16单元,因此其输出会是2。这可不是字节数哦。

那么,既然str.codePointAt(0)能够返回出整坨屎的Unicode码点,那str.codePointAt(1)会返回什么呢?ES标准说了,如果这个下标不是代理对的开头,那么只返回指向的UTF-16单元,也就是说:

console.log(str.codePointAt(1));
// 56489
// 0xDCA9
复制代码

最后说一下for循环的区别:

  • for(...i++;...)...str[i]: 按charCodeAt()进行循环
  • for…in: 按charCodeAt()进行循环
  • for…of: 按codePointAt()进行循环

所以只有for...of是真正理解Unicode的。大家用for来循环的时候,可要小心了,否则一不小心就会把代理对给拆开。

细节是魔鬼

计算机的历史只有短短不到100年的时间,而互联网则只有不到30年。因为历史很短,很多时候我们会产生一种假象,那就是计算机的历史好像是笔直的,一切设计都很合理、恰到好处,只需理解一下高抽象层次的概念和原理即可。而事实上则恰好相反,计算机世界的历史崎岖不平,充满了错误和因为错误而颠簸的设计,这里面隐藏了大量的细节。有时我们假装自己已经对程序了如指掌,“啊,编码嘛,不就是映射一下嘛;哦,HTTP协议嘛,很简单啊,就是个抽象层而已啊”,假装自己是高级程序员,因此好像可以忽略这些细节。实际呢,到处都是坑!

细节就是魔鬼,即使在看起来并不复杂的字符编码上也是如此。如果你忽视了这些,就只有用户来替你承担了。

文章分类
阅读
文章标签