本文将简述字符集,字符编码的概念。以及在遭遇乱码时的一些经常使用诊断技巧。前端
背景:字符集和编码无疑是IT菜鸟甚至是各类大神的头痛问题。当遇到纷繁复杂的字符集,各类火星文和乱码时,问题的定位每每变得很是困难。本文就将会从原理方面对字符集和编码作个简单的科普介绍,同时也会介绍一些通用的乱码故障定位的方法以方便读者之后可以更从容的定位相关问题。在正式介绍以前,先作个小申明:若是你但愿很是精确的理解各个名词的解释,那么能够查阅wikipedia。本文是博主经过本身理解消化后并转化成易懂浅显的表述后的介绍。python
什么是字符集
在介绍字符集以前,咱们先了解下为何要有字符集。咱们在计算机屏幕上看到的是实体化的文字,而在计算机存储介质中存放的实际是二进制的比特流。那么在这二者之间的转换规则就须要一个统一的标准,不然把咱们的U盘插到老板的电脑上,文档就乱码了;小伙伴QQ上传过来的文件,在咱们本地打开又乱码了。因而为了实现转换标准,各类字符集标准就出现了。简单的说字符集就规定了某个文字对应的二进制数字存放方式(编码)和某串二进制数值表明了哪一个文字(解码)的转换关系。mysql
那么为何会有那么多字符集标准呢?这个问题实际很是容易回答。问问本身为何咱们的插头拿到英国就不能用了呢?为何显示器同时有DVI,VGA,HDMI,DP这么多接口呢?不少规范和标准在最初制定时并不会意识到这将会是之后全球普适的准则,或者处于组织自己利益就想从本质上区别于现有标准。因而,就产生了那么多具备相同效果但又不相互兼容的标准了。git
说了那么多咱们来看一个实际例子,下面就是屌
这个字在各类编码下的十六进制和二进制编码结果,怎么样有没有一种很屌的感受?github
字符集 | 16进制编码 | 对应的二进制数据 |
---|---|---|
UTF-8 | 0xE5B18C | 1110 0101 1011 0001 1000 1100 |
UTF-16 | 0x5C4C | 1011 1000 1001 1000 |
GBK | 0x8CC5 | 1000 1100 1100 0101 |
什么是字符编码
字符集只是一个规则集合的名字,对应到真实生活中,字符集就是对某种语言的称呼。例如:英语,汉语,日语。对于一个字符集来讲要正确编码转码一个字符须要三个关键元素:字库表(character repertoire)、编码字符集(coded character set)、字符编码(character encoding form)。其中字库表是一个至关于全部可读或者可显示字符的数据库,字库表决定了整个字符集可以展示表示的全部字符的范围。编码字符集,即用一个编码值code point
来表示一个字符在字库中的位置。字符编码,将编码字符集和实际存储数值之间的转换关系。通常来讲都会直接将code point
的值做为编码后的值直接存储。例如在ASCII中A
在表中排第65位,而编码后A
的数值是0100 0001
也即十进制的65的二进制转换结果。sql
看到这里,可能不少读者都会有和我当初同样的疑问:字库表
和编码字符集
看来是必不可少的,那既然字库表中的每个字符都有一个本身的序号,直接把序号做为存储内容就行了。为何还要画蛇添足经过字符编码
把序号转换成另一种存储格式呢?其实缘由也比较容易理解:统一字库表的目的是为了可以涵盖世界上全部的字符,但实际使用过程当中会发现真正用的上的字符相对整个字库表来讲比例很是低。例如中文地区的程序几乎不会须要日语字符,而一些英语国家甚至简单的ASCII字库表就能知足基本需求。而若是把每一个字符都用字库表中的序号来存储的话,每一个字符就须要3个字节(这里以Unicode字库为例),这样对于本来用仅占一个字符的ASCII编码的英语地区国家显然是一个额外成本(存储体积是原来的三倍)。算的直接一些,一样一块硬盘,用ASCII能够存1500篇文章,而用3字节Unicode序号存储只能存500篇。因而就出现了UTF-8这样的变长编码。在UTF-8编码中本来只须要一个字节的ASCII字符,仍然只占一个字节。而像中文及日语这样的复杂字符就须要2个到3个字节来存储。数据库
UTF-8和Unicode的关系
看完上面两个概念解释,那么解释UTF-8和Unicode的关系就比较简单了。Unicode就是上文中提到的编码字符集,而UTF-8就是字符编码,即Unicode规则字库的一种实现形式。随着互联网的发展,对同一字库集的要求愈来愈迫切,Unicode标准也就天然而然的出现。它几乎涵盖了各个国家语言可能出现的符号和文字,并将为他们编号。详见:Unicode on Wikipedia。Unicode的编号从0000
开始一直到10FFFF
共分为16个Plane,每一个Plane中有65536个字符。而UTF-8则只实现了第一个Plane,可见UTF-8虽然是一个当今接受度最广的字符集编码,可是它并无涵盖整个Unicode的字库,这也形成了它在某些场景下对于特殊字符的处理困难(下文会有提到)。编程
UTF-8编码简介
为了更好的理解后面的实际应用,咱们这里简单的介绍下UTF-8的编码实现方法。即UTF-8的物理存储和Unicode序号的转换关系。浏览器
UTF-8编码为变长编码。最小编码单位(code unit
)为一个字节。一个字节的前1-3个bit为描述性部分,后面为实际序号部分。
- 若是一个字节的第一位为0,那么表明当前字符为单字节字符,占用一个字节的空间。0以后的全部部分(7个bit)表明在Unicode中的序号。
- 若是一个字节以110开头,那么表明当前字符为双字节字符,占用2个字节的空间。110以后的全部部分(7个bit)表明在Unicode中的序号。且第二个字节以10开头
- 若是一个字节以1110开头,那么表明当前字符为三字节字符,占用2个字节的空间。110以后的全部部分(7个bit)表明在Unicode中的序号。且第2、第三个字节以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 |
咱们分别看三个从一个字节到三个字节的UTF-8编码例子:
实际字符 | 在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 |
细心的读者不难从以上的简单介绍中得出如下规律:
- 3个字节的UTF-8十六进制编码必定是以
E
开头的 - 2个字节的UTF-8十六进制编码必定是以
C
或D
开头的 - 1个字节的UTF-8十六进制编码必定是以比
8
小的数字开头的
为何会出现乱码
先科普下乱码
的英文native说法是mojibake
简单的说乱码的出现是由于:编码和解码时用了不一样或者不兼容的字符集。对应到真实生活中,就比如是一个英国人为了表示祝福在纸上写了
bless
(编码过程)。而一个法国人拿到了这张纸,因为在法语中bless表示受伤的意思,因此认为他想表达的是受伤
(解码过程)。这个就是一个现实生活中的乱码状况。在计算机科学中同样,一个用UTF-8编码后的字符,用GBK去解码。因为两个字符集的字库表不同,同一个汉字在两个字符表的位置也不一样,最终就会出现乱码。
咱们来看一个例子:假设咱们用UTF-8编码存储很屌
两个字,会有以下转换:
字符 | UTF-8编码后的十六进制 |
---|---|
很 | E5BE88 |
屌 | E5B18C |
因而咱们获得了E5BE88E5B18C
这么一串数值。而显示时咱们用GBK解码进行展现,经过查表咱们得到如下信息:
两个字节的十六进制数值 | GBK解码后对应的字符 |
---|---|
E5BE | 寰 |
88E5 | 堝 |
B18C | 睂 |
解码后咱们就获得了寰堝睂
这么一个错误的结果,更要命的是连字符个数都变了。
如何识别乱码的原本想要表达的文字
要从乱码字符中反解出原来的正确文字须要对各个字符集编码规则有较为深入的掌握。可是原理很简单,这里用最多见的UTF-8被错误用GBK展现时的乱码为例,来讲明具体反解和识别过程。
第1步 编码
假设咱们在页面上看到寰堝睂
这样的乱码,而又得知咱们的浏览器当前使用GBK编码。那么第一步咱们就能先经过GBK把乱码编码成二进制表达式。固然查表编码效率很低,咱们也能够用如下SQL语句直接经过MySQL客户端来作编码工做:
mysql [localhost] {msandbox} > select hex(convert('寰堝睂' using gbk)); +-------------------------------------+ | hex(convert('寰堝睂' using gbk)) | +-------------------------------------+ | E5BE88E5B18C | +-------------------------------------+ 1 row in set (0.01 sec)
第2步 识别
如今咱们获得了解码后的二进制字符串E5BE88E5B18C
。而后咱们将它按字节拆开。
BYTE 1 | BYTE 2 | BYTE 3 | BYTE 4 | BYTE 5 | BYTE 6 |
---|---|---|---|---|---|
E5 | BE | 88 | E5 | B1 | 8C |
而后套用以前UTF-8编码介绍章节中总结出的规律,就不难发现这6个字节的数据符合UTF-8编码规则。若是整个数据流都符合这个规则的话,咱们就能大胆假设乱码以前的编码字符集是UTF-8
第3步 解码
而后咱们就能拿着E5BE88E5B18C
用UTF-8解码,查看乱码前的文字了。固然咱们能够不查表直接经过SQL得到结果:
mysql [localhost] {msandbox} ((none)) > select convert(0xE5BE88E5B18C using utf8); +------------------------------------+ | convert(0xE5BE88E5B18C using utf8) | +------------------------------------+ | 很屌 | +------------------------------------+ 1 row in set (0.00 sec)
常见问题处理之Emoji
所谓Emoji就是一种在Unicode位于\u1F601
-\u1F64F
区段的字符。这个显然超过了目前经常使用的UTF-8字符集的编码范围\u0000
-\uFFFF
。Emoji表情随着IOS的普及和微信的支持愈来愈常见。下面就是几个常见的Emoji:
那么Emoji字符表情会对咱们平时的开发运维带来什么影响呢?最多见的问题就在于将他存入MySQL数据库的时候。通常来讲MySQL数据库的默认字符集都会配置成UTF-8(三字节),而utf8mb4在5.5之后才被支持,也不多会有DBA主动将系统默认字符集改为utf8mb4。那么问题就来了,当咱们把一个须要4字节UTF-8编码才能表示的字符存入数据库的时候就会报错:ERROR 1366: Incorrect string value: '\xF0\x9D\x8C\x86' for column
。 若是认真阅读了上面的解释,那么这个报错也就不难看懂了。咱们试图将一串Bytes插入到一列中,而这串Bytes的第一个字节是\xF0
意味着这是一个四字节的UTF-8编码。可是当MySQL表和列字符集配置为UTF-8的时候是没法存储这样的字符的,因此报了错。
那么遇到这种状况咱们如何解决呢?有两种方式:升级MySQL到5.6或更高版本,而且将表字符集切换至utf8mb4。第二种方法就是在把内容存入到数据库以前作一次过滤,将Emoji字符替换成一段特殊的文字编码,而后再存入数据库中。以后从数据库获取或者前端展现时再将这段特殊文字编码转换成Emoji显示。第二种方法咱们假设用-*-1F601-*-
来替代4字节的Emoji,那么具体实现python代码能够参见Stackoverflow上的回答