字符编码大总结

参考文献(参考文章较多,未一一列出):

字符集和编码详解(学习,看一篇就够了)

字符,字节和编码

字符编码详解——彻底理解掌握编码知识,“乱码”不复存在

各种字符集和编码详解

1. 字符集和字符编码

1.1 字节 字符

​ 字节是计算机存储容量的单位. 1 字节 = 8 位(二进制位)

​ 字符:任何一个文字或者一个符号都是一个字符

1.2 字符集 和 字符编码

字符集

  • 在一个字库表中,每个字符都有一个对应的二进制地址(即二进制表示方式),而编码字符集就是这些地址的集合.
  • 比如ASCII编码字符集中 A->(0100 0001)65.
  • 字符集和字库表(比如汉字表)一一对应,相互转换.

字符编码

  • 直接使用字符对应的二进制地址来显示文字是十分浪费的,Unicode 编码(包含了各种语言中使用到的所有’字符’)规范中包括了几百万个字符,想要全部包括这些字符,起码需要3字节(2^24=16777216)的容量,为了方便以后的扩展,还将保留更多未使用的空间,最多可以存储4个字节的容量.
  • 因此,为了区分每个字符,哪怕是 00000000 00000000 00000000 00001111 这样的字符(实际只用了一个字节的字符),也要使用4字节的空间,导致了由这类字符组成1G的文件,需要4G才能存储,极其浪费.
  • 于是,就出现了同一编码规范,不同的编码方式. 比如Unicode这个编码规范有 UTF-8,UTF-16,UTF-32这几种编码方式.不同的编码方式,节约空间不同.

字符与编码的发展历程

系统内码说明系统
ASCII计算机刚开始只支持英文英文DOS
ANSI编码(本地化)
0x00~0x7f(127):1个字节表示英文字符(ASCII)
0x80~0xFFFF(扩展的ASCII码)
为了计算机支持更多的语言,通常使用0x80~0xFFFF范围内的两个字节来表示1个字符。比如:“中”在中文系统中使用[0xD6,0xD0]表示.
不同的国家执行不同的标准,由此产生了GB2312,BIG5等各自的标准.使用2个字节表示一个字符的编码方式称为ANSI编码.中文简体系统下,ANSI表示GB2312.
不同的ANSI编码之间互不兼容,信息在国际间交流时,无法将两种语言文字存储在同一段的ANSI编码文本中.
中文DOS,中文Windows等
UNICODE(国际化)为了使国际间的信息交流更加方便,国际组织制定了 UNICODE字符集,为各种语言的每个字符设定了统一并且唯一的编号,以满足跨平台的、跨语言的需求.Linux,Java

image-20200524201323739

1.3 内码

参考: Java中char占用几个字节

内码 :某种语言运行时,其char和 String 在内存中的编码方式。

Java 中的 char 内码使用 utf-16 编码方式.

utf-16:占用 2 / 4 个字节.

当 utf-16 为 4字节时,使用一对 char (具体如何使用没看懂,意思是使用 String ?)

https://docs.oracle.com/javase/tutorial/i18n/text/unicode.html

外码 :除了内码,皆是外码

Java的class文件采用 UTF8 来存储字符,也就是说,class中字符占1~6个字节。
Java序列化时,字符也采用 UTF8 编码,占1~6个字符。

image-20200524202919325

2. 编码方式

2.1 ASCII码

​ 最早的编码规范, 0000 0000 ~ 0111 1111 (0~127,0x7f).可以表示阿拉伯数字和大小写英文字母,以及一些简单的符号标点等. 占用大小 1 字节.

(American Standard Code for Information Interchange美国信息交换标准代码)

2.2 ISO-8859-1

  • 使用 单字节 内的所有空间(8位,0~255),ASCII编码 7 位.
  • 包括ASCII码以外的 西欧、希腊语…等对应的文字符号,不支持中文.
  • 在支持 ISO-8859-1 的系统中传输和存储其他任何编码的字节流都不会被抛弃,换言之,把其他任何编码的字节流当作 ISO-8859-1 编码看待都没有问题.
  • 上面的这个特性应用: MySQL 默认编码 Latin1 就是利用了这个特性.
  • 有时候 tomcat 也使用 ISO-8859-1 编码.

2.3 ANSI 编码规范

  • 编码方式: GB2312 , BIG5, SHIFT_JIS , ISO-8859-2

1. GB2312

  • 规定ASCII 码中127之后的奇异符号直接取消掉,127(0x7f)之前的字符和原来的含义相同,两个大于 127 的字符连在一起表示一个汉字.
  • 前面的一个字节(高字节)从 0xA1~0xF7 ,后面一个字节(低字节) 从 0xA1~0xFE,这样就能组合出大约 7000多个简体汉字了.
  • 这些编码里,把数学符号,罗马希腊字母都编写进去了,连ASCII里本来就有的数组,标点,字母都统统重新编了 2 个字节长的编码.这就是常说的 全角,原来127之前的叫做 半角.
  • 这种汉字方案就叫 GB2312,是对ASCII的扩展,并兼容 ASCII.

2. GBK

  • GBK 字符集所有字符占 2 个字节,不论中文还是英文.
  • 许多人名打不出来,即GB2312 的汉字不够用了,将 GB2312 扩展后称为 GBK.增加了 近20000个新汉字.
  • 支持国际标准 ISO/IEC40646-1 和 国家标准 GB13000-1 中的全部中日韩汉字.

3. GBK18030

  • 少数名族文字无法在电脑显示,然后将 GBK 扩展成 GB18030.
  • GB2312,GBK,GB18030 都是双字节编码,称为 DBCS(double byte character set,双字节字符集).
  • 汉字和英文字符并存,如何区分:值>127,则认为一个双字节字符集里的字符出现了.但在 unicode 中不是这样的.

2.4 UNICODE 编码规范

  • 编码方式: UTF-8 , UTF-16, UnicodeBig 等

0. Unicode 的体系结构

image-20200525094432054

Unicode 也称为 UCS(Universal Coded Character Set:国际编码字符集合) 是一个字符集合,对世界上大部分的文字系统进行了整理,编码,使电脑可以用更为简单的方式来呈现和处理文字。最新的版本 Unicode 11.0 已经包含了 137439 个字符。Unicode 的数量之多,如果完全涵盖它, 需要用 4 个字节来表示,但是计算机存储过程中却不是必须都用 4 个字节来完成(2^32 = 4,294,967,296)。对于有些字符,尤其是编码在前面的字符我们也可以通过 1 个或 两个字节来节省空间。这就涉及到了 unicode 的实现方式。

​ Unicode 常用的编码方式有 UTF-8, UCS-2, UTF-16 三种,另外还有一种 UTF-32 虽然不太常用也需要提一下。

image-20200525093736468

UTF-8: 能够完全兼容 ASCII. 使用最为广泛的编码方式.

UCS-2:使用 2 字节 来表示字符,也就是只能表示 65536 个 字符,它只能表示 BMP 中的字符.当前 unicode 字符数量远超过了 UCS-2,此时新的编码方式出现了 UTF-16.

UTF-16:为了解决 UCS-2 编码问题产生的,扩展自 UCS-2

  • 基本多文种平面中,与 UCS-2 编码完全一致,使用两个字节表示
  • U+010000U+10FFFF 范围 使用 4 个字节表示

UTF-32:UTF-32 对 Unicode 中的每个字符都用 4 个字节来表示,占用的空间比其他编码要多的多,也正是这个原因,人们才用的很少。

1. Unicode 和 BigEndianUnicode

  • 这两个指示了存储顺序不同

    A 的 Unicode -> 6500

    A 的 BigEndianUnicode -> 0065

    endian 可以翻译成字节序.big endian(大尾)和little endian(小尾)是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。还是将49写在前面,就是little endian。

  • 类似网络中传输字节的大小端?

2.UTF

​ 在Unicode里,所有的字符被一视同仁。汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”,注意,现在的汉字是“一个字符”了,于是,拆字、统计字数这些问题也就自然而然的解决了。但是,这个世界不是理想的,不可能在一夜之间所有的系统都使用Unicode来处理字符,所以Unicode在诞生之日,就必须考虑一个严峻的问题:和ASCII字符集之间的不兼容问题。 我们知道,ASCII字符是单个字节的,比如“A”的ASCII是65。而Unicode是双字节的,比如“A”的Unicode是0065,这就造成了一个非常大的问题:以前处理ASCII的那套机制不能被用来处理Unicode了。另一个更加严重的问题是,C语言使用’\0’作为字符串结尾,而Unicode里恰恰有很多字符都有一个字节为0,这样一来,C语言的字符串函数将无法正常处理Unicode,除非把世界上所有用C写的程序以及他们所用的函数库全部换掉。

​ 于是,比Unicode更伟大的东东诞生了,之所以说它更伟大是因为它让Unicode不再存在于纸上,而是真实的存在于我们大家的电脑中。那就是:UTF。UTF= UCS Transformation Format,即UCS转换(传输)格式。它是将Unicode编码规则和计算机的实际编码对应起来的一个规则。现在流行的UTF有2种:UTF-8和UTF-16.

3.UTF-8

编码规则

  • 单字节符号:字节的第一位设为0,后面7位为这个符号的码点.对于英文字母来说,UTF-8和ASCII 码是相同的,即兼容了ASCII码.
  • 对于 n 字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10,剩下的没有提及的二进制位,全部为这个符号的码点.
  • 详细的看 维基百科,解释的通俗易懂.
  • UTF-8 不使用大尾序和小尾序

image-20200524215824811

UTF-8 编码字节的含义:

  • 对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码);
  • 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符);
  • 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节;
  • 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节;
  • 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;
  • 因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;根据前二位,可判断该字节是否为一个字符编码的第一个字节;根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。

示例演示

  • 严的 Unicode 是4E25(‭0100 1110 0010 0101‬),
  • 根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),因此严的 UTF-8 编码需要三个字节,即格式是1110 xxxx 10xx xxxx 10xx xxxx。
  • 然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。
  • 这样就得到了,严的 UTF-8 编码是1110 0100, 1011 1000, 1010 0101,转换成十六进制就是 E4B8A5
  • 还可以参考: https://segmentfault.com/a/1190000018756296

汉字 Unicode 编码范围

image-20200524230602498

注意:

  • 中文在 UTF-8 中并不一定长 3 个字节

    非常见字可能占用 4-6 字节.

    3字节对应的 unicode 为 <= FFFF

    UTF8编码中,英文字符占用一个字节;

    绝大多数汉字占用三个字节,个别汉字占用四个字节

    下面有示例代码

  • UTF-8 一般使用 1-4 字节为每个字符编码.

  • 理论上UTF-8 最多需要用 6 字节表示一个字符.

//https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php
//根据上面的网站可以找到 4字节 的 utf-8 汉字
public static void main(String[] args) {
    String[] chineses={"𠀂","𤭢"};
    //使用UTF-8编码方式进行编码。
    try {
        byte[] byte0 = chineses[0].getBytes("utf-8");
        byte[] byte1 = chineses[1].getBytes("utf-8");

        System.out.println(Arrays.toString(byte0));
        //[-16, -96, -128, -126]
        System.out.println(Arrays.toString(byte1));
        //[-16, -92, -83, -94]
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
}	

4. UTF-16

  • Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。
  • 具体规则参考维基百科
  • 使用 2/ 4 个字节表示

转换规则: 参考链接

image-20200525095443871

//U+22222(大于U+10000) 进行转码UTF16
1)先进行减去0x10000
0x22222 - 0x10000 = 0x12222 =  1 0010 0010 0010 0010
2)转换二进制并且分割位高低10位
二进制1111111111 =  1023十进制
2.1)利用按位与的特性获取低10位
二进制1111111111  &  1 00100010 00100010 = 10 0010 0010
十进制1023 & 0x12222 =  546(10进制,就是上面的二进制)

2.2)利用带符号右移运算符以及按位与获取高100x12222 << 10 = 72 = 1001000
1001000 & 1111111111 = 1001000
72 & 1023  = 72
3)
3.1)10位加0xD800
72 + 0xD800 = 55368 = 0xd848
3.2)10位加0xDC00
546 + 0xDC00 =  56866 = 0xde22

U+22222编码转UTF-16 = [0xd848,0xde22]
var  str = "";
[0xd848,0xde22].forEach(item => {
    str +=String.fromCharCode(item)
})
console.log(str);

2.5 其他的编码方式

MIME:MIME 是“多用途网际邮件扩充协议”的缩写,在 MIME 协议之前,邮件的编码曾经有过 UUENCODE 等编码方式 ,但是由于 MIME 协议算法简单,并且易于扩展,现在已经成为邮件编码方式的主流,不仅是用来传输 8 bit 的字符,也可以用来传送二进制的文件 ,如邮件附件中的图像、音频等信息,而且扩展了很多基于MIME 的应用。从编码方式来说,MIME 定义了两种编码方法Base64与QP(Quote-Printable)

3. UTF 的字节序和 BOM

3.1 字节序

​ UTF-8以字节为编码单元没有字节序的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,首先要弄清楚每个编码单元的字节序。例如收到一个“奎”的Unicode编码是594E,“乙”的Unicode编码是4E59。如果我们收到UTF-16字节流“594E”,那么这是“奎”还是“乙”?

​ Unicode规范中推荐的标记字节顺序的方法是BOM(Byte Order Mark).

  • 在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。
  • UCS规范建议我们在传输字节流前,先传输字符"ZERO WIDTH NO-BREAK SPACE"。
  • 如果接收者收到FEFF,就表明这个字节流是Big-Endian的;
  • 如果收到FFFE,就表明这个字节流是Little-Endian的。
  • 因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。

UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式。字符"ZERO WIDTH NO-BREAK SPACE"的UTF-8编码是EF BB BF(读者可以用我们前面介绍的编码方法验证一下)。所以如果接收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。 (不会验证)

一个特殊的空格ZERO WIDTH NO-BREAK SPACE

编码方式BOM
UTF-8EE BB FF
UTF-16(BE)FE FF
UTF-16(LE)FF FE

3.2 BOM

​ 为了识别 Unicode 文件,Microsoft 建议所有的 Unicode 文件应该以 ZERO WIDTH NOBREAK SPACE(U+FEFF)字符开头。这作为一个“特征符”或“字节顺序标记(byte-order mark,BOM)”来识别文件中使用的编码和字节顺序

image-20200525102921878

BOM 与 XML: XML 解析 XML 文档时的规则:

  • 文档中有 BOM,则定义了文件的编码
  • 无 BOM,就查看 XML 声明 中的编码属性
  • 都没有,假定文档采用 UTF-8 编码

4. 误区

4.1 容易产生的误解

image-20200525104205989

  • Java 中,字符串类 java.lang.String 处理的是 UNICODE 字符串,不是 ANSI 字符串。我们只需要把字符串作为“抽象的符号的串”来看待。因此不存在字符串的内码的问题。

4.2 网页提交字符串

当页面中的表单提交字符串时:

  • 首先把字符串按照当前页面的编码,转化成字节串。
  • 然后再将每个字节转化成 “%XX” 的格式提交到 Web 服务器。比如,一个编码为 GB2312 的页面,提交 “中” 这个字符串时,提交给服务器的内容为 “%D6%D0”。

在服务器端:

  • Web 服务器把收到的 “%D6%D0” 转化成 [0xD6, 0xD0] 两个字节,
  • 然后再根据 GB2312 编码规则得到 “中” 字。

在 Tomcat 服务器中,request.getParameter() 得到乱码时

  • 常常是因为前面提到的“误解一”造成的。
  • 默认情况下,当提交 “%D6%D0” 给 Tomcat 服务器时,request.getParameter() 将返回 [0x00D6, 0x00D0] 两个 UNICODE 字符,而不是返回一个 “中” 字符。
  • 因此,我们需要使用 bytes = string.getBytes(“iso-8859-1”) 得到原始的字节串,再用 string = new String(bytes, “GB2312”) 重新得到正确的字符串 “中”。

4.3 从数据库读取字符串

​ 通过数据库客户端(比如 ODBC 或 JDBC)从数据库服务器中读取字符串时,客户端需要从服务器获知所使用的 ANSI 编码。当数据库服务器发送字节流给客户端时,客户端负责将字节流按照正确的编码转化成 UNICODE 字符串。

​ 如果从数据库读取字符串时得到乱码,而数据库中存放的数据又是正确的,那么往往还是因为前面提到的“误解一”造成的。解决的办法还是通过 string = new String( string.getBytes(“iso-8859-1”), “GB2312”) 的方法,重新得到原始的字节串,再重新使用正确的编码转化成字符串。

4.4 char 能不能存中文

//char 内部使用的是 utf-16 编码,2字节能够保存常见的中文(绝大部分)
//char charA= '𠀂'; 
char charB = '严';
//char t = '𤭢';
// utf-16 编码为: D852 DF62, 两个字节,char 无法存

4.5 解码编码

解码:一串二进制数,使用一种编码方式,转换成字符,这个过程我们称之为解码.使用正确的解码方式才能显示正确的文字,否则就会出现乱码.

编码:一串已经解码后的字符,我们也可以选用任意类型的编码方式重新转换成一串二进制数,这个过程就是编码.

如果编码规范的字库表不包含目标字符,则无法在字符集中找到对应的二进制数。这将导致不可逆的乱码!

例如:像 ISO-8859-1的字库表中不包含中文,因此哪怕将中文字符使用ISO-8859-1进行编码,再使用ISO-8859-1进行解码,也无法显示出正确的中文字符。

// 使用 ISO-8859-1 字符集中不存在的字符进行编码
//中文全部被编码成了 63
public static void main(String[] args) {
    String str = "程序员";
    byte[] bytes = null;
    try {
        bytes = str.getBytes("ISO-8859-1");
        String s1 = new String(bytes, "ISO-8859-1");
        System.out.println(s1); //??? 
		System.out.println(Arrays.toString(bytes));
        //[63, 63, 63]
        
        System.out.println("***************");

        bytes = str.getBytes("GBK");
        String s = new String(bytes, "GBK");
        System.out.println(s); //程序员
        System.out.println(Arrays.toString(bytes));
        //[-77, -52, -48, -14, -44, -79]
        
        //使用什么方式编码,就是用什么方式解码
        String gbk = new String(bytes, "utf-8");
        System.out.println(gbk); // ����Ա  乱码了

    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值