字符编码那些事儿

目录

一、前言

二、字符集的演变

第一阶段:ASCII字符编码

第二阶段:本地化编码

EASCII字符编码

欧洲的ISO8859系列字符编码

中国的GB系列字符编码

其他本地化编码

第三阶段:UNICODE(国际化)

Unicode字符集

UTF-32编码

UTF-16编码

UTF-8编码

其他UTF

三、乱码的产生

四、总结


一、前言

        众所周知,计算中的数据都是以0和1这样的二进制数字形式存储的。用一位来存储1个0或1,称为bit。8个二进制序列(8个bit)为一个字节(byte)。一个字节为现代计算机计算的最小单位。对于经常使用计算机的我们,面对屏幕上纷繁的文字和emoji表情,脑海中是不是会产生如下问题:

  1. 计算机只能存储0和1两种状态,这些字符是如何被计算机进行处理的呢?

  2. 计算机对于英文和中文的字符处理方式是一样的吗?

  3. 接收到的记事本里的内容怎么就变成了乱码?

        要解决上述的问题,我们需要去了解字符编码。字符编码,顾名思义,就是计算机眼中的字符,字符编码是计算机的基石,了解字符编码可以帮我们揭开计算机处理字符的的神秘面纱。了解编码之前,先来熟悉一些概念:

  • 字符: 是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

  • 字符表: 是一个系统支持的所有字符的集合。

  • 字符集: 又称为编码字符集,是一个包含字符表中每一个字符和对应的整数映射关系的集合。

  • 码位值/代码点/码点: 一个字符在某个字符集中的映射的值,一般是一个整数。

  • 字符编码: 是把一个码点转换为用于存储和传输的二进制序列的方式。

        上面这几个概念如下图所示:

二、字符集的演变

        字符编码的发展是从计算机的诞生开始伴随着互联网的发展一步一步进行演进而来的。可能我们大家都听过“UTF-8”,可你知道UTF-8到底是什么吗,为什么是UTF-8而不是“UTF-32”呢?让我们随着字符编码的发展史,来一一找到这些问题的答案。从计算机字符编码的发展历史角度来看,大概经历了如下三个阶段。

第一阶段:ASCII字符编码

        最初计算机诞生于美国,一开始发明的时候是用来解决数字计算的问题,后来人们发现,计算机还可以做更多的事,例如文本处理。但由于计算机只识“数”,因此人们必须告诉计算机哪个数来代表哪个特定字符。但是计算机之间字符-数字的对应关系必须得一致,否则就会造成同一段数字在不同计算机上显示出来的字符不一样。

        因此,1963年,美国国家标准协会(ANSI)制定了一个标准,规定了常用字符的集合以及每个字符对应的编号(即码点),主要用于显示现代英语。这就是ASCII字符集(Character Set),也称ASCII码,作为计算机及其他设备的文本字符编码标准(ASCII码对照表)。如下图:

        ASCII共定义了128个字符,包括95个可显示字符(阿拉伯数字、小写英文字母、大写英文字母、常用的英文标点符号等)和33个控制字符(回车、退格、换行等特殊的控制功能)。每个字符都有一个对应的数字,叫做码点。ASCII字符的码点为0到127之间的数字,比如A根据ASCII的标准对应码点65。标准所支持的所有字符及其对应码点的集合叫做字符集,所以这个叫做ASCII字符集。

编码方式

        由于ASCII字符集中最大的码点是127,二进制的长度都会小于或者等于7比特,也就是7个0或1。且由于计算机一般以8比特,即1字节为基本单位进行读写。所以为了凑整,这些二进制被储存时会在开头留0,用固定的8比特长度来储存每个字符。这种从字符到计算机能够储存的内容之间的映射叫做编码。具体如下:

字符

码点

二进制码点

编码

NUL

0

0

00000000

1

49

110001

00110001

A

65

1000001

01000001

DEL

127

1111111

01111111

第二阶段:本地化编码

        由于英语语言大多是拼读的,文字是字母构成的,而字母数量非常有限,计算机又是这群人发明的,所以在计算机01转换到人类的语言时,他们有了先天的优势。美国人制定的ASCII编码一共就适用于128个字符,只能用于显示现代美国英语,但其他语言就不够用了。比如,法语带音符的字母、中文汉字、日文片假名等都统统表示不了。所以,在ASCII编码方案的基础上,衍生出了很多不同的字符集和不同的字符编码方案。

EASCII字符编码

        EASCII(Extended ASCII,扩展美国标准信息交换码)是将ASCII码由7位二进制扩充为8位而成。EASCII的内码是由0到255共有256个字符组成。EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号,如下:

        原来的ASCII码二进制表示范围为0000 0000~0111 1111(共128个字符),可以看到最高位都是0,是没有用到的。而新增的EASCII二进制表示范围为1000 0000~1111 1111(十进制为129~255,共128个字符),可以看到最高位位都是1。显然,EASCII码虽与ASCII码一样使用单字节编码,但却可以表示最多256个字符(2^8 = 256),比ASCII的128个字符(2^7=128)多了一倍。

        因此,在EASCII码中,当第一个比特位(即字节的最高位)为0时,仍表示之前那些常用的ASCII字符(实际的二进制编码为0000 0000 ~ 0111 1111,对应的十进制就是0~127),而为1时就表示补充扩展的其他衍生字符(实际的二进制编码为1000 0000 ~ 1111 1111,对应的十进制就是128~255)。这样就在ASCII码的基础上,既保证了对ASCII码的兼容性,又补充扩展了新的字符,于是就称之为Extended ASCII(扩展ASCII)码,简称EASCII码。有时也称为“OEM字体”或“high ASCII”,是互不兼容的众多ASCII扩充字集之一。它使得计算机可读语言的解释器能以较小的开发代价支持众多编码,达到对更多常用字符的需求。

欧洲的ISO8859系列字符编码

        虽然EASCII有了这些更多的字符,解决了部分西欧语言的显示问题,但许多语言还是包含无法压缩到 256 个字符。因此,出现了一些 ASCII 的变体来囊括地区性字符和符号,常见的是欧洲的ISO/IEC 8859字符编码方案。

        该方案与EASCII码类似,也同样是在ASCII码的基础上,利用了ASCII的7位编码所没有用到的最高位(首位),将编码范围从原先ASCII码的0x00~0x7F(十进制为0~127),扩展到了0x80~0xFF(十进制为128~255)。显然,ISO/IEC 8859字符编码方案同样是单字节编码方案,也同样完全兼容ASCII。

        注意,与ASCII、EASCII属于单个独立的字符集不同,ISO/IEC 8859是一组字符集的总称,其下共包含了15个字符集,即ISO/IEC 8859-n,其中n=1,2,3,…,15,16(其中12未定义,所以共15个),如下:

- ISO/IEC 8859-1 (Latin-1) - 西欧语言
- ISO/IEC 8859-2 (Latin-2) - 中欧语言
- ISO/IEC 8859-3 (Latin-3) - 南欧语言。世界语也可用此字符集显示。
- ISO/IEC 8859-4 (Latin-4) - 北欧语言
- ISO/IEC 8859-5 (Cyrillic) - 斯拉夫语言
- ISO/IEC 8859-6 (Arabic) - 阿拉伯语
- ISO/IEC 8859-7 (Greek) - 希腊语
- ISO/IEC 8859-8 (Hebrew) - 希伯来语(视觉顺序)
- ISO/IEC 8859-8-I - 希伯来语(逻辑顺序)
- ISO/IEC 8859-9(Latin-5 或 Turkish)土耳其语。
- ISO/IEC 8859-10(Latin-6 或 Nordic)-北日耳曼语支
- ISO/IEC 8859-11 (Thai) - 泰语
- ISO/IEC 8859-12 本来是预留给印度天城体梵文的,但后来却搁置了
- ISO/IEC 8859-13(Latin-7 或 Baltic Rim)- 波罗的语族
- ISO/IEC 8859-14(Latin-8 或 Celtic)- 凯尔特语族
- ISO/IEC 8859-15 (Latin-9)西欧语言,加入Latin-1欠缺的芬兰语字母和大写法语重音字母,以及欧元(€)符号
- ISO/IEC 8859-16 (Latin-10) - 罗马尼亚语使用,并加入欧元符号

        这15个字符集大致上包括了欧洲各国所使用到的字符。其中,ISO/IEC 8859-1 收录了西欧常用字符(包括德法两国的字母),目前使用得最为普遍,往往简称为ISO 8859-1。

中国的GB系列字符编码

        很明显,中国汉字远远超过128个。当计算机引入中国后,为了显示中文,必须重新设计一套字符集。为了显示中文及相关字符,中国设计了GB系列编码(“GB”为“国标”的汉语拼音首字母缩写,即“国家标准”之意)。

GB2312编码字符集

        新的字符集在1981年由中国国家标准总局发布,取名为GB2312GB2312-80。全称为《信息交换用汉字编码字符集·基本集》。

        GB2312中只收录6763个常用汉字,并对所收字符进行了“分区”处理,每区含有94个字符,共计94个区,即最多可表示94*94(8836个符号)。GB2312字符集片段如下:

01~09区(682个):特殊符号、数字、英文字符、制表符等,包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母等在内的682个全角字符;
10~15区:空区,留待扩展;在附录3,第10区推荐作为 GB 1988–80 中的94个图形字符区域(即第3区字符之半形版本)。
16~55区(3755个):常用汉字(也称一级汉字),按拼音排序;
56~87区(3008个):非常用汉字(也称二级汉字),按部首/笔画排序;
88~94区:空区,留待扩展。

        为了兼容ASCII字符集,GB2312规定一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个GB2312字符。每个字符以两个字节来表示。第一个字节称为“高位字节”,第二个字节称为“低位字节”。

半角和全角

在GB2312中除了包含常用中文字符外,还把数学符号、罗马希腊字母、日文的片假名等都编进去了。连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符。而原来在ASCII中的那些符号就叫"半角"字符了。

编码方式

        “侃”字在表4中显示是57区第0行9列,所以“侃”字的码位(字符集编号)为5709。57的十六进制为0x39,09的十六进制为0x09 。0x39+0xA0=0xD9,0x09+0xA0=0xA9,所以“侃”字的GB2312编码为0xD9A9。

GBK编码字符集

        GB2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但有不少罕用字(如"镕"等)、繁体字、朝鲜语汉字等,并未收录在内。于是中华人民共和国全国信息技术标准化技术委员会利用GB2312未使用的编码空间,对其进行扩展形成了GBK编码(汉字内码扩展规范,K为“扩展”的汉语拼音首字母)。

        GBK共收录21886个汉字和图形符号,其中汉字(包括部首和构件)21003个,图形符号883个,并向下完全兼容GB2312编码。与GB2312类似,GBK中字符有一字节和双字节编码,00–7F范围内是第一个字节,和ASCII保持一致。之后每个字符以两个字节来表示。

GB18030编码字符集

        中国有56个民族,所以再次对GBK编码规则进行了扩展,又加了近几千个少数民族的字符,于是再次扩展后,国家质量技术监督局于2000年3月17日发布了叫做GB18030,全称为国家标准GB 18030-2005《信息技术 中文编码字符集》

        GB18030共收录汉字70244个,与GB2312完全兼容,与GBK基本兼容。GB 18030主要有以下特点:

  • 采用变长多字节编码,每个字可以由1个、2个或4个字节组成。

  • 编码空间庞大,最多可定义161万个字符。

  • 支持中国国内少数民族文字。

  • 汉字收录范围包含繁体汉字以及日韩汉字。

中国港澳台地区的Big5字符编码

        “大五码”(Big5)是由台湾财团法人信息产业策进会为五大中文套装软件所设计的,在1983年12月完成,是使用繁体中文社群中最常用的电脑汉字字符集标准。

        Big5码是一套双字节字符集,共收录13,060个汉字。它使用双八码存储方法,以两个字节来安放一个字。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0x81-0xFE,“低位字节”使用了0x40-0x7E,及0xA1-0xFE。在Big5的分区中:

  • 0x8140-0xA0FE 保留给用户自定义字符(造字区),

  • 0xA140-0xA3BF 标点符号、希腊字母及特殊符号,

  • 包括在0xA259-0xA261,安放了九个计量用汉字:兙兛兞兝兡兣嗧瓩糎。

  • 0xA3C0-0xA3FE 保留。此区没有开放作造字区用。

  • 0xA440-0xC67E 常用汉字,先按笔划再按部首排序。

  • 0xC6A1-0xC8FE 保留给用户自定义字符(造字区)

  • 0xC940-0xF9D5 次常用汉字,亦是先按笔划再按部首排序。

  • 0xF9D6-0xFEFE 保留给用户自定义字符(造字区)

其他本地化编码

        除上述字符集编码之外,还有用于日本的Shift JIS字符集、用于越南的VISCII字符集、用于印度的ISCII字符集等。

ANSI

        ANSI(American National Standards Institute),美国国家标准学会。它是负责制定美国国家标准的非营利组织。为使计算机支持更多的语言,不同的国家和地区制定了不同的标准,并得到了ASNI的认可,全世界在表示对应国家或地区文字的时候都通用的编码就叫ANSI编码。如中国大陆版本ANSI为GB2312,港澳台地区的ANSI为BIG5,美国的ANSI为ASCII,日本的ANSI为JIS等等,各国家或地区各不相同。

        不同的国家或地区,有不同的ANSI编码实现,相互不能兼容。若要显示一份文本,就必须要知道其编码,处理不当就会乱码。

第三阶段:UNICODE(国际化)

        当计算机传到世界各个国家时,为了适合当地语言文字,都会实现各自的一套编码方案。各国和地区在本地使用没有问题,当互联网出现时,各个国家地区相互沟通交流就会出现乱码现象。为了解决这个问题,有两个组织几乎同时开始了统一码的制定,他们分别是:国际标准化组织(ISO)和由Xerox、Apple等软件制造商于1988年组成的统一码联盟(后文简称统一码联盟)。

ISO工作组制定的标准称为通用字符集(Universal Character Set),简称UCS。编码方式:

  • UCS-2,2个字节来表示一个码点;

  • UCS-4,4个字节来表示一个码点(它的范围为 U+00000000~U+7FFFFFFF,其中 U+00000000~U+0000FFFF和UCS-2是一样的。)

统一码联盟制定的标准称为Unicode字符集。编码方式:

  • UTF:Unicode Transformation Format,简称为UTF。常见的如:UTF-8、UTF-16、UTF-32

        1991年前后,两个项目的参与者都认识到,世界上不应该同时存在2个统一码,那就不统一了。所以双方在字符集上做了协调,达成了一致,但是这两个组织以及标准还是分别独立存在的。这是2套标准,但是又互相之间做了基本兼容,大家大概了解即可。目前Unicode大行其道,UCS已经淡出视野,后文仅介绍Unicode。

Unicode字符集

        1991年,Unicode字符集发布,目标在于让世界上每个人都能在电脑上阅读自己的文字。随着版本的迭代,Unicode囊括的字符越来越多,包括汉字、平片假名、藏文、阿拉伯文、甚至古象形文字等,2010年emoji也被纳入Unicode。

Unicode 是一个很大的集合,现在的规模可以容纳100多万个符号,如今已经包含了超过十四万个字符,具体的符号对应表:unicode.org。目前Unicode字符分为17组编排,每组称为平面(Plane)。第一个平面称为基本多语言平面(简称BMP),其他平面称为辅助平面(Supplementary Planes)。而每平面拥有65536(即2^16)个代码点,如下:

平面

始末字符值

中文名称

英文名称

0号平面

U+0000 – U+FFFF

基本多文种平面/基本多语言平面

Basic Multilingual Plane,简称BMP

1号平面

U+10000 – U+1FFFF

多文种补充平面

Supplementary Multilingual Plane,简称SMP

2号平面

U+20000 – U+2FFFF

表意文字补充平面

Supplementary Ideographic Plane,简称SIP

3号平面

U+30000 – U+3FFFF

表意文字第三平面(未正式使用)

Tertiary Ideographic Plane,简称TIP

4号平面至13号平面

U+40000 – U+DFFFF

(尚未使用)

14号平面

U+E0000 – U+EFFFF

特别用途补充平面

Supplementary Special-purpose Plane,简称SSP

15号平面

U+F0000 – U+FFFFF

保留作为私人使用区(A区)

Private Use Area-A,简称PUA-A

16号平面

U+100000 – U+10FFFF

保留作为私人使用区(B区)

Private Use Area-B,简称PUA-B

        Unicode字符集字符集最大码点为U+10FFFF,为什么不是U+FFFFFF 呢,可能人们觉得已经够用了。虽然出现了扩充平面,然而目前只用了少数平面,常用的一些字符还是在BMP这个平面内的。

        但需要注意的是,Unicode字符集只是字符及字符对应码点的集合,它只规定了符号的二进制代码,却没有规定如何以二进制代码形式如何存储,字符编码才是真正定义了从字符到计算机存储内容的映射。所以我们需要UTF来把码点的编码落地,而UTF又有好几个版本。

UTF-32编码

        最简单的编码规则自然就是把字符对应的码点直接以二进制存储在计算机里,之前了解的ASCII编码就是针对ASCII字符集这样做的。但由于Unicode字符集海纳百川,也随之带来一个问题,就是单个字符需要的存储空间更大。像ASCII只有128个字符,8比特以内的二进制数字就足以表示所有字符,但Unicode如今已经囊括了十万多个字符。

        如这个💩emoji,Unicode里对应的十进制码点是128169,二进制为1 1111 0100 1010 1001,整整有17比特。而且还不能让这个二进制数字直接跟在前面数字的屁股后面,因为会分不清每个字符是从哪里到哪里。UTF-32编码将字符对应的Unicode码点都以32比特(即4字节)的长度来储存,位数不够就在前面补0。32比特足够表示Unicode里所有字符。

UTF-32优点

        固定的长度能够帮助计算机明确每个字符的截断范围,且可以直接由Unicode码点来索引,提高效率。

UTF-32缺点

  • 浪费空间。ASCII编码的每个字符只要1字节,现在UTF-32要4字节,这相当于相同内容的英文字符在UTF-32中所占空间会是ASCII 的4倍。而且英文字符的码点在Unicode里数字较小,很多空间都用来放0了。UTF-32也没让汉字使用者占便宜。GBK里一个汉字只占2字节,相当于用UTF-32的话空间会多一倍。

  • UTF-32 存在字节序问题(大端和小端)使用时需要提前协定好。

  • 容错性差,因为存在字节序的问题,如果中间某一个字节丢失时可能会导致后面的解码错误。

  • 不兼容ASCII码。

UTF-16编码

        UTF-16编码长度是不固定的。其规则如下:

  • 如果字符码点在U+0000 ~ U+FFFF范围内,则直接使用两字节表示;

  • 如果字符码点在U+10000 ~ U+10FFFF范围内(该区间共有0xFFFFF个编码,也就是需要20个bit就可以表示这些编码),将其码点减去0x10000,得到的值前10 bit作为高位与0xD800(1101 1000 0000 0000)进行逻辑or操作(相加),后10 bit作为低位与0xDC00(1101 1100 0000 000)做逻辑or操作(相加),这样组成的 4个byte就构成了对应的编码。

16进制编码范围

UTF-16表示方法(二进制)

10进制码范围

字节数量

U+0000~U+FFFF

xxxxxxxx xxxxxxxx yyyyyyyy yyyyyyyy

0-65535

2

U+10000~U+10FFFF

110110yyyyyyyyyy 110111xxxxxxxxxx

65536-1114111

4

 接下来以U+10437编码(𐐷)为例,演示如何实现 UTF-16 编码:

  1. 0x10437 减去 0x10000,结果为0x00437,二进制为 0000 0000 0100 0011 0111

  2. 分割它的上10位值和下10位值(使用二进制):0000 0000 01 和 00 0011 0111

  3. 添加 0xD800 到上值,以形成高位:0xD800 + 0x0001 = 0xD801

  4. 添加 0xDC00 到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37

        下表总结了一些示例的转换过程,颜色指示码点位如何分布在所述的UTF-16中。由UTF-16编码过程中加入附加位的以黑色显示。

字符

普通二进制

UTF-16二进制

UTF-16 十六进制
字符代码

UTF-16BE
十六进制字节

UTF-16LE
十六进制字节

$

U+0024

0000 0000 0010 0100

0000 0000 0010 0100

0024

00 24

24 00

U+20AC

0010 0000 1010 1100

0010 0000 1010 1100

20AC

20 AC

AC 20

𐐷

U+10437

0001 0000 0100 0011 0111

1101 1000 0000 0001 1101 1100 0011 0111

D801 DC37

D8 01 DC 37

01 D8 37 DC

基本思想:

        基本多语言平面(BMP)内,从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区块的码位来对辅助平面的字符的码位进行编码。

        对于码点在U+10000 ~ U+10FFFF范围的字符, 将两个在基本平面内未指定字符的码点进行组合表示一个非基本平面内字符。

大端序(BE)和小端序(LE)

有意思的小故事:

        Endian 本身不是一个英文单词,来自英国作家斯威夫特的《格列佛游记》。书里提到了小人国的战争,战争原因就是吃鸡蛋是从大头磕还是小头磕。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。描述这一段时,作者使用了Endian一词。

        80年代网络协议的一个早期作者在人们关于大端序和小端序的争论时引用了这个单词,从而出现了BE和LE这两个单词。

        不同的计算机系统会以不同的顺序保存字节。这取决于该系统使用的是大端序(big-endian)还是小端序(little-endian)。

Little endian:将低序字节存储在地址低位

Big endian:将高序字节存储在地址低位

        这意味着十六进制编码为U+4E59的字符,在UTF-16编码方式下可能被保存为4E 59(大端序,字符为“乙”)或者59 4E(小端序,字符为“奎”)。

        所以为了弄清楚UTF-16文件的大小端序,在UTF-16文件的开首,都会放置一个U+FEFF字符作为Byte Order Mark(BOM),以显示这个文本文件是以UTF-16编码。

        不同编码的字节序表示:


发散:为什么 UTF-8 不存在字节序的问题?

        首先要搞清楚,不是所有的东西都有字节序,而且字符序是以单字节为单位的顺序问题,不是字节内部的。

        这样问题其实就一目了然了,UTF16处理单元是2个字节或4个字节,UTF32的处理单元分别是4个字节。在C语言中的定义就决定了这两个超过8位的整数需要考虑存储和网络传输的字节序。不定义的话,可能会出现上述4E 59(大端序,字符为“乙”)和59 4E(小端序,字符为“奎”)这样的错误。

        而UTF8的编码是以1个字节为单位处理的,需要考虑下一位时就地址+1,不会受CPU大小端的影响。

UTF-16优点

        由于是固定2个字节和4个字节,所以在计算字符串长度、执行索引操作时速度很快。

UTF-16缺点

  • UTF-16 能表示的字符数有 6 万多,但是实际上目前 Unicode 5.0 收录的字符已经达到 99024 个字符,早已超过 UTF-16 的存储范围。

  • UTF-16 存在字节序问题(大端和小端)使用时需要提前协定好。

  • 容错性差,因为存在字节序的问题,如果中间某一个字节丢失时可能会导致后面的解码错误。

  • 不兼容ASCII码。

UTF-8编码

        为了改善空间效率,拯救字符苍生的UTF-8在1992年诞生。UTF-8是针对Unicode的可变长编码,是在互联网上使用最广的一种 Unicode 的编码实现方式。

        不同于编码后长度固定为32比特的UTF-32,UTF-8针对不同字符编码后的长度可以是32比特、24比特、16比特、8比特。具体规则如下:

(1)码点在0到127范围的字符,直接映射为1字节长度的二进制,第一位补0。

(2)码点在128到2047范围的字符,映射为2字节长度的二进制。UTF-8为了解决我们之前提到的计算机需要能够知道各个字符之间到底在哪里分割,就让二字节编码的第一个字节由110开头,表示自己及后面一个字节是一起的,都在表示同一个字符。然后第二个字节由10开头。Unicode的二进制码点会被分割成两个部分,填入UTF-8编码的数字里。

(3)码点在2048到65535范围的字符,映射为3字节长度的二进制。第一个字节由1110开头,表示自己及后面2个字节是一起的,都在表示同一个字符。然后后面两个字节都由10开头。Unicode的二进制码点会被分割成3个部分,填入UTF-8编码的数字里。

(4)码点在65536到1114111范围的字符,映射为4字节长度的二进制。第一个字节由11110开头,表示自己及后面3个字节是一起的,都在表示同一个字符。然后后面3个字节都由10开头。Unicode的二进制码点会被分割成4个部分,填入UTF-8编码的数字里。

码点范围

二进制码点

存储在计算机为

0-127

0zzzzzzz

0zzzzzzz

128-2047

00000yyy yyzzzzzz

110yyyyy 10zzzzzz

2048-65535

xxxxyyyy yyzzzzzz

1110xxxx 10yyyyyy 10zzzzzz

65536-1114111

000wwwxx xxxxyyyy yyzzzzzz

11110www 10xxxxxx 10yyyyyy 10zzzzz

 以汉字“严”为例,演示如何实现 UTF-8 编码:

        “严”的 Unicode码点 是4E25(100 1110 0010 0101)。根据上表,可以发现4E25处在第三行的范围内,因此其 UTF-8 编码需要三个字节,即格式是1110xxxx 10yyyyyy 10zzzzzz。然后,从其二进制码点最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到其 UTF-8 编码:11100100 10111000 10100101,转换成十六进制就是E4B8A5。

UTF-8优点

  • 兼容ASCII,Unicode前128个字符正好也是ASCII的字符,码点一样。而且UTF-8也会把那些字符映射为1字节长度,和ASCII编码相同。这说明,把一个ASCII编码的英文文本直接用UTF-8编码进行读取,不会有任何问题。

  • 节约空间。UTF-8让Unicode里码点小的字符也相应拥有更短的长度,比UTF-32的一视同仁4字节更节约空间。通过前缀信息也能让计算机辨别各字符在内存里的总长度,解决分割不明这个问题。

  • 容错性强。假如中间缺失去了一个字节,整个文本不会受影响,只会某一处出现错字或者乱码。

UTF-8缺点

        由于它有一些标志位的存在,编码、解码效率相对较低。

其他UTF

已走入历史而很少再被使用的UTF-7、尚未被完整开发的UTF-6和UTF-5等。

三、乱码的产生

你有没有见过这种文件内容,让人梦回火星文年代?

         没在深夜见过乱码的人,不足以语人生。但乱码到底是怎么来的?乱码,指的是由于本地计算机在用文本编辑器打开源文件时,使用了不相应字符集而造成部分或所有字符无法被阅读的一系列字符。可以通过乱码文字生成器体验一下乱码的产生。

乱码神兽“锟斤拷”

        程序猿有个经典的内部笑话:手持两把锟斤拷,口中疾呼烫烫烫。“锟斤拷”是一个常见的中文乱码,常见到有了一定的知名度。它一般在utf-8和中文编码,比如和GBK的转化过程中产生。

Unicode字符集有一个特殊的替换符号,专门用于表示无法识别或展示的字符,如下图:

        有些编辑器在编码为UTF-8时,会把无法识别或展示的字符自动替换为这个替换符号,用于提示用户。它在经过UTF-8编码后,是3字节长度的二进制数字,转换成更简洁的十六进制就是EF BF BD。如果用户在编辑器替换后点了保存,这个特殊符号就会被写入文件内容里。如果有两个替换符号又正好连在一起,内存里就会有这个EF BF BD EF BF BD。

         如果这个时候把文件再用GBK编码读取,或者是把文件发送给GBK编码的朋友,打开后就会看到所有以前是两个替换符号的地方,现在都变成了“锟斤拷”。因为在GBK中,每个汉字用两个字节。储存在计算机里的EF BF对应汉字锟,BD EF对应汉字斤, BF BD对应汉字拷。

        所以,乱码神兽“锟斤拷”,是从UTF-8的两个连在一起的替换符号进化来的,没用的知识又增加了。

四、总结

        在了解编码和乱码后,我们应该都意识到,选择正确编码的重要性。文本编辑器基本都可以指定编码进行读写。html也会通过head里的meta标签表明该网页的编码,从而让浏览器为用户展示出正确的内容。

        需要额外提一下,当使用特定字符编码解析字节流的时候,一旦遇到无法解析的字节流时,就会用乱码来替代。因此,一旦你最终解析得到的文本包含这样的乱码字符,而你又无法得到原始字节流的时候,说明正确的信息已经彻底丢失了,尝试任何字符编码都无法从这样的字符文本中还原出正确的信息来

        作为研发,尽量使用统一的编码。特别是Java开发的,推荐从页面到数据库再到配置文件都使用UTF-8进行编码,安全第一。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注