不管作为一个前端还是后端, 经常能在各种地方遇到关于编码的问题. 这里稍微记录一下关于 Unicode 的理解.
大一的时候就在大机课程上学过这个名字, Unicode 又称万国码, 它的目标是把全世界所有的字符都包含在内, 计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。
Unicode 的编码方式很简单, 就是一一对应.
它从 0 开始, 为每一个符号指定一个唯一的编码, 这个编号就是 码点
. 为了保持兼容性, Unicode 的前 128 位与 ASCII 编码相同.
/u0000 // null
Unicode 中的编码是分区定义的, 每个区存放 65536 (2^16) 个符号, 称之为一个 平面
.
其中第一个平面(0-65535)被称为 基本平面(BMP) , 所有最常见的字符都放在这个平面
/u0000 -> /uFFFF // 基本平面
其余的称之为 辅助平面(SMP) , 码点范围 /u10000
到 /u10FFFF
.
Unicode 只规定了每个字符的码点,也就是映射关系, 但是在机器编码中, 到底用什么样的字节序表示这个码点,就涉及到了不同的编码方法, 例如 UTF8 / UTF16 等
这是最简单的编码方式.
UTF-32 编码对任何的单一 Unicode 符号采用 32 位二进制位 (4字节) 来表示.
比如字符 哈
的 码点为 54c8
(16进制)
/u0000 ==> 00000000 00000000 00000000 00000000 /u54c8 ==> 00000000 00000000 01010100 11001000
这是最常用的编码方式, 网页, 代码中大量使用这种编码. UTF-8 是一种变长的编码方式.
码点位于 0x00 - 0x7f 的字符, 只使用 1 个字节表示, 与 ASCII 完全一致.
码点位于 0x0080 - 0x07ff 的字符, 使用 2 个字节表示.
码点位于 0x0800 - 0xFFFF 的字符, 使用 3 个字节表示.
码点位于 0x010000 - 0x10FFFF 的字符, 使用 4 个字节表示.
对于 单字节的字符 , 编码的第一位为 0, 后七位为该符号的 Unicode 码
'a' => 在 ASCII 编号 97 // 编码 = 01100001
Step1. 编码第一个字节 (8位) 的前 n 位为 1, 第 n + 1 位为 0, 其余字节 (8位) 的头 2 位都是 10Step2. 对于剩下的空位, 使用这个符号的 Unicode 码点填充.
例子. 字符 哈
的码点为 /u54c8
, 码点落在 0x0800 - 0xFFFF
范围内, 所以编码长度为 3 字节
0x54c8
的二进制形式是 101010011001000
00000000 00000000 00000000 // 3 字节 111(00000) 10(000000) 10(000000) // step 1, 括号内为未填充的空位 11100101 10010011 10001000 // step 2, 这就是 '哈' 的UTF8编码形式
UTF-16 编码也非常常见, 它结合了 UTF-32 定长和 UTF-8 不定长的特点.
它的编码规则很简单:基本平面的字符占用2个字节,辅助平面的字符占用4个字节
但是当遇到两个字节并列的情况, 如何判断它是一个辅助平面的字符还是两个基本平面的字符呢?
解决方案: 在 BMP 内, 0xD800
- 0xDFFF
是一个空段, 于是就可以使用这个空段来 映射 SMP 字符.
如何映射呢?SMP 一共有 2^20 个, 把 SMP 字符的码点看做地址, 即至少需要 20 个二进制位来进行映射表示.
那么
前 10 位 (称之为高位 H) 映射在 /uD800
- /uDBFF
之间
后 10 位 (称之为低位 L ) 映射在 /uDC00
- /uDFFF
之间
故一个 SMP 字符, 在 UTF-16 编码中就被拆成了两个 BMP 字符来表示.
所以当遇到一个 /uD800
- /uDBFF
之间的码点, 即可断定它后面的码点是位于 /uDC00
- /uDFFF
之间的, 应该一起解析.
先说结论:UCS-2 是 UTF-16的一个子集, UCS-2 诞生的时候, Unicode 只有基本平面, 所以 UCS-2 编码用 2 字节表示 BMP 内的所有的字符.
或者说 UTF-16 是 UCS-2 的一个超集, 后者完全兼容前者, 所以我们很少听说 UCS-2 编码, 而只听说过 UTF-16 编码, 但是其实, JavaScript 这门语言采用的标准编码是 UCS-2.
由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。JavaScript的字符函数都受到这一点的影响,无法返回正确结果。
比如 :smiling_imp:
这个字符, 就属于 SMP 字符, 它的码点是 0x1f608
十进制是 128520
, UTF-16 编码是 0xd83d 0xde08
由于 JavaScript 标准的 "BUG", 所以 ES2015 之前 JS 是无法直接处理这样的字符的.
var s = ':smiling_imp:'; s.length // 2 s.charCodeAt() // 55357 === 0xd83d 只取了前面2字节的内容
JavaScript 的最新标准 ES2015 修复了这些问题, 并增强了语法, 能够识别 4 字节的 UTF-16 字符.
具体可以到 http://es6.ruanyifeng.com/#docs/string 查看
其实这个编码问题在大多数时候造成的实际 bug 是与 emoji 表情有关的. 因为很大一部分 emoji 是位于 SMP 的字符, 不管是前端还是服务端数据库, 都需要对多字节的字符进行支持才能正确存取. 比如把数据库的编码设为 UTF8-MB4
等.
其实我们完全可以通过前端来解决这个问题.
在 HTML 中有一种特殊的字符被称之为 字符实体. 比如我们经常使用的
这是一种特殊的表示方式, 更标准的方式是 "&#"
+ 十进制 Unicode 码点 + ";"
浏览器会自动解析为该实体具体代表的字符.
例如刚才的 emoji :smiling_imp: 例子, 它的 HTML 字符实体就是
由刚才的 UTF16 编码规则, 我们可以直接对多字节 emoji 进行转换 HTML 字符实体的处理.
var patt = /[/ud800-/udbff][/udc00-/udfff]/g; // 检测utf16字符正则 function utf16toEntities(str) { str = String(str); str = str.replace(patt, function(char) { var H, L, code; if (char.length===2) { H = char.charCodeAt(0); // 取出高位 L = char.charCodeAt(1); // 取出低位 code = (H - 0xD800) * 0x400 + 0x10000 + L - 0xDC00; // 转换算法 return "&#" + code + ";"; } else { return char; } }); return str; }
test
var s = ':smiling_imp:'; utf16toEntities(s); // => "" var div = document.createElement('div'); div.innerHTML = ""; console.log(div.innerHTML) // => ":smiling_imp:"
这样无需后端支持, 就可以直接在前端转换 emoji 表情了.
另外, 在 ES2015 中, 我们可以直接使用 String.prototype.codePointAt()
方法来直接返回十进制的 Unicode 码点.
GBK 全称汉字内码扩展规范, 是天朝官方规定的汉字编码标准, GBK 是 GB2312 的扩展集, 除了 GB2312 中规定的简体中文字符, 它还可以显示繁体中文, 日文等.
GBK 的具体编码规则与本文关系不大, 就不赘述了.
由于 GBK 与 Unicode 是属于两套不同的编码体系, 它们之间并没有什么关系, 这就导致了无法直接通过算法将两者进行转换.
具体业务中, 由于微软推动的关系, 有很多老项目是从前端 后端 数据库都采用的 GBK 编码, 这样就不利于与现有系统结合使用, 甚至影响国际化拓展.
Linux 下有著名的 Iconv 函数库, 它可以用来解决编码转换的问题, 在 PHP 中它是内置的.
echo iconv('GB2312', 'UTF-8', $str); //将字符串的编码从 GB2312 转到 UTF-8
在 Node 下并没有内置这个函数, 我们可以使用 npm 上数以万计的第三方包.
我推荐使用 iconv-lite
这个纯 JS 的包, 它号称比 C++ 的 node-iconv
要快, API 也很简洁:
var iconv = require('iconv-lite'); var str = iconv.decode(buf, 'GBK'); //return unicode string from GBK encoded bytes var buf = iconv.encode(str, 'UTF-8');//return GBK encoded bytes from unicode string
在配合 request
这样的库抓取 GBK 网页的时候, 可以直接传入 Buffer 提高性能
import request from 'request-promise'; import iconv from 'iconv-lite'; const bodyBuffer = await request({ encoding: null // 直接返回 buffer, 不编码成 String }); const bodyUtfStr = iconv.decode(bodyBuffer, 'GBK'); // ...