字符编码学习-ASCII,Unicode,ISO-8859-1等

1. ASCII

  我们知道,计算机内部是通过二进制数据进行操作的,所有的信息最终都会转换为一个二进制值,二进制只有0和1两种状态,因此8个二进制位就可以组合出256种状态,而8个二进制位被称为一个字节(byte),也就是说一个字节可以用来表示256种状态,状态的范围是0000 00001111 1111

  最开始计算机刚出现的时候,计算机的发源地,也就是美国的一群人,通过编码的方式将所有的英文及相应的标点符号,空格,数字等,编到了1个字节的后7位上,也就是一个字节的0-127种状态,范围是0000 00000111 1111,这样当计算机读到对应的二进制值时,就可以解析为相应的符号,然后进行后续的操作,而这种编码方式被称为ASCII编码,所以ASCII码的编码形式也就是 0 XXX XXXX
  该字节的第一位有时候也称为偶校验位,加上二进制编码的剩余7位,恰好是一个字节。ASCII码规定8个二进制位的最高一位是0,剩下的7位可以给出128个编码,表示128个不同的字符。其中95个编码,对应着计算机终端能敲入并且可以显示的95个字符,比如大小写的26个英文字母,数字和标点符号等;另外的33个字符,编码值为0~31和127,他们被用作控制码,控制计算机设备和软件的一些运行情况。

一开始,计算机只在美国使用,所以大家都使用ASCII码来处理英文没什么问题,后来,随着计算机的普及,越来越多的国家都开始使用计算机,而每个国家的语言并不是相同的,这就产生了一个问题,对一些国家来说,常规的ASCII码所表示的128个字符是不够用的,所以就引申出了ISO-8859-1,GB2312等编码。

2. ISO-8859-1

其中,上述ASCII码满足不了的国家就包括欧洲的一些国家,于是这些欧洲国家就决定:

  1. 将自己使用的语言给编码到原先一个字节中闲置的第一位上,范围也就是从 1000 00001111 1111,这样的话,这一套编码系统就可以表示256个符号了,其中0到127和ASCII码表示的符号是相同的,而128到255则分别表示自己国家所使用的符号。
  1. 这其中,就包括了西欧国家的ISO-8859-1,中欧的ISO-8859-2,南欧的ISO-8859-3,北欧的ISO-8859-4 以及相应的阿拉伯,希腊等相关国家的ISO-8859-X系列,其中,ISO-8859-1又名Latin-1,而ISO-8859-2又名Latin-2等,而有关相应的ISO-8859系列的详细说明可参考维基百科:维基百科-ISO/IEC 8859 详细介绍
3. GB2312

  我们把视线转回国内,在20世纪后期计算机在国内开始普及的时候,同样也遇到了相同的问题,并且由于中华文化博大精深,汉字的发展源远流长,光常用的汉字就有几千多个,很显然即使把ASCII码的第一位也用上,也保存不了多少个汉字。所以呢,国内的开发者就整出了一套新的编码方式,不过我们先来看下区位码。

3.1 区位码

  GB2312编码中对汉字进行了“分区”处理,编号为01区至94区;每区含有94个字符,编号为01位至94位。这种表示方式也称为区位码,每一个字符都由与其唯一对应的区号和位号所确定,区位码一般用十进制来表示,其中:

  • 01–09区为符号,数字;
  • 16–55区为一级汉字,约3755个,按汉语拼音字母/笔形顺序排列;
  • 56–87区为二级汉字,约3008个,按部首/笔画排序排列;
  • 10-15区和88-94区 未使用空白区;

举例来说,“啊”字是第16区中的第1个字符,它的区位码就是1601;“琛”字是第72区的第41个字符,它的区位码就是7241。然后我们再来看一下具体的编码方式。

3.2 GB2312编码
  1. 首先,一个字节1到127内的符号不变,和ASCII码保持一致;然后再使用一个字节和前面的那个字节进行连接,通过两个字节来表示一个汉字,其中第一个字节称为高位字节,第二个字节称为低位字节;
    • 高位字节= 区号 + 0xA0
    • 低位字节= 位号 + 0xA0

举例:“啊”的区位码是1601,
高位字节 = 0x10(=16) + 0xA0 = 0xB0
低位字节 = 0x01(=1) + 0xA0 = 0xA1
所以,“啊”的编码就是0xB0(第一个编码单元)0xA1(第二个编码单元),所以最终编码就是B0A1。

不过需要简单说明的是:

  1. 高位字节编码从0xA1到 0xF7(把01–87区的区号加上0xA0),低位字节编码从0xA1到0xFE(把01–94加上0xA0),这样我们就可以组合出大约7000多个简体汉字了。
  2. 在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。
  3. 编码完成之后,不管是高位字节还是低位字节,它们的最高位都是1,这样就可以和ASCII字符的编码区分开,所以说,GB2312是对ASCII码的中文扩展。

另外,区位码这个东西可以理解为一种字符集,而GB2312则可以算作支持该字符集的编码,这样的话其实有点类似于我们后面要说的Unicode和UTF-8的关系。

4. GBK,Big5,GB18030

  GB2312虽然收录了常用的简体中文,但由于汉字实在太多,还包括一些不常用的生僻字及繁体中文,所以GBK就诞生了。GBK是在GB2312的基础上,将生僻字,繁体中文,还有日韩汉字,以及GB2312中不包含的汉字部首符号、竖排标点符号等都收录进去,组成了GBK编码。

  1. GBK总体编码范围是0x8140到0xFEFE,高位字节范围是0x81-0xFE,低位字节范围是0x40-7E和0x80-0xFE,其中低位字节不包括0x7F的组合。

Big5:至于Big5,算是和GB2312同一时期的,是台湾,香港等地流行的主要基于繁体中文的双字节字符集,同样使用了两个字节,第一个字节称为高位字节,第二个字节称为低位字节,高位字节的范围在0x81-0xFE,低位字节的范围在0x40-0x7E以及0xA1-0xFE。要了解更多Big5的,可参考:维基百科-大五码(Big5)

GB18030:而GB18030,则是在GBK的基础上,新收录了少数民族的字符,及GBK不支持的字符。GBK和GB2312都是双字节编码,而GB18030则是变长字节编码,变长字节编码,包含了单字节、双字节和四字节三种方式:

GB18030的单字节编码范围是0x00-0x7F,与ASCII码完全一致;而双字节编码的范围和GBK相同,高字节是0x81-0xFE,低字节的编码范围是0x40-0x7E和0x80-0xFE;四字节编码中第一、三字节的编码范围是0x81-0xFE,第二、四字节的范围是0x30-0x39。

5. Unicode

  由于当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码。当时的中国人想让电脑显示汉字,就必须装上一个”汉字系统”,专门用来处理汉字的显示、输入的问题,装错了字符系统,显示就会乱了套。这怎么办?就在这时,一个叫 ISO (国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它“Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 UNICODE

  1. Unicode 开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 ISO 就直接规定必须用两个字节,也就是16位来统一表示所有的字符,对于 ASCII 里的那些”半角”字符,UNICODE 包持其原编码不变,只是将其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于”半角”英文符号只需要用到低8位,所以其高 8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。
  1. 在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。Unicode的编码范围从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符,Unicode的编码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。
  2. 17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从00到10,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800到U+DFFF之间的码位区块是永久保留不映射到Unicode字符。

Unicode的编码方式是UCS-2,也就是双字节的编码方式,针对的是基本多语言平面(BMP),而相应的辅助平面,则是对应UCS-4。

6. UTF系列
  1. Unicode虽然能覆盖世界上所有的符号,但由于Unicode 在制订时没有考虑与任何一种现有的编码方案保持兼容,这使得Unicode和另一种编码进行转换时,没有什么直接的方式,必须通过查表来进行。所以Unicode 在很长一段时间内都没有得到很大的推广普及。并且由于计算机网络的兴起,Unicode如何在网络上传输也是一个很重要的问题。

这时候,就出现了面向传输的Unicode的实现方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF),其中UTF家族比较出名的就是UTF-8,UTF-16,UTF-32等。

6.1 UTF-8

  UTF-8,是一种变长的编码方式,它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

比如说如果一个仅包含基本7位 ASCII 字符的Unicode符号,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0,这就造成了比较大的浪费。对于这种情况,就可以使用UTF-8编码,因为这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。

UTF-8的编码方式比较简单:

  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的;
  • 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码;

编码规则如下(上面是16进制范围,下面是二进制范围):

Unicode符号范围(16进制) UTF-8编码方式(二进制)
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
Unicode符号范围(二进制) UTF-8编码方式(二进制)
00000000-01111111 0xxxxxxx
00000000 10000000-00000111 11111111 110xxxxx 10xxxxxx
00001000 00000000-11111111 11111111 1110xxxx 10xxxxxx 10xxxxxx
00000001 00000000 00000000 - 00010000 11111111 11111111 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

根据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节,其中中文一般都是在三个字节的范围内。

例如”严”字的Unicode编码是\u4E25。0x4E25在0x0800~0xFFFF之间,所以使用第三行模板 1110xxxx 10xxxxxx 10xxxxxx。将4E25写成二进制是:0100 1110 0010 0101,然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0,得到 11100100 10111000 10100101,也就是 0xE4B8A5 ,这就是严字的UTF8编码。

参考:维基百科-UTF-8

6.2 UTF-16,UTF-32

  UTF-16也是一种变长的编码方式,它可以使用两个字节或四个字节来表示一个符号。其实UTF-16与Unicode的表示方式UCS-2是对应的,在没有辅助平面字符(surrogate code points)前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。

不过UTF-16在表示BMP字符的时候,有一个问题:

  1. 在Mac机和个人PC上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在Mac OS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS环境下打开会显示成“奎”;
  2. 此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian,简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号(BOM)解决方案,目前在PC机上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16 LE。

至于Big-Endian,是第一个字节在前,而Little-Endian,则是第二个字节在前。

而所谓的字节顺序记号,在UTF-16表示为在文件或字符串流的最前面加入一个表示编码顺序的字符,这个字符的名字叫做"零宽度非换行空格"(zero width no-break space),用0xFE 0xFF表示。这正好是两个字节,而且FF比FE大1。

  • 如果一个文本文件的头两个字节是FE FF,就表示该文件采用大端序方式;
  • 如果头两个字节是FF FE,就表示该文件采用小端序方式。

至于UTF-32的编码,则对应于USC-4,也就是4个字节。与其他UTF相比,UTF-32的编码长度是固定的,UTF-32中的每个32位值代表一个Unicode码位,并且与该码位的数值完全一致。

UTF-32的主要优点是可以直接由Unicode码位来索引,相比之下,其他可变长度编码需要进行"循序访问"操作才能在编码序列中找到第N个编码;而UTF-32的主要缺点是每个码位使用四个字节,空间浪费较多。

这里参考自:
维基百科-字节顺序标记
维基百科-UTF-16介绍
维基百科-Unicode介绍

7. emoji表情问题

  该问题是说Emoji表情在保存到Mysql的时候保存失败,提示乱码。

  这里要简单说下Emoji表情了,Emoji表情是一种特殊的字符,而不是像QQ表情一样的普通字符的转义表示。在Unicode编码中,占用了U+1F300到U+1F64F中的部分范围:

  1. Emoji字符的特殊之处在于,其使用的Unicode字符超出了通常使用的三字节UTF-8编码的Unicode范围,即BMP范围U+0000到U+FFFF。按照UTF-8编码规范,Emoji字符属于辅助平面范围,通常对应4字节的UTF-8编码;
  2. 而Mysql的UTF-8最多只支持3个字节来表示一个符号,表示的只是17个平面中的基本多语言面 (Basic Multilingual Plane,BMP)字符,所以保存不了Emoji表情;

所以,这里就引申出了Mysql的utf8mb4字符集(其实就是4字节 UTF-8 Unicode 编码),utf8mb4 字符集使用最多四个字节扩展UTF-8:

  • 对于 BMP字符 UTF8 和 utf8mb4 存储时是完全一样的;
  • 对于其他平面的字符,utf8mb4通过使用四个字节来存储,并且utf8mb4是完全兼容UTF-8,因此从旧版本的MySQL UTF-8 升级数据时 不用担心字符转换或丢失数据;

不过需要注意的是,utf8mb4是mysql在5.5版本之后引入的字符集,并不是诸如UTF-8,UTF-16等通用的字符集。

8. 总结
  1. 可以这么理解,Unicode是字符集,ASCII、GB2312、GBK、GB18030既是字符集也是编码方式,UTF-8只是编码方式;
  2. Unicode从开始到现在一直在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2018年6月5日公布的11.0.0,已经收录超过13万个字符。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。
  3. 一开始,Unicode的2 字节形式通常称作 UCS-2,然而,由于2 字节数量的限制,UCS-2 只能表示最多 65536 个字符,所以后来有了4字节,Unicode 的 4 字节形式被称为 UCS-4 或 UTF-32,能够定义 Unicode 的全部扩展,最多可定义 100 万个以上唯一字符。UCS 只是规定了如何编码,并没有规定如何传输、保存编码,所以有些人说 Unicode编码占用两个字节,这种说法是不准确的。

本文参考自:维基百科
潜行者m-网页编码就是那点事
阮一峰-字符编码笔记:ASCII,Unicode 和 UTF-8
UTF-16与UCS-2的区别
IBM developerWorks-深入分析 Java 中的中文编码问题
《Java核心技术I》

推荐阅读更多精彩内容