前言
手持两把锟斤拷,(GBK与UTF-8) 口中疾呼烫烫烫。(VC ) 脚踏千朵屯屯屯,(VC ) 笑看万物锘锘锘。(HTML)
为什么会有锟斤拷、烫烫烫乱码?全角符号和半角符号区别是什么?为什么旧系统的手机收到新emoji表情会显示为���?这些都是编码问题的范畴,相信很多人和我一样,平时在访问网页、打开文档、从数据库读取数据时经常会莫名其妙的出现乱码,不胜其烦,本文从简单的概念出发对编码进行介绍,属于扫盲篇,为本系列的终篇MySQL编码问题做个铺垫。
概念
之前讲浮点数上篇的时候已经提到过:计算机只认识0和1,存储的任何信息都是用二进制表示的,而我们通过计算机看到的、听到的都是二进制数字转换后的结果。实际上本篇文章在计算机存储介质中就是一串0和1表示的数字,因此就需要一套二进制数字和实际显示的字符的转换标准,各类字符集就是不同的转换标准,以下是所涉及的基本定义:
字节:是计算存储容量的一种计量单位,计算机只能识别1和0两个数字,一个数就是1位(bit),为了方便计算,我们规定8位就是一个字节。
字符:字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等,字符和字节一字之差但却是完全是不同的概念,字节是计量单位,字符是符号。存储一个字符所占内存的计量单位是是字节,比如GBK编码中存储一个汉字(即一个字符)需要占用两个字节(16位)。
字库表:字库表是对字符视觉形态描述的集合,简单说字库表描述了每个符号长什么样子,是一系列字符描述按照顺序排列的集合。
字符集:是一个系统支持的所有抽象字符的编码集合,每个编码对应字库表的一个符号的地址(我理解就是序号)。字符集是编码集,是机器里对字符个体描述的集合,而字库是对字符视觉形态描述的集合。
字符编码:字符集中每个字符的编码和字库表中每个字符的存储地址(序号)的对应关系。
举个例子来解释以上概念:每个人相当于一个字符,我们为每个人拍一张照片并且按照姓名的字母序从0开始排序,那么这些照片的集合就是字库表(描述了字符的视觉形态),每个人都有一张身份证(身份证号即编码),所有身份证的集合就是字符集(身份证描述了字符个体),身份证号和字库表序号的对应关系就是字符编码。
读到这里基本的概念介绍完毕了,但是会产生一个疑问:字符编码存在的意义是什么呢?字库表每个字符都有一个编号,字符集每个字符都有一个编码,他们又是一一对应的,为什么不直接使用序号作为字符编码呢?
字符编码存在的意义
在回答上面这个问题之前,我们先来讨论另一个问题:为什么会有ASKII编码、ISO8859、GBK等这么多编码?
答:历史原因。
ASCII码:
世界上第一台计算机诞生于美国,毫无疑问最早产生的编码规范ASCII码是仅支持英文字符的,ASCII码一共包含128个字符,其编码范围是00000000~01111111,可以表示阿拉伯数字、大小写英文字母以及一些简单的符号。可以看出ASCII码一个字符只需要占用1个字节的存储空间,最高位为0。ASCII码没有特定的编码方式,直接使用地址对应的二进制数来表示,即字库表符号序号等于字符集符号编码。
对照表:https://baike.baidu.com/item/ASCII/309296?fr=aladdin
ISO8859系列标准:
英语用128个符号编码就够了,然而随着计算机在各国的普及,世界上语言有几千种,128个符号显然是不够的,上文讲到ASCII码每个符号占了8位,但实际只使用7位,最高位置为0没有使用。于是一些欧洲国家就决定利用字节中闲置的最高位编入新的符号,这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号,这也就是ISO8859标准系列,注意ISO8859是标准系列,不是一个标准,这是因为不同的国家有不同的语言,尽管0~127表示的符号是一样的(和ASCII相同),但128-255所代表的字母各不一样。比如130在法语编码中(代表é)和在希伯来语编码中(代表λ)分别表示不同的符号,因此ISO8859就制定了一系列的标准来为不同的语言编码,这些标准中0~127同ASCII编码,128~255在不同的语言中分别表示不同的字符。
字符集 | 字符集描述 |
---|---|
ISO8859-1 字符集( Latin-1) | 西欧常用字符,包括德、法两国的字母 |
ISO8859-2 字符集( Latin-2) | 东欧字符 |
ISO8859-3 字符集( Latin-3) | 南欧字符 |
ISO8859-4 字符集( Latin-4) | 北欧字符 |
ISO8859-5 字符集( Cyrillic) | 斯拉夫语系字符 |
ISO8859-6 字符集( Arabic) | 阿拉伯语系字符 |
ISO8859-7 字符集( Greek) | 希腊字符 |
ISO8859-8 字符集( Hebrew) | 西伯莱 (犹太人) 字符 |
ISO8859-9 字符集( Latin-5/Turkish) | 土耳其字符 |
ISO8859-10 字符集( Latin-6/Nordic) | 北欧 (主要指斯堪地那维亚半岛) 的字符 |
ISO8859-11 字符集( Thai) | 泰国的 TIS620 标准字符集演化而来 |
ISO8859-12 字符集 | 尚未定义 |
ISO8859-13 字符集( Latin-7) | 波罗的海(Baltic) 诸国以及一些在 Latin-6 中遗漏字符。 |
ISO8859-14 字符集( Latin-8) | Latin-1 中的某些符号换成塞尔特语 (Celtic) 的字符 |
ISO8859-15 字符集( Latin-9) | 被匿称为 Latin-0,它是 Latin-1 的修改版 |
ISO8859-16 字符集( Latin-10) | 东南欧国家语言字符 |
然而在神秘的东方,其语言复杂程度远远高于西方语言,单汉字就10 万,更别提还有日语、韩语等,1个字节最多表示256个字符,是远远不够的,因此必须使用多个字节表达一个符号,也就产生了多字节表达的字符集,比如中文GB类编码,这导致世界上各种编码越发混乱。
Unicode字符集:
Unicode就是上文中提到的字符集,不同的语言都有一套字符编码标准,这在互联网这个开放的环境非常不友好,世界各国对同一字库集的要求越来越迫切,Unicode标准也就自然而然的出现。
Unicode就是为了让全球能用上统一的字符集而发明的。它几乎涵盖了各个国家语言可能出现的符号和文字,并将为他们从U 0000开始一直到U 10FFFF编号,共分为17个Plane,每个Plane中有65536个字符,最常用的是第0平面(中文只收录部分最常用的所以相比GB系列Unicode所表示的中文字符是要少的),每个字符占2个字节。
Unicode虽然是大一统的标准,可以满足世界各国的标准,但是在表示英文时浪费空间,比如英文字母a,ASCII是一个字节表示(01100001)、Unicode则是两个字节表示(0000000001100001),换个说法同样一张内存卡加入使用ASCII编码可以存储100部英文小说,但使用Unicode则只能存储50部小说。并且许多古老的程序只支持ASCII,Unicode从标准设计上来说是不兼容ASCII,这也导致Unicode很长一段时间没有被使用。
需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储,UTF-8是Unicode的实现。
UTF-8编码:
UTF-8编码是变长编码,巧妙的解决了Unicode浪费空间的问题,其编码规则只有二条:
1)对于单字节的符号,字节的最高位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同(兼容)的。
2)对于N字节的符号(N > 1),第一个字节的前N位都设为1,第N 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
UTF-8的字符长度如下表:
UTF-8长度 | Unicode范围 |
---|---|
1个字节 | Unicode码为0 - 127 |
2个字节 | Unicode码为128 - 2047 |
3个字节 | Unicode码为2048 - 0xFFFF |
4个字节 | Unicode码为65536 - 0x1FFFFF |
5个字节 | Unicode码为0x200000 - 0x3FFFFFF |
6个字节 | Unicode码为0x4000000 - 0x7FFFFFFF |
以汉字“中”为例: 1)确认长度:中文字符“中”的Unicode码值为20013,位于2048-0xFFFF的区间,所以占3个字节,其二进制值为1001110 00101101(15位)
2)填充编码:使用3个字节1110xxxx 10xxxxxx 10xxxxxx格式进行补码(16个x),将上面的15位二进制值从右到左填到16个x中(不足位则将x变为0),得到中文字符“中”的UTF-8编码位11100100 10111000 10101101(24位)。
到这里,UTF-8的出现可以很好的回答为什么需要字符编码了,为了在存储的时候省内存。其实原因也比较容易理解:统一字库表的目的是为了能够涵盖世界上所有的字符,但实际使用过程中会发现真正用的上的字符相对整个字库表来说所占比例非常低,例如在中国几乎不会使用日语字符,甚至中国最常使用的汉字也只占所有汉字极少一部分。而如果把每个字符都用字库表中的序号来存储的话,每个字符就需要3个字节(仅指汉字),这样对于原本用仅占一个字符的ASCII编码的英语地区国家显然是一个额外成本(存储体积是原来的4倍)。于是就出现了UTF-8这样的变长编码。在UTF-8编码中原本只需要一个字节的ASCII字符,仍然只占一个字节。而像中文及日语这样的复杂字符就需要2个到3个字节来存储。
有意思的乱码
锟斤拷乱码:
上文介绍Unicode编码时提到Unicode编码并不能包含所有的老编码体系,也就是说存在一些字符是Unicode字符集所没有的,于是Unicode官方就使用一个占位符表示这些文字,这个占位符就是 U FFFD,而这个U FFFD使用UTF-8表示就是“xEFxBFxBD”,如果使用GB系列编码打开,一个汉字占两个字节,原来“xEFxBFxBDxEFxBFxBD”就变成了0xEFBF、0xBDEF、0xBFBD,这三个编码对应的汉字分别是锟斤拷。
烫烫烫/屯屯屯乱码:
windows平台vc带的编译器是ms,这个编译器在 Debug模式下会把未初始化的栈内存使用0XCC填充,未初始化的堆内存全部填成0xCD,一个汉字占两个字节,原来的0xCC0xCC0xCC0xCC就变成0xCCCC、0xCCCC、0xCCCC,0xCCCC对应的用字符就是”烫”,而0xCDCD对应的字符就是“屯”。所以这个错误是由于变量的未初始化导致的。
锘系乱码:
这个发生于HTML页面,先介绍一个概念:BOM是UTF编码方案里用于标识编码标准的标记,FFFE表示UTF-16,EFBBBF表示UTF-8。微软在自己的UTF-8格式的文本文件之前加上了EF BB BF三个字节,Notepad 等程序就是根据这三个字节来确定一个文本文件是ASCII的还是UTF-8的, 然而这个标记只是微软添加的, 其它平台上并没有对UTF-8文本文件做个这样的标记,解析的时候0XEFBB就被解析为锘,剩下的BF使原来的内容依次顺延一个字节,导致乱码。
问号乱码:
这个乱码是我们最常见的,原理很简单,是中文字符经ISO8859-1编码造成的。中文的编码范围超出了ISO8859-1的编码范围,ISO8859-1会将不识别的最编码强制转换为3F,而3F对应的字符就是是“?”,所以中文会全部显示为问号。
总结以上出现乱码原因无非两个:一个是文本写入编码和读出编码不一致;一个是原文写入的时候被添加了额外的标记而读出的时候并没有去掉这部分标记。
全角半角字符:
这部分内容放在这里比较牵强,但是觉得很常见就写出来了,我们平时接触到标点符号分为全角(汉语中标点)和半角(英语标点),在计算机屏幕上,一个汉字要占两个英文字符的位置,虽然大多数字体来说,半角字符的大小看起来是全角字符的一半,但这不是本质区别了,其本质区别是全角是指中GB2312-80(《信息交换用汉字编码字符集·基本集》)中的各种符号,而半角是指英文件ASCII码中的各种符号。
本篇是扫盲篇,仅介绍基本概念,中篇是介绍Unicode和UTF编码,额外介绍了emoji表情的原理,下篇介绍MySQL的编码问题。