转载

浅谈前端的 Unicode

浅谈前端的 Unicode 不管作为一个前端还是后端, 经常能在各种地方遇到关于编码的问题. 这里稍微记录一下关于 Unicode 的理解.

啥是 Unicode

大一的时候就在大机课程上学过这个名字, Unicode 又称万国码, 它的目标是把全世界所有的字符都包含在内, 计算机只要支持这一个字符集,就能显示所有的字符,再也不会有乱码了。

Unicode 的编码方式很简单, 就是一一对应.

它从 0 开始, 为每一个符号指定一个唯一的编码, 这个编号就是 码点 . 为了保持兼容性, Unicode 的前 128 位与 ASCII 编码相同.

/u0000 // null 

平面

Unicode 中的编码是分区定义的, 每个区存放 65536 (2^16) 个符号, 称之为一个 平面 .

其中第一个平面(0-65535)被称为 基本平面(BMP) , 所有最常见的字符都放在这个平面

/u0000 -> /uFFFF  // 基本平面 

其余的称之为 辅助平面(SMP) , 码点范围 /u10000/u10FFFF .

编码方式

Unicode 只规定了每个字符的码点,也就是映射关系, 但是在机器编码中, 到底用什么样的字节序表示这个码点,就涉及到了不同的编码方法, 例如 UTF8 / UTF16 等

UTF32

这是最简单的编码方式.

UTF-32 编码对任何的单一 Unicode 符号采用 32 位二进制位 (4字节) 来表示.

比如字符 的 码点为 54c8 (16进制)

/u0000   ==>   00000000 00000000 00000000 00000000 /u54c8   ==>   00000000 00000000 01010100 11001000 
  • 优点: 转换直观, 简单, 查找效率高
  • 缺点: 空间浪费严重

UTF8

这是最常用的编码方式, 网页, 代码中大量使用这种编码. UTF-8 是一种变长的编码方式.

码点位于 0x00 - 0x7f 的字符, 只使用 1 个字节表示, 与 ASCII 完全一致.

码点位于 0x0080 - 0x07ff 的字符, 使用 2 个字节表示.

码点位于 0x0800 - 0xFFFF 的字符, 使用 3 个字节表示.

码点位于 0x010000 - 0x10FFFF 的字符, 使用 4 个字节表示.

对于 单字节的字符 , 编码的第一位为 0, 后七位为该符号的 Unicode 码

'a' => 在 ASCII 编号 97   // 编码 = 01100001 

对于 n (n > 1) 字节的字符

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编码形式   

UTF16 & UCS2

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 之间的, 应该一起解析.

那么 USC-2 又是什么? 跟 UTF-16 有什么关系?

先说结论:UCS-2 是 UTF-16的一个子集, UCS-2 诞生的时候, Unicode 只有基本平面, 所以 UCS-2 编码用 2 字节表示 BMP 内的所有的字符.

或者说 UTF-16 是 UCS-2 的一个超集, 后者完全兼容前者, 所以我们很少听说 UCS-2 编码, 而只听说过 UTF-16 编码, 但是其实, JavaScript 这门语言采用的标准编码是 UCS-2.

Unicode 与 JavaScript

由于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字节的内容   

ES2015

JavaScript 的最新标准 ES2015 修复了这些问题, 并增强了语法, 能够识别 4 字节的 UTF-16 字符.

具体可以到 http://es6.ruanyifeng.com/#docs/string 查看

emoji

其实这个编码问题在大多数时候造成的实际 bug 是与 emoji 表情有关的. 因为很大一部分 emoji 是位于 SMP 的字符, 不管是前端还是服务端数据库, 都需要对多字节的字符进行支持才能正确存取. 比如把数据库的编码设为 UTF8-MB4 等.

其实我们完全可以通过前端来解决这个问题.

HTML 字符实体

在 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、GB2312 转换

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');   // ... 

参考:

  • 阮一峰 - Unicode与JavaScript详解
  • 百度
原文  http://www.zeroling.com/qian-tan-qian-duan-de-unicode/
正文到此结束
Loading...