夜来风雨声,字符知多少

542 阅读17分钟

前言

字符和字符编码其实是很常见的模块,但在我们学习计算机知识的过程中,这部分都是随便提提,好像很简单但我们又似懂非懂,直到在工作中我们经常遇到各种问题。

所以把整个字符的来龙去脉搞懂对我们有很大的帮助,这也是本篇文章编写的初衷。

问题导入

平常工作中会遇到字符相关的问题,包括但不限于以下几个方面:

  • 长度问题:一个中文字符占2个还是3个字节?同样个数的颜文字或泰语,为什么会比阿拉伯字母占位多?
  • 乱码问题:一个文件在我电脑打开ok,怎么换一台电脑就乱码了?
  • 使用问题:为什么UTF-8是使用得最广泛的?

本文思维导图如下: image.png

编码的背景

我们都知道计算机只认识数字0和1,分别代表电路的关和开。 任何其他字符都要转成0和1才能被计算机识别,这就需要编码。

在讲解具体的编码类型和编码实现前,我们先来看一组基础的名词介绍。

名词解释

  • :bit,二进制位,计算机信息表示的最小单位
  • 字节:一连串比特组成的位串,一般来说1byte = 8bit
  • 编码:信息从一种形式转换为另一种形式的过程,如字符、音频转成二进制
  • 解码:编码的逆过程
  • 字符集:charset,字符的集合。如ASCII字符集、ISO 8859系列字符集(ISO 8859-1~8859-16)、GB系列字符集(GB2312、GBK、GB18030)、BIG5字符集、Unicode字符集
  • 字符编码:编码字符集和实际存储字符的转换关系。如何将文字编号存储到内存中

字符编码关系图

从我们最熟悉的ASCII字符集出发,不同国家和机构在其基础上做了扩展衍生出了很多不同的字符集,接下来将会逐一讲解。

charset.drawio.png

常见的编码类型

ASCII码

ASCII(发音: /ˈæski/ ASS-kee[1],American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母(也成罗马字母)的一套电脑编码系统。ASCII虽然占用一个字节(8bit,256种)表示,然而只规定了128位字符(占字节后7位)的编码,最前面的一位定为0。

最高位置为1的字节,即128-255的部分被不同的国家赋予了不一样的含义,所以不同字符集解析出来的信息是不一样的。 如欧洲对于ASCII的扩展形成了ISO 8859-1 ~ ISO 8859-16 字符集。

扩展的ASCII码:www.ascii-code.com/

缺点: ASCII这种编码只适用于显示26个拉丁字母、阿拉伯数字和键盘上的英式标点符号,是单字节的,世界上还有很多种语言,无法进行表示,于是有了unicode码,它就像一本字典,包含了所有语言的编码。

Unicode是什么?有什么优缺点?

同样的信息,不同的字符集解析结果是不一样的,为了能让信息解析一致,需要有一种编码将所有字符纳入其中,给每一个符号定义一个唯一的编码,于是unicode出现了。Unicode的学名是"Universal Multiple-Octet Coded Character Set",即通用多八位编码字符集,简称为UCS。

关于Unicode平面

目前的Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536(即2^16)个code point(码位)。unicode 已经给 0 ~ 1114111 (十六进制为0x10FFFF)的字符都定义了一个唯一的codePoint。例如©字符被命名为“copyright sign”并且有一个值为U+00A9(0xA9,十进制169)的码位。摘录维基百科的码位范围归类图如下: image.png

关于Unicode辅助平面

辅助平面:除了0号平面的其他16个平面都是辅助平面,可从上图中查看。

码位范围:0x10000 ~ 0x10FFFF。

0号平面内的字符用2个字节(1个UTF-16编码,UTF-16后文会有说明)的码位即可表示,辅助平面的字符需要用到4个字节(1对UTF-16编码,又称代理对),前两个字节称为高位代理,后两个称为低位代理,具体的转换过程为:

高位代理:H = Math.floor((c - 0x10000) / 0x400) + 0xD800

低位代理:L = (c - 0x10000) % 0x400 + 0xDC00

如 code point为 U+2A6A5的字符 会转为 0xD869 0xDEA5。这也是0号平面内 U+D800  ~ U+DFFF 间的字符要作为保留字符使用的原因。 image.png

UCS分类

UCS编码码位范围码位数量
UCS-2U+0000 ~ U+FFFF2^16=65536
UCS-4U+00000000 ~ U+7FFFFFFF2^31=2147483648

unicode —— 大端和小端

  • 大小端是指数据在内存中的字节存放顺序,统称为字节序。
  • 大端:高字节存放到内存的低地址中
  • 小端:高字节存放到内存的高地址中 十六进制数值 0x12345678 的小端字节序和大端字节序的写法如下:

image.png

至于为什么会有大端和小端之分呢?对于 16 位或者 32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节排放的问题,因为不同操作系统读取多字节的顺序不一样,x86和一般的OS(如windows,Linux)使用的是小端模式。Mac OS是大端模式。

(如果一个文本文件的头两个字节是FE FF,就表示该文件采用大端方式;如果头两个字节是FF FE,就表示该文件采用小端方式。) 大小端的作用主要在用于数值计算或比较大小时,具体可见字节序探析:大端和小端的比较

unicode的缺点

unicode虽然解决了不同字符与二进制的对应关系,但这只是对于单个字符的,它没有定义该如何去拆分一段文字,不是所有的符号都只占1个字节,那个当一个符号占有n(2、3、4)个及以上的字节数时,计算机该怎么知道这n个字节表示1个符号,而不是n个字节表示n个符号呢?如果说unicode规定每个符号都占用4个字节,那么对于简单的字母来说,前面都将被填为0,这将会是一个极大的内存浪费。unicode因此无法普及。

UTF-8是什么?有什么优缺点?

针对unicode出现的问题,出现了UTF-8,它不是新的字符集,只是unicode的一种实现方式。 UTF是”UCS Transformation Format“ 的缩写 UTF-8的特点是可变长,根据不同的符号变化字节的长度。 UTF-8 将unicode编码成1~4个码元,每个为8位,所以叫UTF-8,它的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。

2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 unicode 码。

下表总结了编码规则,字母x表示可用编码的位。

unicode符号范围(十六进制)UTF-8编码方式(二进制)
0000 0000-0000 007F0xxxxxxx
0000 0080-0000 07FF110xxxxx 10xxxxxx
0000 0800-0000 FFFF1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

根据上表可知,解析 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。

为什么UTF-8不需要考虑大小端问题?

对于 UTF-8,最小单位是1个字节,因此它是面向字节的。该算法一次读或写一个字节。字节在所有机器上都以相同的方式表示。 对于 UTF-16,最小单位是2个字节,对于 UTF-32,最小单位是4个字节。该算法一次读或写2字节或4字节。在 big-endian 和 little-endian 机器上,每个单词中字节的顺序是不同的。

UTF-16和UTF-32

  • UFT-16 使用 2 个或者 4 个字节(用于表示辅助平面里的字符)来存储。Unicode字符被编码成1至2个码元,每个码元为16位。
  • UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 编号即可,不需要任何编码转换。浪费了空间,提高了效率。UTF-32 与 UTF-16 一样有大端和小端,编码前会放置 U+0000FEFF 或 U+FFFE0000 以区分。
Unicode字符UTF-8编码UTF-16编码UTF-16 BE编码UTF-16 LE编码UTF-32编码UTF-32 BE编码UTF-32 LE编码
\u0041A0x410x00410x00 0x410x41 0x000x000000410x00 0x00 0x00 0x410x41 0x00 0x00 0x00
\u4f9b0xE4 0x‌BE 0x‌9B0‌x4F‌9B0‌x4F‌ 0‌x9B0‌x9B 0‌x4F‌0x00004F9B0x00 0x00 0‌x4F‌ 0‌x9B0‌x9B 0‌x4F‌ 0x00 0x00
\u5e940xE5 0x‌BA 0x‌940‌x5E‌940‌x5E‌ 0‌x940‌x94 0‌x5E‌0‌x00005E940x00 0x00 0‌x5E 0‌x940‌x94 0‌x5E 0x00 0x00
\u94FE0xE9 0x‌93 0x‌BE0‌x94FE0‌x94 0‌xFE0‌xFE 0‌x940‌x000094FE0x00 0x00 0‌x94 0‌xFE0‌xFE 0‌x94 0x00 0x00
\u524d0xE5 0x‌89 0x‌8D0‌x52‌4D0‌x52‌ 0‌x4D0‌x4D 0‌x52‌0‌x0000524D0x00 0x00 0‌x52 0‌x4D0‌x4D 0‌x52 0x00 0x00
\uA8BAจบ0xE0 0xB8 0x88 0xE0 0xB8 0x9A0‌x080E1A0E0‌x08 0x0E 0x1A 0x0E0x0E 0‌x1A 0x0E 0x080‌x0000080E00001A0E0x00 0x00 0‌x08 0‌x0E 0x00 0x00 0‌x1A 0‌x0E0‌x0E 0‌x1A 0x00 0x00 0‌x0E 0‌x08 0000 0000

UCS-2 和 UTF-16的关系

上文说到,UCS-2是用2个字节来表示编码值,只能表示U+0000-U+FFFF之间的符号。例如,在UCS-2和UTF-16中,BMP中的字符U+00A9 copyright sign(©)都被编码为0x00A9。但对于非BMP的字符,例如😂是由\uD83D, \uDE02组成,UTF-16会把它们解释为一个字符,但UCS-2会把它们当成两个字符(UCS-2的时代,U+D800到U+DFFF内的值被占用)。

对比UTF-8UTF-16UTF-32UCS-2UCS-4
编码空间0-10FFFF0-10FFFF0-10FFFF0-FFFF0-7FFFFFFF
最少编码字节数12424
最多编码字节数44424
是否有字节序

如何表示简体中文?GB2312是什么?

GB/T 2312是中华人民共和国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,通常简称GB(“国标”汉语拼音首字母)。共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。(GB2312编码表

GB/T 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但对于人名、古汉语等方面出现的罕用字和繁体字,GB/T 2312不能处理,因此后来GBK及GB 18030汉字字符集相继出现以解决这些问题。

GBK由于GB2312只有6763个汉字,但汉语太多了,在保证和GBK2312以及ASCII码不冲突的前提下又进行了扩充,GBK的k是扩展的意思。还是占2个字节,GBK编码可以表示20902个汉字,这些汉字包含了繁体字。(GBK编码表

GB18130 随着时代发展,GBK两万字也无法满足需求,还有很多的汉字需要进行编码,这时2个字节(最多65536)已经不够,需要用4个字节。目前,GBK18130含有7万多汉字,包括少数民族文字。(GB18030-2005文档GB18030-2000文档

国标码位置布局图如下: image.png

ANSI是什么?

ANSI不是一种特定的字符编码,而是针对不同系统设置的默认编码方式。对于英文文件是ASCII编码,对于简体中文文件是GBK编码,繁体中文版会采用 Big5 码。

image.png

Javascript中的字符和编码

JS中的字符串取length是怎么做的?

image.png 上图的这种情况我们基本都遇到过,但是我们很少去探索为什么会出现这样的现象。探索的第一步肯定是google JS的length到底是怎么工作的,从mdn中可以看到:

该属性返回字符串中字符编码单元的数量。JavaScript 使用 UTF-16 编码,该编码使用一个 16 比特的编码单元来表示大部分常见的字符,使用两个代码单元表示不常用的字符。因此 length 返回值可能与字符串中实际的字符数量不相同。空字符串的 length 为 0。静态属性 String.length 返回 1。

对于大多数字符来说,JS使用了两个字节(作为一个单元)来保存,但是我们知道有些字符用UTF-16表示会需要4个字节,那么该字符就会占用2个单元,造成以上的数量获取和直观感觉不一样的问题。

下面通过一些转换测试来看一下

1. 使用split分割查看每个字符

image.png

2. charCodeAt 和 codePointAt (ECMAScript 6)

str.charCodeAt(index)

charCodeAt() 方法返回 0 到 65535 之间的整数,表示给定索引处的 UTF-16 代码单元。UTF-16 编码单元匹配能用一个 UTF-16 编码单元表示的 Unicode 码点。如果 Unicode 码点不能用一个 UTF-16 编码单元表示(因为它的值大于0xFFFF),则所返回的编码单元会是这个码点代理对的第一个编码单元) 。如果你想要整个码点的值,使用 codePointAt()

形参index:一个大于等于 0,小于字符串长度的整数。如果不是一个数值,则默认为 0。如果 index 超出范围,charCodeAt() 返回 NaN

str.codePointAt(pos)

codePointAt()返回值是在字符串中的给定索引的编码单元体现的数字,如果在索引处没找到元素则返回 undefined 。

API比较:

API返回值概括字符处于基本平面时的返回值字符处于辅助平面时的返回值
charCodeAt 0 到 65535 之间的整数 或 NaNUnicode 码点返回第一个编码单元对应码点
codePointAt字符串中的给定索引的编码单元体现的数字 或 undefined和charCodeAt 返回一致返回整个码点的值

image.png

3. String.prototype.normalize()

The normalize()  method returns the Unicode Normalization Form of the string.

let string1 = '\u00F1';            // ñ
let string2 = '\u006E\u0303';      // ñ

console.log(string1 === string2); // false
console.log(string1.length);      // 1
console.log(string2.length);      // 2
let string1 = '\u00F1';           // ñ
let string2 = '\u006E\u0303';     // ñ

string1 = string1.normalize('NFD');
string2 = string2.normalize('NFD');

console.log(string1 === string2); // true
console.log(string1.length);      // 2
console.log(string2.length);      // 2
4. for infor of

ES6之前 Unicode 只能表示 \u0000 ~ \uFFFF 之间的字符,ES6对超出这个范围的字符进行了支持。 image.png

5. 转化成数组后,可以看到非组合字符可以正确识别字符,组合字符会拆开

image.png

6. 组合字符和零宽字符

前面我们用了🇨🇳图标来举例,将🇨🇳字符展开后得到了'C'、'N'两个字符,下面是字符的合成过程:

image.png

一家四口emoji的合成过程:

image.png

image.png

image.png

隐形的字符:

image.png

零宽字符是计算机中不可打印的字符,上面的\u200d即是一种零宽字符,在JS中可以用于反爬虫

Zero Width Joiner (ZWJ) is a Unicode character that joins two or more other characters together in sequence to create a new emoji.

常见零宽连字:

名称用处示例
零宽空格(zero-width space, ZWSP)用于可能需要换行处Unicode: U+200B;
HTML: ​
零宽不连字 (zero-width non-joiner,ZWNJ)放在电子文本的两个字符之间,抑制本来会发生的连字,而是以这两个字符原本的字形来绘制。Unicode: U+200C;
HTML: ‌
零宽连字(zero-width joiner,ZWJ)一个控制字符,放在某些需要复杂排版语言(如阿拉伯语、印地语)的两个字符之间,使得这两个本不会发生连字的字符产生了连字效果。Unicode: U+200D;
HTML: ‍
左至右符号(Left-to-right mark,LRM)一种控制字符,用于计算机的双向文稿排版中Unicode: U+200E;
HTML: ‎ ‎ ‎
右至左符号(Right-to-left mark,RLM)一种控制字符,用于计算机的双向文稿排版中。Unicode: U+200F;
HTML: ‏ ‏ ‏
字节顺序标记(byte-order mark,BOM)常被用来当做标示文件是以UTF-8、UTF-16或UTF-32编码的标记。Unicode: U+FEFF

为什么返回字符单元而不是字符数量

如果要知道字符数量的话,需要扫描整个字符串然后知道哪些字节组成一个字符,而且现在有组合字符,比如上面的红旗由两个字符构成。而返回字符单元的话,只需要用分配的字符单元除以每个字符单元长度即可,这样可以提高效率。

转码工具

进制转换

编码转换

汉字二进制转换

unicode table

emoji 字典

参考资料:

字符编码笔记:ASCII,Unicode 和 UTF-8

一图弄懂ASCII、GB2312、GBK、GB18030编码

探索emoji🤦🏻‍♂️字符串长度之谜

UTF-16与UCS-2的区别

Why does code points between U+D800 and U+DBFF generate one-length string in ECMAScript 6?