引言
我们的CPU只能识别二进制数据 0 和 1,但是计算机怎样呈现给我们缤纷多彩的界面和文字的呢?
答案就是编码。
本文主要把焦点集中在文字的编码上面,对文字的编码意思就是建立数字和文字的关系。例如:汉字中的 你 对应数字 20320。这个对应关系不是凭空捏造的,是根据unicode字符集表查询得到的。相应的我们在计算机上看到的颜色也有与之对应的编码,比如 CSS 用的 24 位色 RGB,这也就是为什么我们在CSS中能够用过#000000来表示对应的颜色。
tips:计算机中
你对应的转义字符序列为 "\304\343\272\303",我们通过键盘打出“你” ----> 操作系统处理为转义字符 ----> 映射为unicode字符 的过程是操作系统完成的。
unicode编码
最初的unicode编码是固定长度的,16位,也就是2两个字节代表一个字符,这样一共可以表示65536个字符。显然,这样要表示各种语言中所有的字符是远远不够的。Unicode4.0规范考虑到了这种情况,定义了一组附加字符编码(下文有详述)。
目前Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。
平面
所谓平面就是范围,将unicode所能表示的字符划分为17个范围。
| 平面 | 范围 | 中文名称 | 英文名称 |
|---|---|---|---|
| 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区) | Private Use Area-A,简称PUA-A |
| 16号平面 | U+100000 - U+10FFFF | 保留作为私人使用区(B区) | Private Use Area-B,简称PUA-B |
0号平面里是我们经常使用的字符,中英文,韩文,日文等都在里面。
1号平面里是用来记录我们不经常使用的古文,例如:爱琴海数字、古希腊数字等。
2号平面用来记录不经常使用的中韩日文字。
Plane 3 到 Plane 14 还没有使用,TIP(Plane 3) 准备用来映射甲骨文、金文、小篆等表意文字。PUA-A, PUA-B 为私人使用区,是用来给大家自己玩儿的——存储自定义的一些字符。
UTF-32, UTF-16 和 UTF-8
在计算机中,存储的只是字符的代码点。而 Unicode 标准只规定了代码点对应的字符,而没有规定代码点怎么存储。于是我们使用UTF-32, UTF-16 或者 UTF-8的方式储存字符。
UTF-32
UTF-32是一种用于编码Unicode的协定,该协定使用32位比特对每个Unicode码位进行编码(但规定前导比特数必须为零,故仅能表示2^21个Unicode码位。2^21所能表示的数据已经足够表示当前已知的所有字符。
UTF-32的主要优点是可以直接由Unicode码位来索引。在编码序列中查找第N个编码是一个常数时间操作。相比之下,其他可变长度编码需要进行循序存取操作才能在编码序列中找到第N个编码。
但是在大多数文本中,非基本文种平面的字符非常罕见,这使得UTF-32所需空间接近UTF-16的两倍和UTF-8的四倍(具体取决于文本中ASCII字符的比例,ASCII字符数量越多,倍数越与上述理论接近)
UTF-16
| 16进制编码范围 | UTF-16表示方法(二进制) | 10进制编码范围 | 字节数量 |
|---|---|---|---|
U+0000 - U+FFFF | xxxx xxxx xxxx xxxx - yyyy yyyy yyyy yyyy | 0-65535 | 2 |
U+10000 - U+10FFFF | 1101 10yy yyyy yyyy - 1101 11xx xxxx xxxx | 65536-1114111 | 4 |
这里提示一下,不是说utf-32就是32个字节表示码位,utf-16就是16个字节表示码位,utf-8就是8个字节表示码位。utf后面的数字表示该编码协议下表示字符的码位最低需要的字节数量。如上表所示,utf-16表示字符最少需要16个字节表示码位。
那么什么时候utf-16需要32个字节表示字符呢?
例如将码位为0x64321的字符使用utf-16表示。64321 > ffff 所以我们不能只使用16个字节了。
计算过程为:
V = 0x64321
Vx = V - 0x10000
= 0x54321
= 0101 0100 0011 0010 0001
Vh = 01 0101 0000 // Vx的高位部份的10 bits
Vl = 11 0010 0001 // Vx的低位部份的10 bits
w1 = 0xD800 //结果的前16位元初始值
w2 = 0xDC00 //结果的後16位元初始值
w1 = w1 | Vh
= 1101 1000 0000 0000
| 01 0101 0000
= 1101 1001 0101 0000
= 0xD950
w2 = w2 | Vl
= 1101 1100 0000 0000
| 11 0010 0001
= 1101 1111 0010 0001
= 0xDF21
计算规则:
如果代码点位于 `0x010000` - `0x10ffff`,则:
1. 代码点减去 `0x10000`,会得到一个位于 `0x000000` 和 `0x0fffff` 之间的数字。
2. 这个数字转换为 20 位二进制数,位数不够的,左边充 0,记作:`yyyy yyyy yyxx xxxx xxxx`。
3. 取出 `yy yyyyyyyy` 与 `11011000 00000000` 进行或运算。
4. 取出 `xx xxxxxxxx` 与 `11011100 00000000` 进行或运算。
5. 将 3&4 计算的记过相连接得到 `110110yy yyyyyyyy 110111xx xxxxxxxx`
6. 将 5 中得到的结果分别取出前十六位和后十六位计算对应的十六进制。
UTF-8
| 代码范围 十六進制 | 二進制 | UTF-8 二进制/十六进制 | 注释 |
|---|---|---|---|
| 000000 - 00007F 128个字符 | 00000000 00000000 0zzzzzzz(7个z) | 0zzzzzzz(00-7F) | ASCII字元范围,位元组由零开始 |
| 000080 - 0007FF 1920个字符 | 00000000 00000yyy yyzzzzzz (6个z,5个y) | 110yyyyy(C0-DF) 10zzzzzz(80-BF) | 第一个位元组由110开始,接着的位元组由10开始 |
| 000800 - 00D7FF 00E000 - 00FFFF 61440个字符 | 00000000 xxxxyyyy yyzzzzzz(4个x,6个y,6个z) | 1110xxxx(E0-EF) 10yyyyyy 10zzzzzz | 第一个位元组由1110开始,接着的位元组由10开始 |
| 010000 - 10FFFF 1048576个字符 | 000wwwxx xxxxyyyy yyzzzzzz(3个w,6个x,6个y,6个z,最多可以表示2^21个码位,对应utf-32中最多也只有2^21个码位) | 11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz | 将由11110开始,接着的位元组由10开始 |
不过巧合的是
-
第一个元组有110开始意味着utf-8解析器需要找两个元组来解析。
-
第一个元组有1110开始意味着utf-8解析器需要找三个元组来解析。
-
第一个元组有11110开始意味着utf-8解析器需要找四个元组来解析。
所以当出现ASCII编码表示不了的字符时就使用多位的方式保存,如下图所示,在第一个字节的前几位表示当前字符的长度,几个‘1’就代表占几个字节。比如第三行那个,程序读取到第一个字节,看到前面有3个‘1’,程序在读取时在向后寻找2两个字节凑够3个字节,在将图中的‘x’位拼到一起读取,就得到了一个完整的字符。
对比&应用
对比
utf-32 编码规则解析字符时时间复杂度几乎是常量级的。但是占用的空间较大。
utf-16 编码规则是一种中和的选择,在16位字节范围内能够表示我们通常用到的字符,与utf-8解析性能比较,空间是有浪费的(取决于ASCII字符占得比例),平均情况下查询时间比utf-8要快一些。与utf-32解析性能比较,空间是有节省的,平均情况下查询时间比utf-32要慢一些。
utf-8 编码规则是我们常用的规则,我们编程中经常使用的字符是A-Z以及a-z或者1-9,如果没有其他特殊字符(注释使用 // 或者 # 对应的unicode码位都小于2^7-1),utf-8对空间占用的是最少的,解析速率是最快的。
应用
Java编程中程序员一般会使用的utf-8字符集。但是JVM使用utf-16字符集。
http协议中,我们一般也使用utf-8字符集。
JS实现
/**
* Encode character or code point to UTF32, UTF16, UTF8
* @param x {String} Use first character
* {Number} Code Point
* @author Liulinwj
*/
function encode(x) {
let cp = typeof x === "string" ? x.codePointAt(0) : Math.floor(x);
if (typeof cp !== "number" || cp < 0 || cp > 0x10FFFF) {
throw new TypeError("Invalid Code Point!");
}
let UTF32LE, UTF32BE, UTF16LE, UTF16BE, UTF8;
if (cp > 0xFFFF) {
UTF32LE = combine(0, cp << 8 >>> 24, cp << 16 >>> 24, cp & 0xFF);
} else {
UTF32LE = combine(0, 0, cp >>> 8, cp & 0xFF);
}
UTF32BE = convertBOM(UTF32LE);
if (cp > 0xFFFF) {
let c = cp - 0x10000;
let sh = (c >>> 10) + 0xD800;
let sl = (c & 0xFFF) + 0xDC00;
UTF16LE = combine(sh >>> 8, sh & 0xFF, sl >>> 8, sl & 0xFF);
} else {
UTF16LE = combine(cp >>> 8, cp & 0xFF);
}
UTF16BE = convertBOM(UTF16LE);
if (cp < 0x80) {
UTF8 = combine(cp);
} else if (cp < 0x800) {
UTF8 = combine((cp >>> 6) | 0xC0, cp & 0x3F | 0x80);
} else if (cp < 0x10000) {
UTF8 = combine(
(cp >>> 12) | 0xE0,
((cp & 0xFC0) >>> 6) | 0x80,
cp & 0x3F | 0x80,
);
} else {
UTF8 = combine(
(cp >>> 18) | 0xF0,
((cp & 0x3F000) >>> 12) | 0x80,
((cp & 0xFC0) >>> 6) | 0x80, cp & 0x3F | 0x80,
);
}
return { UTF32LE, UTF32BE, UTF16LE, UTF16BE, UTF8 };
function combine() {
return [...arguments].map(function(n) {
let hex = n.toString(16).toUpperCase();
return n < 0x10 ? ("0" + hex) : hex;
}).join(" ");
}
function convertBOM(str) {
return str.replace(/(\w\w) (\w\w)/g, "$2 $1");
}
}
此代码的功能: