简介
大家都知道计算机是美国人发明的,发明计算机以后必然会面临一些问题。比如,如何把自己把他们国家语言中用到的字符存储在计算机中。
Ascii的由来
当时美国人想非常简单,美国人把他们要用到的字符一个一个的罗列出来,然后给每个字符一个唯一编码,就是码位,对应字符转成二进制信息存储。
如下所示
图来源
可能直观地看这张图大家感受不强烈,下面我把这个转成稍微通俗一点的方式展示。
| 码位 | 字符 | 二进制 |
|---|---|---|
| 0 | 空字符 | 0000 0000 |
| 1 | 标题开始 | 0000 0001 |
| 2 | 正文开始 | 0000 0010 |
| ⋮ | ⋮ | ⋮ |
| 125 | } | 0111 1101 |
| 126 | - | 0111 1110 |
| 127 | 删除字符 | 0111 1111 |
美国人基本有了这些字符就够用了。后来欧洲人也要用计算机,发现了计算机中没有自己国家的字符,然后在ascii的基础上开始扩展128到255(这部分称为ascii扩展字符集)。想看的可以点击这里。
GB2312 的由来
后来我们中国人也开始使用计算机了,我们要上计算机上显示汉字。我们的汉字非常多ascii字符集的扩展不够用,那么我们要有自己的字符集,但是我们不能破坏人家原有的ascii字符集,所以在ascii的基础上我们扩展。
我们中国的第一套字符集GB2312, 使用了分区管理,一共设计了94个分区,每个分区含94个位,一共8836个码位。
01-09 区收录了除汉字以外的628个字符
10-15 区为空白区,没有使用
16-55 收录3955个一级字符,按拼音排序
56-89 收录2008个二级汉字,按部首/笔画排序
88-94 区为空白区,没有使用
具体规范大家可以点这里, 这里给大家简单做一个示例
这是GB2312中找到的17区对应的码位与字符。发现”本“这个字符在17区对应的30 的位置。我们把对应的区域内的汉字进行一个排序。我们现在有了对应字符的码位,但是没有规定怎么存储。举例”本“的码位是1730转成16进制。上图可以发现自己数一下"本"的位置是不是30.我们也可以按照下面这种方式排列
双字节编码 GB2312规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494。区号和位号分别加上0xA0就是GB2312编码。例如最后一个码位是9494,区号和位号分别转换成十六进制是5E5E,0x5E+0xA0=0xFE,所以该码位的GB2312编码是FEFE。
GB2312编码范围:A1A1-FEFE,其中汉字的编码范围为B0A1-F7FE,第一字节0xB0-0xF7(对应区号:16-87),第二个字节0xA1-0xFE(对应位号:01-94)。
为什么是从A1开始因为A0是160无论是高位还是地位远远大于127 与-128 不会与ascii冲突
17 0 1 2 3 4 5 6 7 8 9
0 薄 雹 保 堡 饱 宝 抱 报 暴
1 豹 鲍 爆 杯 碑 悲 卑 北 辈 背
2 贝 钡 倍 狈 备 惫 焙 被 奔 苯
3 本 笨 崩 绷 甭 泵 蹦 迸 逼 鼻
4 比 鄙 笔 彼 碧 蓖 蔽 毕 毙 毖
5 币 庇 痹 闭 敝 弊 必 辟 壁 臂
6 避 陛 鞭 边 编 贬 扁 便 变 卞
7 辨 辩 辫 遍 标 彪 膘 表 鳖 憋
8 别 瘪 彬 斌 濒 滨 宾 摈 兵 冰
9 柄 丙 秉 饼 炳
// 1730 转成16进制
看明白上面的编码方式后我们开始计算”本“这个位置是多少。
17转成16进制 0x11 0x11 + 0xA0 =0xB1
30转成16进制 0x1E 0x1E + 0xA0 =0xBE
0xB10xBE
后来中国字符越来越多在GB2312的基础上扩展了GBK 还有少数名族字符 GB18030。
unicode 由来
随着不同的国家会用自己的字符集这样情况下,字符集越来越多,iso(国际标准化组织)认为应该把全世界的所有字符都放在一起,然后进行编号,这样就不需要那么多的字符集了,只要一种就够了,于是出现了unicode字符集(unicode character set) 简称UCS。
一开始使用的是ucs-2字符集, 就是使用两个字节表示一个字符,但是两个字节16位,2^16=65536无法表示所有世界上所以的字符。
| 码位 | 解释 | 编码 |
|---|---|---|
| 0x0000 | 第一个字符 | 0x0000 |
| ⋮ | ⋮ | ⋮ |
| 0xFFFF | 最后一个字符 | 0xFFFF |
所以紧接着出现了ucs-4, 就是用4个字节表示一个字符,这样的话就是32字符,完全满足表示世界上所有的字符。
| 码位 | 解释 | 编码 |
|---|---|---|
| 0x00000000 | 第一个字符 | 0x00000000 |
| ⋮ | ⋮ | ⋮ |
| 0xFFFFFFFF | 最后一个字符 | 0xFFFFFFFF |
这种2^32-1 可以表示43亿个字符,起码因为占用的空间比较大,所以很长的一段时间没有被接受,后来互联网发达了出现了utf-8所以慢慢被接受了。
这里我给大家再放一个图,iso开始对世界上所有的字符进行一一罗列,然后对每个字符都进行编码。
原图请点击这里
我们常说的编码方式就是存储这个字符的方式。比如前面ascii可以用一个字节来存储,这是一种编码方式,比如GB2312表示一个汉字用两个字节,这也是一种编码方式。那么unicode 的编码方式是啥呢?
unicode 的编码方式目前主要有三种:UTF-8、UTF-16、UTF-32。
unicode的编码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016到1016,共计17个平面。
utf-8
utf-8是一种针对unicode 的可变长度字符编码方式。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码。utf-8将unicode 的码位划分为4个区间
| 码点的位数 | 码点起值 | 码点终值 | 字节序列 | Byte1 | Byte2 | Byte3 | Byte4 | Byte5 | Byte6 |
|---|---|---|---|---|---|---|---|---|---|
| 7 | U+0000 | U+00F(127) | 1 | 0xxxxxxxx | |||||
| 8 | U+0080 | U+07FF(2047) | 2 | 110xxxxx | 10xxxxxx | ||||
| 16 | U+0800 | U+FFFF(65535) | 3 | 1110xxxx | 10xxxxxx | 10xxxxxx | |||
| 21 | U+10000 | U+1FFFFF(1114111) | 4 | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
UTF-8的编码规则:
(1) 对于ASCII码中的符号,使用单字节编码,其编码值与ASCII值相同(详见:U0000.pdf)。其中ASCII值的范围为0~0x7F,所有编码的二进制值中第一位为0(这个正好可以用来区分单字节编码和多字节编码)。
(2) 其它字符用多个字节来编码(假设用N个字节),多字节编码需满足:第一个字节的前N位都为1,第N+1位为0,后面N-1 个字节的前两位都为10,这N个字节中其余位全部用来存储Unicode中的码位值。 参考
简单的做一个练习:”我“ unicode对应编码的位置对应的16进制的码位0x6211,这个码位描述的字符”我“。对应的编码是多少我们来用utf-8算一下。
0x6211对应的位置是上述table的U+0800| U+FFFF(65535)范围,
现在我们用转成对应utf-8编码方式是|10xxxxxx|10xxxxxx
用3个字节来表示一个字符. 这是3个字节对应二进制位 00000000 0000000 0000000,对应起来0x6211。最高位就空了下来。
我这样写可能有些人不理解那我稍微解释一下 一个16进制的数最大是F,对应的是16,对应的二进制数是1111.这样可能会好理解一点。
0x6211 对应的二进制位是 0110 0010 0001 0001, 我们将二进制信息从后往前依次插入对应的编码样式中 1110xxxx 10xxxxxx 10xxxxxx
11100110 10001000 10010001 转成16进制
1110 0110 (E6)1000 1000 (88) 1001 0001 (91) => 0xE6 0x88 0x91
这就是utf-8的编码方式,以utf-8存储对应的内存位置就是0xE60x880x91这个位置 下面用python语言来做一个验证演示一下。
讲到这里相信大家应该都理解了什么是uft-8的编码方式。
utf-16
Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。Unicode的编码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016到1016,共计17个平面。基本多语言平面内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。
白话再解释一下这段话,上述GB2312分区,分平面跟这个道理相似,u+xx0000到u+xxFFFF其中的xx表示表示不同平面,00 是16进制转成10进制就是0,10是16进制转成10进制就是16,0到16一共17个数字所以表示不同的平面。不同平面可以有U+0000到U+FFFF也就是 2^16 个字符。一共可以表示的字符就是17*2^16 - 1=1114111个字符。从U+D800到DFFF之间的码点是不在unicode对应的码位之中,所以减去这段0xDFFF-0xD800=0x7FF。转成10进制是2047. 1114112个字符减去2047个字符就还剩1,112,065个码位,[我算的跟维基百科有点不一样] (zh.wikipedia.org/wiki/UTF-16…)
所以unicode排序的时候不会用0xDFFF-0xD800之间的码位,那就要划范围表示了从U+0000至U+D7FF以及从U+E000至U+FFFF的码位。
前置的知识都说的差不多了,下面继续深入一点。给unicode 来进行分平面。
| 平面 | 码位起始 | 码位终止 | 二进制 |
|---|---|---|---|
| 第一平面(基本平面) | U+0000 U+E000 | U+D7FF U+FFFF | 0000 0000 0000 0000 - 1101 0111 1111 1111 1110 0000 0000 0000 - 1111 1111 1111 1111 |
| 第二平面(辅助平面) | U+010000 | U+01FFFF | 0000 0000 0000 0001 0000 0000 0000 0000 - 0000 0000 0000 0001 1111 1111 1111 1111 |
| 第三平面(辅助平面) | U+020000 | U+01FFFF | |
| ⋮ | ⋮ | ⋮ | |
| 第十七平面(辅助平面) | U+0100000 | U+10FFFF |
看了这个table大家应该更能理解了。如果有错误或者不理解的地方请大家留言。utf-16编码在第一平面占了2个字节U+0000 到 U+FFFF),其他平面占用4个字节(U+010000 到 U+10FFFF)。
在辅助平面中的码位,在UTF-16中被编码成了一对16比特长度我码元(32位,4个字节),称为代理对。
码元(Code Unit,也称“代码单元”)是指一个已编码的文本中具有最短的比特组合的单元。对于UTF-8来说,码元是8比特长;对于UTF-16来说,码元是16比特长;对于UTF-32来说,码元是32比特长。码值(Code Value)是过时的用法。
就是用16位二进制数来表示一个unicode的位置。
具体操作方法:
- 码位减去0x100000,得到的值的范围为20位长的0x....0xFFFFF。
这里说一下为什么要减去0x100000? unicode码位 - 0x100000 = 大于等于0|| 小于0,大于等于0说明在其他平面是4个字节表示,小于0说明是在基础平面。
Unicode的最大码位是0x10FFFF,减去0x10000后,U'的最大值是0xFFFF,所以肯定可以用20个二进制位表示。
辅助平面中的码位从U+100000到U+10FFFF,共计FFFFF个,0x10FFFF - 0x10000(BMP) = OxFFFFF(0000 0000 0000 0000 0000)即2^20=1,048,576个,需要20位来表示。看到这里相信大家明白了第一步了吧。 把前10位座位高位,把后10位作为低位。把前10位隐射到0xD800到DBFF(空间大小是2^10),把后10位映射到0xDC00到0xDFFF(2^10),成为低位。
- 高位的10bit值(值的范围是0...0x3FF)被加上0xD800得到第一个码元(高位代理)值的范围0xD800到0xDBFF。由于高位代理比地位代理的值小,所以为了避免混淆使用,Unicode标准在称高位代理为前导(lean surrogates)。
为什么是0到0x3FF呢。10位二进制数的最大值是(11 1111 1111)变成16进制0x3FF。
为什么高位要加上D800?原因是0xDB00到0xFFFF是一个空的字段,这些码点不对应任何字符,因此这些码点可以用来映射辅助平面的字符。 上面也说了也解释了10位二进制最大是1111 1111 11,最小是0000 0000 00 也就是可以表示0x3FF个字符,所以高位是0xD800 然后加上0x3FF,映射区域是0xD800 到 0xDBFF。
- 低位的10比特值(值的范围也是0...0x3FF)被加上0xDC00得到第二个码元(低位代理),现在值的范围就是(0xDC00-0xDFFF)。由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准在称低位代理为后尾代理(trail surrogates)。
0xDC00-0xDFFF为什么是低位?
2^10 -1 = 1023 (16进制3FF)加上0一共是1024位. 现在我们想要再用一共范围表示低位那么我们就要再这个基础上增加1034位,0xDBFF + 0x3FF + 1 = 这样就可以算到低位的最大值是0xDFFF了。至于低字节0xDC00 是怎么来的?就是高位字节的最大范围0xDBFF + 1 = 0xDC00。 希望大家可以理解。
上面把一个辅助平面的字符拆成了两个基本平面字符表示。这样就是4个字节了,大于4个字节我们就要用到高位与低位的代理了。
- 示例 我在Unicode其他平面找一个字符U+1141B,对应HTML的编码是"𑐛"
- 进行编码第一步 0x1141B - 0x10000 = 0x0141B 转为二进制 0000 0001 0100 0001 1011
- 分割高位比特与低位比特 高位:0000 0001 01 (00 0000 0101)0x0005
低位:00 0001 1011 (00 0001 1011)0x001B
- 添加对应的代理位置 0xD800 + 0x0005 = 0xD805
0xDC00 + 0x001B = 0xDC1B
对应的16进制编码就是0xD805 0xDC1B 二进制码是(11011000 00000101 11011100 000110111)
这个示例解释完了也就剩下了其他的一点小知识点了。 我们计算机有不同的操作系统,比如说,linux、window、macos等存储字节的时候比如说window操作系统从左到右读取,高位到低位 拿上面的列子来说,0xD805 0xDC1B,但是macos中就认为D8是低位,要这样写 0x05D8 0x1BDC。所以就出现了大尾序,和小尾序号。用一个表格来表示吧。
| 编码名称 | 编码次序 | 编码 |
|---|---|---|
| UTF-16 LE | 小尾 | 05 D8 1B DC |
| UTF-16 BE | 大尾 | D8 05 DC 1B |
| BOM是用来标记字节的顺序。有数据流开头的字符代码 U+FEFF组成,它可用作定义字节顺序和编码形式的签名,主要是来标记纯文本行。BOM用在唇纹开头非常有用,不清楚文本是大字节序或者小字节序格式的,他也可以作为提示。 | ||
| 如果包含BOM的话会在文本前加入FF FE 小端,FE FF表示大端。 |
| 编码名称 | 编码次序 | 编码 | |
|---|---|---|---|
| BOM | |||
| UTF-16 LE | 小尾 | FF FE | 05 D8 1B DC | |
| UTF-16 BE | 大尾 | FE FF | D8 05 DC 1B |
UTF-32
UTF-32是32位的Unicode转换格式。使用32位的比特位对每个Unicode码位进行编码。UTF-32中的每个32位值可以代表一个Unicode码位,并且与该码位的数值完全一致。
- UTF-32 优点
- 可以直接由Unicode码位来索引
- UTF-32 缺点
- UTF-32的主要缺点是每个码位使用四个字节,空间浪费较多。 UTF-16也是包含大尾序和小尾序的概念。这里可以参考UTF-16不详细叙述了。
JavaScript
JavaScript语言采用Unicode字符集,采用的是UCS-2的方式,用两个字节表示一个字符。所以就JavaScript如果出现一个字符是4个字节会被当做两个双字节处理。在JavaScript中的字符函数也都受到了这点影响。 比如U+10000。 这块知识后续再继续补充吧。
"𐀀".length // 2
"0x10000" === "𐀀" //false
"𐀀".charAt(0) // \ud800
"𐀀".charAt(1) // \udc00
"𐀀".charCodeAt(0) //55296
"𐀀" === '\uD800\uDC00' // true
String.fromCharCode("0x10000") //"𐀀"