字符集只是一个规则集合的名字,对于一个字符集来说要正确编码转码一个字符需要三个关键元素:
code point
来表示一个字符在字库中的位置统一字库表的目的是为了能够涵盖世界上所有的字符,但实际使用过程中会发现真正用的上的字符相对整个字库表来说比例非常低。
例如,中文地区的程序几乎不会需要日语字符,而一些英语国家甚至简单的ASCII字库表就能满足基本需求。而如果把每个字符都用字库表中的序号来存储的话,每个字符就需要3个字节(这里以Unicode字库为例),这样对于原本用仅占一个字符的ASCII编码的英语地区国家显然是一个额外成本(存储体积是原来的三倍)。
Unicode就是编码字符集,而UTF-8就是字符编码,即Unicode规则字库的一种实现形式。
随着互联网的发展,对同一字库集的要求越来越迫切,Unicode标准也就自然而然的出现。它几乎涵盖了各个国家语言可能出现的符号和文字,并将为他们编号。Unicode的编号从0000
开始一直到10FFFF
共分为17个Plane,每个Plane中有65536个字符。而UTF-8则只实现了第一个Plane,可见UTF-8虽然是一个当今接受度最广的字符集编码,但是它并没有涵盖整个Unicode的字库,这也造成了它在某些场景下对于特殊字符的处理困难。
UTF-8编码为变长编码。最小编码单位(code unit)为一个字节。一个字节的前1-3个bit为描述性部分,后面为实际序号部分。
0
,那么代表当前字符为单字节字符,占用一个字节的空间。0之后的所有部分(7个bit)代表在Unicode中的序号110
开头,那么代表当前字符为双字节字符,占用2个字节的空间。110之后的所有部分(5个bit)加上后一个字节的除10外的部分(6个bit)代表在Unicode中的序号。且第二个字节以10开头1110
开头,那么代表当前字符为三字节字符,占用3个字节的空间。110之后的所有部分(5个bit)加上后两个字节的除10外的部分(12个bit)代表在Unicode中的序号。且第二、第三个字节以10开头10
开头,那么代表当前字节为多字节字符的第二个字节。10之后的所有部分(6个bit)和之前的部分一同组成在Unicode中的序号具体每个字节的特征可见下表,其中x代表序号部分,把各个字节中的所有x部分拼接在一起就组成了在Unicode字库中的序号。
Byte 1 | Byte 2 | Byte3 |
---|---|---|
0xxx xxxx | ||
110x xxxx | 10xx xxxx | |
1110 xxxx | 10xx xxxx | 10xx xxxx |
实际字符 | 在Unicode字库序号的十六进制 | 在Unicode字库序号的二进制 | UTF-8编码后的二进制 | UTF-8编码后的十六进制 |
---|---|---|---|---|
$ | 0024 | 010 0100 | 0010 0100 | 24 |
¢ | 00A2 | 000 1010 0010 | 1100 0010 1010 0010 | C2 A2 |
€ | 20AC | 0010 0000 1010 1100 | 1110 0010 1000 0010 1010 1100 | E2 82 AC |
E
开头的C
或D
开头的8
小的数字开头的乱码的出现是因为:编码和解码时用了不同或者不兼容的字符集。一个用UTF-8编码后的字符,用GBK去解码。由于两个字符集的字库表不一样,同一个汉字在两个字符表的位置也不同,最终就会出现乱码。
要从乱码字符中反解出原来的正确文字需要对各个字符集编码规则有较为深刻的掌握。但是原理很简单,这里用最常见的UTF-8被错误用GBK展示时的乱码为例,来说明具体反解和识别过程。
借助MySQL来操作。
mysql> select hex(convert('你好' using utf8));
+-----------------------------------+
| hex(convert('你好' using utf8)) |
+-----------------------------------+
| E4BDA0E5A5BD |
+-----------------------------------+
1 row in set (0.00 sec)
mysql> select hex(convert('你' using utf8));
+--------------------------------+
| hex(convert('你' using utf8)) |
+--------------------------------+
| E4BDA0 |
+--------------------------------+
1 row in set (0.00 sec)
mysql> select hex(convert('好' using utf8));
+--------------------------------+
| hex(convert('好' using utf8)) |
+--------------------------------+
| E5A5BD |
+--------------------------------+
1 row in set (0.00 sec)
中文,“你”的UTF-8编码为E4BDA0
,“好”的UTF-8编码为E5A5BD
。
mysql> select hex(convert('你好' using gbk));
+----------------------------------+
| hex(convert('你好' using gbk)) |
+----------------------------------+
| C4E3BAC3 |
+----------------------------------+
1 row in set (0.00 sec)
mysql> select hex(convert('你' using gbk));
+-------------------------------+
| hex(convert('你' using gbk)) |
+-------------------------------+
| C4E3 |
+-------------------------------+
1 row in set (0.00 sec)
mysql> select hex(convert('好' using gbk));
+-------------------------------+
| hex(convert('好' using gbk)) |
+-------------------------------+
| BAC3 |
+-------------------------------+
1 row in set (0.00 sec)
中文,“你”的GBK编码为C4E3
,“好”的GBK编码为BAC3
。
整理在表格里里看:
中文 | UTF-8 | GBK |
---|---|---|
你 | E4BDA0 | C4E3 |
好 | E5A5BD | BAC3 |
UTF-8对中文使用了三个字节来编码,而GBK只使用了2个字节来编码。
这些都是16进制的表示,所以需要前缀0x
。
# gbk编码的你,用utf-8解码,结果是null
mysql> select convert(0xC4E3 using utf8);
+----------------------------+
| convert(0xC4E3 using utf8) |
+----------------------------+
| NULL |
+----------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select convert(0xC4E3 using GBK);
+---------------------------+
| convert(0xC4E3 using GBK) |
+---------------------------+
| 你 |
+---------------------------+
1 row in set (0.00 sec)
# utf-8编码的你,用gbk解码,结果是null
mysql> select convert(0xE4BDA0 using gbk);
+-----------------------------+
| convert(0xE4BDA0 using gbk) |
+-----------------------------+
| NULL |
+-----------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select convert(0xE4BDA0 using utf8);
+------------------------------+
| convert(0xE4BDA0 using utf8) |
+------------------------------+
| 你 |
+------------------------------+
1 row in set (0.00 sec)
因为一个中文字太短,错误编码的时候就显示了,使用两个中文字,就能看到乱码的效果了。
# utf-8编码的你好,用gbk解码为三个字
mysql> select convert(0xE4BDA0E5A5BD using gbk);
+-----------------------------------+
| convert(0xE4BDA0E5A5BD using gbk) |
+-----------------------------------+
| 浣犲ソ |
+-----------------------------------+
1 row in set (0.01 sec)
# gbk编码的你好,用utf-8解码为null
mysql> select convert(0xC4E3BAC3 using utf8);
+--------------------------------+
| convert(0xC4E3BAC3 using utf8) |
+--------------------------------+
| NULL |
+--------------------------------+
1 row in set, 1 warning (0.00 sec)
Emoji在Unicode位于\u1F601
-\u1F64F
区段的字符。这超过了目前常用的UTF-8字符集的编码范围\u0000
-\uFFFF
。
如何将emoji存入MySQL数据库。
一般MySQL数据库的默认字符集都会配置成UTF-8(三字节),而utf8mb4
在5.5以后才被支持,很少会将系统默认字符集改成utf8mb4。当把一个需要4字节UTF-8编码才能表示的字符存入数据库的时候就会报错:
ERROR 1366: Incorrect string value: '\xF0\x9D\x8C\x86' for column
# 试图将一串Bytes插入到一列中,而这串Bytes的第一个字节是`\xF0`意味着这是一个四字节的UTF-8编码。
当MySQL表和列字符集配置为UTF-8的时候是无法存储这样的字符。
-*-1F601-*-
来替代4字节的Emoji。