字符集与字符编码

作为一个程序员,很有可能被字符集和字符编码给坑过(如果没有,那可能是业务不同,也可能是时候未到)。最简单的问题,字符集和字符编码有啥关系?我们常常听到的GBK、GB2312、Code page 936到底是什么,它们的关系是什么?再譬如,为什么写爬虫代码时,获取到的是乱码(或者在其他环境下打开Windows下的文本文件会出现乱码)?再譬如UTF-8、UTF-16、UTF-32的关系是什么,它们和Unicode的关系又是什么?

我之前也糊里糊涂,最近这几天看了很多文档(建议直接看Wiki),终于把这个问题给理清了。文章有点长,还请耐心阅读。

1.  概述

1.1 基本概念

字符集(Character set),顾名思义是字符的集合。字符是各种文字和符号的总称,包括文字、标点符号、图形符号、数字、控制符号等。常用的字符集有:ASCII字符集、GB2312字符集、GBK字符集、GB 18030字符集、Big5字符集、Unicode字符集等。 

字符编码(Character Encoding),是字符的编码方式,是将字符集中的字符码映射为字节流的一种具体实现方案。例如ASCII字符编码规定使用单字节中低位的7个比特去编码所有的字符。例如字母’A’的编号是65(ASCII码),用单字节表示就是0x41,因此写入存储设备的时候就是b’01000001’。常用的字符编码方式有ASCII编码、UTF-8编码、GBK编码、Big5编码等。

字符码(Code point):也称编码点,在每个字符集中,每个字符有个编号,称之为字符集编号,该编号就是字符码。Code point(code point or code position is any of the numerical values that make up the code space,该字符在子库表中的位置)。譬如,ASCII字符集中字母’A’的字符码就是65。 

字符集和字符编码是完全不同的概念。字符编码依赖于字符集,没有字符集,字符编码就无意义,也就是说字符集是字符编码的基础。一个字符集可以有多个编码实现,所以有众多的字符编码方式。对于ASCII、GB2312、Big5、GBK,其既是字符集,本身又是字符编码方式,所以会让人很混乱,这个不冲突,后续会详细讲。

1.2 映射关系

1. 字符集与字符编码一对一映射

有很多的字符编码方案,一个字符集只有唯一的编码实现,两者是一一对应的。比如 GB2312,这种情况,无论你怎么去称呼它们,比如“GB2312编码”,“GB2312字符集”,说来说去其实都是一个东西,可能它本身就没有特意去做什么区分,所以无论怎么说都不会错。

为什么一对一是一种普遍的情况呢?

我们以 GB2312 为例,GB(国家标准),标准出来本来就为了统一,你一个标准弄出 N 个编码实现来,你让人家用哪个呢?

2. 字符集与编码一对多映射

事情到了 Unicode 这里,变得不一样了,唯一的 Unicode 字符集对应了三种编码:UTF-8,UTF-16,UTF-32。如果还是这么笼统地去称呼,就很容易搞混了。对于Unicode,还有UCS-2、UCS-4等编码方案,更让人傻傻分不清。

字符集与字符编码

2.  ASCII字符集及编码

ASCII(美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以勉强显示其他西欧语言。它是现今最通用的单字节编码系统,并等同于国际标准ISO/IEC 646。

ASCII字符集:主要包括控制字符(回车键、退格、换行键等);可显示字符(英文大小写字符、阿拉伯数字和西文符号)。

ASCII编码:将ASCII字符集转换为计算机可以接受的数字系统的数的规则。使用7位(bits)表示一个字符,共128字符,ASCII字符集映射到数字编码规则如下图所示:

字符集与字符编码

编码特点:

1. 遵循ASCII字符集,共含有128个字符

2. 使用1个字节存储

ASCII字符集只能显示127个字符,因为只使用了1个字节中的低7Bit,为了扩展,也将最高位也用上,就是EASCII,表示ASCII的扩展: ASCII扩展字符集使用8位(bits)表示一个字符,共256字符,扩展部分如下图所示:

字符集与字符编码

3.  GBXXXX字符集及编码

3.1 GB2312

ASCII字符集中没有汉字,所以中国制定了汉字的字符集GB2312。在国标GB2312-80(1980年颁布)中规定,所有的国标汉字及符号分配在一个94行、94列的方阵中,方阵的每一行称为一个“区”,编号为01区到94区,每一列称为一个“位”,编号为01 位到94位,方阵中的每一个汉字和符号所在的区号和位号组合在一起形成的长度为四的阿拉伯数字就是它们的“区位码”。

国标码(国家标准汉字交换码,也称为交换码):1980年中国制定的用于不同的具有汉字处理功能的计算机系统间交换汉字信息时使用的编码(GB2312),这种编码又称为国标码。

在国标码的字符集*收录了一级汉字3755个,二级汉字3008 个,图形符号682个,三项字符总计7445个。  

一级汉字为常用字,按拼音顺序排列,二级汉字为次常用字,按部首排列。国标码的范围是2121H---7E7EH。

机内码(内码):汉字在计算机汉字系统内部的表示方法,是计算机汉字系统的基础代码。

区位码其实是和ASCII码是有重复的。ASCII码的前32个为控制字符,因此将区位码的每一行、每一列偏移32(0x32)就得到了国标码。但是国标码又和ASCII的普通字符冲突,为了兼容ASCII码,于是将国标码的区及位的字节的高位置为1,也就是加上0x80,得到机内码(内码)。

从上述看,GB2312是不兼容E-ASCII的。

国标码、机内码、区位码的转换关系如下:

1. 国标码(交换码)与区位码

国标码高位字节=(区号)H+20H

国标码低位字节=(位号)H+20H

2. 机内码与区位码

机内码高位字节=(区号)H+A0H (128 + 32)

机内码低位字节=(位号)H+A0H(128 + 32)

3. 机内码和国标码

机内码高位字节=国标码高位字节 + 80H

机内码低位字节=国标码高位字节 + 80H(最高位由0变为1)

 GB2312字符集的编码规则为:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的字母、日文的假名们都编进去了。将在ASCII里本来就有的数字、标点、字母都重新编为两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了

编码特点:

1. 遵循GB2312字符集,支持常见的中文

2. 使用1个字节存储英文和数字等字符,使用2个字节存储中文字符

3. GB2312兼容ASCII

GB2312编码通行于*;新加坡等地也采用此编码。GB2312基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖*99.75%的使用频率。对于人名、古汉语等方面出现的罕用字,GB2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现。列举部分汉字,具体可查看GB2312简体中文编码表:

字符集与字符编码

3.2 GBK

由于GB 2312字符集只收录6763个汉字,有不少汉字,如部分在GB 2312推出以后才简化的汉字,部分人名用字,繁体字,日语及朝鲜语汉字等,并未有收录在内。于是就出现了让众多程序员头疼的GBK(微软提出,其他平台不一定支持)。 

GBK(国标扩展,GuobiaoKuozhan)是GB2312字符集的一个扩展,收录GB 13000.1-93全部字符制定了GBK编码。GBK是对GB2312的扩展,最早实现于Windows 95简体中文版。虽然GBK收录GB 13000.1-93的全部字符,但编码方式并不相同。GBK自身并非国家标准,只是曾由国家技术监督局标准化司、电子工业部科技与质量监督司公布为"技术规范指导性文件"。原始GB13000一直未被业界采用,后续国家标准GB18030技术上兼容GBK而非GB13000。

GBK 共收入 21886 个汉字和图形符号,包括:

1. GB 2312 中的全部汉字、非汉字符号

2. Big5 中的全部汉字

3. 与 ISO 10646 相应的国家标准 GB 13000 中的其它 CJK 汉字,以上合计 20902 个汉字

4. 其它汉字、部首、符号,共计 984 个

GBK 向下与 GB 2312 完全兼容,向上支持 ISO 10646 国际标准,在前者向后者过渡过程中起到的承上启下的作用。

编码特点:

1. 遵循GBK字符集,支持常见的中文,罕见中文,繁体中文,日文的假名。

2. 使用1个字节存储英文和数字等字符,使用2个字节存储中文字符。

3. GBK兼容ASCII与GB2312

GBK 采用双字节表示,总体编码范围为 8140-FEFE 之间,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 XX7F 一条线。 

字符集与字符编码

GBK 编码区分三部分:汉字区、图形符号区、用户自定义区。

汉字区:

1. GBK/2:OXBOA1-F7FE, 收录 GB 2312 汉字 6763 个,按原序排列 

2. GBK/3:OX8140-AOFE,收录 CJK 汉字 6080 个

3. GBK/4:OXAA40-FEAO,收录 CJK 汉字和增补的汉字 8160 个

图形符号区:

1. GBK/1:OXA1A1-A9FE,除 GB 2312 的符号外,还增补了其它符号         

2. GBK/5:OXA840-A9AO,扩除非汉字区

用户自定义区:

GBK 区域中的空白区,用户可以自己定义字符

字符集与字符编码

3.3 GB 18030

GB 18030的全称为:国家标准GB 18030-2005《信息技术 中文编码字符集》,最新的内码字集,是GB 18030-2000《信息技术 信息交换用汉字编码字符集 基本集的扩充》的修订版,完全兼容GB 2312-80,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个。

GB 18030主要有以下特点:

1. 与UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成

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

3. 支持中国国内少数民族的文字,不需要动用造字区。

4. 汉字收录范围包含繁体汉字以及日韩汉字

3.4 代码页

字符内码(character code)指的是用来代表字符的内码,内码分为:

1. 单字节内码(SBCS):Single-Byte character sets,可以支持256个字符编码

2.  双字节内码(DBCS):Double-Byte character sets,可以支持65000个字符编码,主要用来对大字符集的东方文字进行编码

 代码页(Code Page)指的是一个经过挑选的以特定顺序排列的字符内码列表。对于早期的单字节内码的语种,  Code Page中的内码顺序使得系统可以按照此列表来根据键盘的输入值给出一个对应的内码。

由于没有统一的标准,不同IT厂商对于不同的字符编码,采用了不同的数字进行标识。譬如,对于Windows,936代表简体中文,950代表繁体中文。

Windows code page 936(MS936,Windows-936,CP936)是微软针对简体中文设计的字符编码方式。最开始Windows-936支持GB2312(EUN-CN格式,Extended Unix Code for Chinese),但是在Windows95开始,Windows-936也支持了GBK中的大部分字符。

Code page 936已经逐渐被code page 54936(GB18030)取代。

Windows-936,GBK,GB2312、EUC-CN的概念通常给人很混乱的感觉。Code page 936并不等于GBK,它只是实现了GBK字符集中的大部分字符的编码,并且GBK也实现了GBK中没有的字符,如€。

 Windows下查看Code page:chcp 

C:\Users\Administrator>chcp

活动代码页: 936

修改Code Page:chcp 代码页编号         

4.  Big5字符集&编码

Big5,又称为大五码或五大码,是繁体中文最常用的电脑汉字字符集标准,共收录13060个汉字。中文码分为内码及交换码两类,Big5属中文内码,知名的中文交换码有CCCII、CNS11643。Big5虽普及于*、香港与澳门等繁体中文通行区,但长期以来并非当地的国家标准,而只是业界标准。2003年,Big5被收录到CNS11643中文标准交换码的附录当中,取得了较正式的地位。这个最新版本被称为Big5-2003。

Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为"高位字节",第二个字节称为"低位字节"。"高位字节"使用了0x81-0xFE,"低位字节"使用了0x40-0x7E,及0xA1-0xFE。

Big5的分区如下:

字符集与字符编码

5.  Unicode字符集

因为有太多的字符集及字符编码格式,对于交流使用很不方便,尤其涉及到网络时。于是就出现了:Unicode(统一码、万国码、单一码、标准万国码)。

Unicode字符集涵盖了目前人类使用的所有字符,并为每个字符进行统一编号,分配唯一的字符码(Code Point)。Unicode字符集将所有字符按照使用上的频繁度划分为17个层面(Plane),每个层面上有216=65536个字符码空间。

其中第0个层面BMP,基本涵盖了当今世界用到的所有字符。其他的层面要么是用来表示一些远古时期的文字,要么是留作扩展。我们平常用到的Unicode字符,一般都是位于BMP层面上的。目前Unicode字符集中尚有大量字符空间未使用。

字符集与字符编码

Unicode 是基于通用字符集(Universal Character Set,UCS)的标准来发展。UCS是由ISO制定的ISO 10646标准所定义的标准字符集。历史上存在两个独立的尝试创立单一字符集的组织,即国际标准化组织(ISO)和多语言软件制造商组成的统一码联盟。前者开发的 ISO/IEC 10646 项目(UCS),后者开发的统一码项目(Unicode)。因此最初制定了不同的标准。

1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都存在,并独立地公布各自的标准。但统一码联盟和ISO/IEC JTC1/SC2都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。

虽然每个字符在Unicode字符集中都能找到唯一确定的编号(字符码,code point),但是决定最终字节流的却是具体的字符编码。Unicode字符集的编码方式主要为:UTF-32,UTF-16,UTF-8,其中UTF表示:Unicode  Transformation Format,而UCS字符集的编码方式主要是UCS-2,UCS-4。

Unicode的code point分为17个平面(plane),每个平面包含65536个code point,而第一个平面称为“基本多语言平面”(Basic Multilingual Plane,简称BMP),其余平面称为“辅助平面”(Supplementary Planes)。其中“基本多语言平面”(0~0xFFFF)中0xD800~0xDFFF之间的code point作为保留,未使用。 

6.  UTF编码

Unicode的编码方式是UTF(Unicode Transformation Format),UTF分为UTF-8,UTF-16,UTF-32等编码方式。

6.1. UTF-32编码

UTF-32以4个字节编码一个Unicode字符,不管该字符有没有用到4个字节。因此,该方式很浪费空间,但是对于处理器而言,操作却很友好(字节对齐)。

在Unicode与ISO 10646合并之前,ISO 10646标准为“通用字符集”(UCS)定义了一种31位的编码形式(即UCS-4),其编码固定占用4个字节,编码空间为0x00000000~0x7FFFFFFF(可以编码20多亿个字符)。

UCS-4有20多亿个编码空间,但实际使用范围并不超过0x10FFFF,并且为了兼容Unicode标准,ISO也承诺将不会为超出0x10FFFF的UCS-4编码赋值。由此UTF-32编码被提出来了,它的编码值与UCS-4相同,只不过其编码空间被限定在了0~0x10FFFF之间。因此也可以说:UTF-32是UCS-4的一个子集。

6.2. UTF-16编码

UTF-16是变长编码,使用2字节或4字节进行编码:它使用两个字节为全世界最常用的63K字符编码,它使用4个字节对不常用的字符进行编码。

编码特点:

1. 遵循Unicode字符集

2. 使用2个字节存储各种语言的常用字符,使用4个字节存储其他罕见字符

3. UTF-16并不兼容ASCII或者ISO8859-1,原因是UTF-16使用2个字节表示英文和西欧字符

 对于BMP“基本多语言平面”中的字符, UTF-16直接使用Unicode的code point作为编码值。例:“清”在Unicode中的code point为0x6E05,而在UTF-16编码也为0x6E05。

UTF-16中如何对“辅助平面”进行编码呢?

Unicode的code point区间为0~0x10FFFF,除“基本多语言平面”外,还剩0xFFFFF个code point(并且其值都大于或等于0x10000)。对于“辅助平面”内的字符来说,如果用它们在Unicode中code point值减去0x10000,则可以得到一个0~0xFFFFF的区间(该区间中的任意值都可以用一个20-bits的数字表示)。该数字的前10位加上0xD800,就得到UTF-16四字节编码中的前两个字节;该数字的后10位加上0xDC00,就得到UTF-16四字节编码中的后两个字节。 

字符集与字符编码

高2字节范围:0xD800-0xDBFF

低2字节范围:0xDC00-0xDFFF 

以U+10437为例,讲述UTF-16以四字节编码的情况。

1. 将U+10437减去0x10000,得到0x0437

2. 高2字节:将0x0437右移10位(也即除以0x400),得到0x0001,然后加上0xD800,得到0xD801

3. 低2字节:将0x0437左移10位(除以0x400后的余数),得到0x0037,然后加上0xDC00,得到0xDC37

4. 得到U+10437的UTF-16的编码值:0xD801DC37

从UTF-16编码值0xD801DC37解码得到U+10347:

1. 将高位0xD801减去0xD800,然后乘以0x400,得到0x0400

2. 将低位0xDC37减去0xDC00,得到0x37

3. 将上述两步的结果相加得到0x0437,最终加上0x10000得到0x10437,就是其UTF-32的code point

UTF-16与UCS-2的关系:

UTF-16由UCS-2发展而来,只不过后续增加了4字节的编码方式。UCS-2是以固定的2字节编码,UTF-16以2字节或4字节进行编码,对于2字节的部分,UTF-16和UCS-2相同。

6.3. UTF-8编码

UTF-32是以4字节表示字符,UTF-16是以2(或4字节)字节表示字符,那么UTF-8就是以1字节(或2字节或4字节)表示字符咯,哪里有这么简单?

编码特点:

1. 遵循Unicode字符集

2. 使用1个字节存储英文和数字等字符,使用2-3个字节存储常用中文,使用4个字符存储罕见字符

3. UTF-8兼容ASCII,但不兼容UTF-16,因为UTF-8存储字符所需的空间与UTF-16是不一样的

4. UTF-8默认不带BOM,最好也不要加上BOM

如何编码:

1. 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

字符集与字符编码
UTF-8:一字节编码

 

2. 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码。

对于Binary code point栏中的不同颜色和Binary UTF-8中对应颜色的值是相同的。只不过是个填空题:将Code point的二进制值依次填入Binary UTF-8的空余部分中。不同字节编码的UTF-8的空余部分不一致,具体如下图:

字符集与字符编码
UTF-8:二字节编码

 

字符集与字符编码
UTF-8:三字节编码

 

字符集与字符编码
UTF-8:四字节编码

如U+20AC,为3字节的字符,按照第二点的叙述(先考虑规定的那部分):n为3,那么第一个字节的前3位都为1,第4位为0,第二字节和第三字节的前两位为10,那么填充如下:

字符集与字符编码

U+20AC的二进制为:100000 10101100

将100000 10101100填入上述空格中:

字符集与字符编码

汉字“清”的Unicode字符集中的code point为0x6E05,其UTF-8的编码为:0xE6B885

解码规则:

1. 如果一个字节的第一位为0,那么代表当前字符为单字节字符,占用一个字节的空间。0之后的所有部分(7个bit)代表在Unicode中的序号

2. 如果一个字节以10开头,那么代表当前字节为多字节字符的第二个字节。10之后的所有部分(6个bit)和之前的部分一同组成在Unicode中的序号

3. 如果一个字节以110开头,那么代表当前字符为双字节字符,占用2个字节的空间。110之后的所有部分(5个bit)加上后一个字节的除10外的部分(6个bit)代表在Unicode中的序号,且第二个字节以10开头

4. 如果一个字节以1110开头,那么代表当前字符为三字节字符,占用2个字节的空间。第二、第三个字节以10开头,110之后的所有部分(5个bit)加上后两个字节的除10外的部分(12个bit)代表在Unicode中的序号

5. 如果一个字节以11110开头,那么代码当前字符为四字节字符,占用4个字节空间,第二、三、四字节以10开头

 UTF-8越来越得到普及,其趋势图如下:

字符集与字符编码

6.4.  Unicode编码范围

不同范围的Code point所对应的Unicode编码方式所需字节数如下图所示:

字符集与字符编码

7.  BOM(Byte order mark)

BOM全称是Byte order mark,也即字节序。Unicode 规范定义,每一个文件的最前面分别加入一个表示编码顺序的字符。Unicode标准允许在UTF-8中使用BOM,但是不推荐。

BOM的作用:

1. 指明字节序是大端还是小端

2. 文件是以Unicode进行编码的

3. 使用的是哪种Unicode编码方式:UTF-8、UTF-16还是UTF-32

7.1 大端及小端

对于汉字“清”,在Unicode字符集的page point 为0x6E05,存储时需要两个字节。如果6E在前,05在后(高字节在左,低字节在右),叫做大端(Big endian),如果6E在后,05在前(低字节在左,高字节在右),叫做小端(Little endian)。

关于左,右,其实是以存储空间而言的。假如0x6E05在内存中的位置是0x10000000,靠近1的位置称为左(低地址);远离1的位置称为右(低地址)。

那么大端的定义就变为:高字节在低地址,低字节在高地址;小端的定义就变为:高字节在高地址,低字节在低地址

7.2 UTF-8

在UTF-8中,BOM通过0xEF,0xBB,0xBF来标识,但是不推荐使用。字节序在UTF-8中没有意义,只是用于表明文本流是以UTF-8编码的或者是由包含可选BOM的流中转换而来。

以汉字“清”为例(在Unicode字符集中的code point为 0x6E05,二进制位01101110 00000101),讲述UTF-8的编码格式。用Windows的Notepad以UTF-8编码另存为文件“清-UTF8.bin”(只包含汉字“清”,在编码方式栏选择UTF-8),随后用UltraEdit打开文件(Ctrl+H显示16进制):

字符集与字符编码

可见Notepad默认在UTF-8编码时添加了BOM。

 通过Notepad++以“UTF-8 without BOM”方式另存为文件“清-UTF-8-withoutBOM.bin”,然后用UltraEdit打开:

字符集与字符编码

此时,文件中没有BOM。

7.3 UTF-16

在UTF-16中,有两种BOM:一个是U+FEFF,一个是U+FFFE:

1.  如果文本流是大端序,那么BOM就是0xFE及0xFF两字节

2.  如果文本流是小端序,那么BOM就是0xFF及0xFE两字节

UTF-16中的BOM可以指明文本流是以UTF-16编码的,还可以指明其是大端还是小端。

依然以汉字“清”为例,讲述UTF-16下的大小端编码。

清在UTF-16的大端编码格式如下:

字符集与字符编码

清在UTF-16的小端编码格式如下:

字符集与字符编码

 Windows Notepad中的Unicode编码是:UTF-16小端。

字符集与字符编码

UTF-16分为大端及小端存储:BOM为0x FE FF时为大端;BOM为0x FF FE时为小端。

7.4 UTF-32

UTF-32中可以使用BOM,但很少使用。UTF-32的小端BOM和UTF-16的小端BOM一致,只不过后续增加了两个NULL字符;UTF-32的大端BOM和UTF-16的大端BOM一致,只不过在之前增加了两个NULL字符。

UTF-32分为大端及小端存储:BOM为0x00 00 FE FF时为大端;BOM为0x FF FE 00 00时为小端。

不同Unicode编码方式的BOM如下图:

字符集与字符编码

8.  ANSI

ANSI严格来说并不是一种编码规则,它表示:根据当前操作系统以及操作系统的语言,选择对应的编码规则进行编码。例如,对于简体中文的Windows操作系统,ANSI代表GBK。在繁体中文Windows操作系统中,ANSI代表Big5。在日文Windows操作系统中,ANSI代表Shift_JIS 编码。

依然以汉字“清”为例,通过Notepad保存为ANSI。通过UltraEdit打开,结果如下:

字符集与字符编码

我们看到,汉字“清”的16进制值编码值为0xC7E5,0xC7E5正好是汉字“清”的GBK编码。可见,Windows默认以大端方式存储。

9.  读取文本文件

对于一个Unicode编码保存的文本文件,如果在保存时指定了BOM头,那么通过BOM信息就可以确定是通过UTF-8、UTF-16还是UTF-32编码所得。在解析时,通过相应的编码格式,通过读取的字节还原Code point,然后在Unicode字符集中查找改code point对应的字符并显示。示例代码如下:

#include <string.h>   /* memcmp() */



const char *UTF_16_BE_BOM =   "\xFE\xFF";

const char *UTF_16_LE_BOM =   "\xFF\xFE";

const char *UTF_8_BOM =   "\xEF\xBB\xBF";

const char *UTF_32_BE_BOM =   "\x00\x00\xFE\xFF";

const char *UTF_32_LE_BOM =   "\xFF\xFE\x00\x00";



char* check_bom(const char *data, size_t   size)

{

      if (size >= 3) {

          if (memcmp(data, UTF_8_BOM, 3) == 0)

            return "UTF-8";

      }

      if (size >= 4) {

          if (memcmp(data, UTF_32_LE_BOM, 4) == 0)

            return "UTF-32-LE";

          if (memcmp(data, UTF_32_BE_BOM, 4) == 0)

            return "UTF-32-BE";

      }

      if (size >= 2) {

          if (memcmp(data, UTF_16_LE_BOM, 2) == 0)

            return "UTF-16-LE";

          if (memcmp(data, UTF_16_BE_BOM, 2) == 0)

            return "UTF-16-BE";

      }

      return NULL;

}

但是如果该文本文件中没有BOM头,文本编辑器(如Notepad++)如何正确显示其内容?因为没有指定BOM,文本编辑器不知道其是哪种Unicode编码方式,所以这个时候需要做的事情就是去猜。当然这个猜也不是毫无目的乱猜。

1. 逐字节判断

2. 多个字节一起判断

最朴素的做法:

1. 首先假定是UTF-8编码,那么根据第一字节值的大小确定该字符是1字节、2字节、3字节还是4字节编码。确定编码字节数后,读取相应的字节,进而还原其code point,在Unicode字符集中就可以找到对应的字符。如果读取的值不满足UTF-8的特点(第一字节后的字节范围不满足UTF-8的要求),那么就不是UTF-8编码的。

#include <stdint.h>



int isUTF8(const char *data, size_t size)

{

      const unsigned char *str = (unsigned char*)data;

      const unsigned char *end = str + size;

      unsigned char byte;

      unsigned int code_length, i;

      uint32_t ch;

      while (str != end) {

          byte = *str;

          if (byte <= 0x7F) {

            /* 1 byte sequence:   U+0000..U+007F */

            str += 1;

            continue;

          }



          if (0xC2 <= byte && byte <= 0xDF)

            /* 0b110xxxxx: 2 bytes sequence   */

            code_length = 2;

          else if (0xE0 <= byte && byte <= 0xEF)

            /* 0b1110xxxx: 3 bytes sequence   */

            code_length = 3;

          else if (0xF0 <= byte && byte <= 0xF4)

              /* 0b11110xxx: 4 bytes   sequence */

            code_length = 4;

          else {

            /* invalid first byte of a   multibyte character */

            return 0;

          }



          if (str + (code_length - 1) >= end) {

            /* truncated string or invalid   byte sequence */

            return 0;

          }



          /* Check continuation bytes: bit 7 should be set, bit 6 should be

           * unset (b10xxxxxx). */

          for (i=1; i < code_length; i++) {

            if ((str[i] & 0xC0) != 0x80)

                  return 0;

          }



          if (code_length == 2) {

            /* 2 bytes sequence:   U+0080..U+07FF */

            ch = ((str[0] & 0x1f)   << 6) + (str[1] & 0x3f);

            /* str[0] >= 0xC2, so ch >=   0x0080.

               str[0] <= 0xDF, (str[1]   & 0x3f) <= 0x3f, so ch <= 0x07ff */

          } else if (code_length == 3) {

            /* 3 bytes sequence:   U+0800..U+FFFF */

            ch = ((str[0] & 0x0f)   << 12) + ((str[1] & 0x3f) << 6) +

                  (str[2] & 0x3f);

            /* (0xff & 0x0f) << 12   | (0xff & 0x3f) << 6 | (0xff & 0x3f) = 0xffff,

               so ch <= 0xffff */

            if (ch < 0x0800)

                return 0;



            /* surrogates (U+D800-U+DFFF) are   invalid in UTF-8:

                 test if (0xD800 <= ch   && ch <= 0xDFFF) */

            if ((ch >> 11) == 0x1b)

                return 0;

          } else if (code_length == 4) {

            /* 4 bytes sequence:   U+10000..U+10FFFF */

            ch = ((str[0] & 0x07)   << 18) + ((str[1] & 0x3f) << 12) +

                 ((str[2] & 0x3f)   << 6) + (str[3] & 0x3f);

            if ((ch < 0x10000) ||   (0x10FFFF < ch))

                return 0;

          }

          str += code_length;

      }

      return 1;

}

2. 如果假定是UTF-16编码,那么每两个字节进行判断(由于UTF-16的编码范围以code point 0x10000 为界,低于0x10000的code point以2字节进行编码,高于0x10000的以四字节编码。)

根据读取的最开始的两个字节的范围进行判断该字符的code point是否是小于0x10000 ,如果最开始的两字节范围在U+0000 to U+D7FF and U+E000 to U+FFFF,那么是用两字节进行编码;如果读取的最开始的两字节的范围在0xD800–0xDBFF 或0xDC00–0xDFFF,表示是用四字节编码,并且还可以判断出是大端还是小端

3. 如果是UTF-32编码,将字符的code point 转化为16进制(对于不足四字节的字符,会填NULL(根据大端还是小端,NULL会填在前面或后面))就得到该字符的UTF-32编码。

ASCII:字符都小于0x80,判断代码如下:

int isASCII(const char *data, size_t   size)

{

      const unsigned char *str = (const unsigned char*)data;

      const unsigned char *end = str + size;

      for (; str != end; str++) {

          if (*str & 0x80)

            return 0;

      }

      return 1;

}

 对于ASCII字符集,UTF-8兼容,因此,对于全是ASCII编码而成的文本(无BOM),文本解析器在解析时可能会解析为ASCII或UTF-8。

如果没有按照文件生成时的编码格式进行解析,那么解析的结果很有可能就是乱码(解析失败),这个就是乱码产生的原因。

字符集与字符编码
扫描二维码,关注“清远的梦呓”公众号,在手机端查看文章

 参考资料 

  1. https://en.wikipedia.org/wiki/Unicode

  2. https://en.wikipedia.org/wiki/UTF-8

  3. https://en.wikipedia.org/wiki/UTF-16

  4. https://en.wikipedia.org/wiki/UTF-32

  5. https://en.wikipedia.org/wiki/GBK_(character_encoding)

  6. https://en.wikipedia.org/wiki/GB_2312

  7. https://en.wikipedia.org/wiki/Code_page_1386

  8. http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html

  9. https://www.jianshu.com/p/092adb8ea442

  10. https://blog.csdn.net/qq_29028175/article/details/52959551

  11. https://xiaogd.net/%E5%AD%97%E7%AC%A6%E9%9B%86%E4%B8%8E%E7%BC%96%E7%A0%81%EF%BC%88%E4%B8%80%EF%BC%89-charset-vs-encoding/#comment-1851

  12. https://www.zhihu.com/question/19677619

  13. http://www.imkevinyang.com/2010/06/%E5%85%B3%E4%BA%8E%E5%AD%97%E7%AC%A6%E7%BC%96%E7%A0%81%EF%BC%8C%E4%BD%A0%E6%89%80%E9%9C%80%E8%A6%81%E7%9F%A5%E9%81%93%E7%9A%84.html

  14. https://www.cnblogs.com/malecrab/p/5300503.html

  15. https://naveenr.net/unicode-character-set-and-utf-8-utf-16-utf-32-encoding/

  16. https://unicodebook.readthedocs.io/guess_encoding.html