聊聊 ASCII、Unicode 和 UTF-8
相信很多程序员对编码既熟悉又陌生,特别是 UTF-8,几乎每天都在打交道,显得格外亲切。但是 UTF-8 到底是个啥玩意儿,还有啥 Unicode、ASCII 等都是什么鬼,不少人也都是懵逼状态,仅仅瞅着眼熟。于是我就打算理一理这三者之间的关系。
ASCII
我们知道,计算机内部使用二进制存储信息的,每一个二进制的位(bit)有 0 和 1 两种状态。八位就被称为一个字节(byte),一个字节有 256(2^8) 种表现状态呢。
于是,上世纪 60 年代,美国就将英语字符和二进制位之间的关系做了统一的规定,被称为 ASCII,全称是 American Standard Code for Information Interchange(美国信息交换标准代码)。ASCII 一共定义了 128 个字符,其中 33 个字符无法显示(都是一些控制字符),剩下的 95 种是可显示的字符,比如英文字母大小写,英文标点符号,数字等。这 128 个字符只占用了一个字节的后 7 位(2^7 等于 128),最前面一位规定为 0。
下面是 128 种 ASCII 字符部分截图(对照表可以看ASCII码对照表)
Unicode
英语中使用 ASCII 编码没啥问题,但是并不能满足其它语言。于是欧洲有些国家就将闲置的最高位利用起来,这样就能表示 256 种字符。问题又来了,256 个符号也不能全部表示各个国家字符,更何况还有汉字这个博大精深的符号。那有没有一种编码,将世界上所有的符号都囊括其中,每个符号都有一个独一无二的编码,岂不是爽歪歪?有,它就是 Unicode(Universal Code)。
Unicode 是指一张表,里面包含了所有可能出现的字符,每个字符对应一个数字,这个数字称为码点(Code Point)。比如“柳”的码点是 67E3(十六进制),大写字母“H”的码点是 0048(十六进制)。Unicode 表包含了 1114112 个码点,即从 000000(十六进制)- 10FFFF(十六进制)。地球上几乎所有的字符都可以在 Unicode 表中找到对应的码点。这个网站可以搜索对应字符的码点。
然后呢,Unicode 将这么多码空间分为 17 个平面(Plane),即 0 - 16,每个平面有 65536(2^16)个码点。如下
其中 0 号平面(码位从 0000 - FFFF,十进制 0 - 65535),包含了最常用的字符,所以该平面被称为基本多语言平面(Basic Multilingual Plane,简称 BMP),其它的平面被称为辅助平面(Supplementary Planes)。
Unicode 的问题
Unicode 虽然包罗万象,但它只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制该如何存储。比如“柳”字的十六进制数(码点)是“67F3”,转换成二进制是“110011111110011”,足足十五位,至少需要两个字节来存储。那其它的字符,可能需要 3 个字节或 4 个字节,甚至更多。
那问题就来了
- 如何区分 Unicode 和 ASCII?计算机如何知道三个字节表示一个字符还是表示三个字符
- 对于英文字母,一个字节就够了,如果 Unicode 规定统一符号用 3 个字节或 4 个字节来表示,那么英文字母前面的字节必然都是 0,这对存储是极大的浪费。
所以急需一个统一的编码方式。
UTF-8
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式(注:UTF-8 是 Unicode 一种实现方式,还有比如 UTF-16,UTF-32等)。
UTF-8 最大的特点,就是它是一种可变的编码方式。它可以使用 1 - 4 个字节来表示一个字符,根据不同的字符变化字节长度。
UTF-8 的编码规则很简单,只有两条:
- 对于单字节的字符,字节的第一位设为 0, 后面 7 位为这个符号的 Unicode 码。因此对于英文字母来说,UTF-8 编码和 ASCII 编码是一样的。
- 对于 n(n > 1) 个字节的字符,第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
编码规则如下
Unicode符号范围 | UTF-8编码方式
(十六进制) (十进制) | (二进制)
--------------------------------------------------------------------------------------------
0000-0000 007F (0-127) | 0xxxxxxx
0080-0000 07FF (128-2047) | 110xxxxx 10xxxxxx
0800-0000 FFFF (2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx
0000-0010 FFFF (65536-1114111) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
根据上表,解读 UTF-8 编码非常简答。如果第一个字节的第一位是 0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个 1,就表示这个字符有多少个字节。
下面以“柳”为例,演示如何进行 UTF-8 编码
“柳”的 Unicode 是 67F3,十进制是 26611,处于上表中第三行的范围。所以“柳”的 UTF-8 编码需要 3 个字节,格式为 1110xxxx 10xxxxxx 10xxxxxx。“柳”的二进制是 110011111110011,从这个二进制的最后一位开始,依次填入格式的 x,多出的位补 0。这样就得到“柳”的 UTF-8 编码是 11100110 10011111 10110011,十六进制为 e69fb3。过程如下
另外,从上表我们也可以看出,第一行到第三行(0000 - FFFF)对应 Unicode 平面中的基本多语言平面,第四行则是辅助平面。也就是说码点小于等于 65535(十进制)的字符,UTF-8 编码需要 1 - 3 个字节,码点大于 65535 则需要 4 个字节。
注:UTF-8 编码格式最开始支持最多 6 个字节,最新的规范只支持最多 4 个字节。
MySQL 中 utf8 和 utf8mb4
MySQL 中的字符集 utf8 最多只支持三字节的字符,也就是 Unicode 平面中的基本多语言平面。由上文可知,真正的 UTF-8 编码是支持最多 4 字节的字符,MySQL 的这个 utf8 其实是个阉割版的 UTF-8。那当时 MySQL 为什么这么规定,可能只是觉得 65535 个字符足够全世界使用吧。如果 MySQL varchar 使用 utf8 作为字符集试,是存不了占 4 个字节的 emoji 表情的。所以后来 5.5.3 版本的 MySQL 推出了 utf8mb4 字符集,也就是可以支持 4 个字节的字符。
另外需要注意的是:
MySQL 中字符串的长度是字符长度而不是字节长度。例如 varchar(20),最多只能存 20 个字符,不管是英文还是中文。
参考
字符编码笔记:ASCII,Unicode 和 UTF-8
【字符编码】彻底理解字符编码
全面了解mysql中utf8和utf8mb4的区别