在阅读本文章之前,我建议你首先看阮一峰的博客:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
1 字符 & 编码
字符:
是文字和符号的总称。一个汉字、标点符号、英文字母、数字,这都是字符
字符集:
是多个字符的集合。我们可以理解为一本大字典。字符集种类很多,每个字符集包含的字符个数也不同。常见的字符集有:ASCII字符集、Unicode字符集、GB2312字符集、ISO-8859-1字符集等
字符编码:
计算机只能识别二进制1和0。字符集中的字符,计算机是不能直接识别的,所以要将字符集转换为计算机可以识别的二进制,这个转换过程就是编码,而字符编码就是将二进制的数与字符集中的字符对应起来的一套规则
全角和半角(针对中文字符集):
中文字符集将ASCII里本来就有的大小写英文字母、阿拉伯数字、标点符号重新编写了一套两个字节长的编码,这就是 “ 全角 ”字符,而原来在127以下的那些就叫 “ 半角 ”字符了。全角和半角只针对于字母、数字和标点而言的,对于中文字是没有全角和半角区分的。
全角字符:字母和数字与汉字占等宽位置的字并且占2个字节 半角字符:正常ASCII字体宽度且占一个字节
全角字符:abcdef123 -- 字体要宽一点 总共有18个字节
半角字符:abcedf123 -- 字体要窄一点 总共有 9个字节
GB2312、GBK、GB18030区别:
GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的PC平台必须支持GB18030,对嵌入式产品暂不作要求。所以手机、MP3一般只支持GB2312。
从ASCII、GB2312、GBK到GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK到GB18030都属于双字节字符集 (DBCS)。
Unicode汉字范围:
Unicode采用两字节表示,因此总共能表示65536个字符,汉字的开始编号是 19968,十六进制表示 \u4E00,最后一个汉字的位置编号是40869,十六进制表示 \u9FA5。[\u4E00-\u9FA5]
Unicode中每个字符的编码都是2个字节,包括英文字母、数字。
2 记事本下的乱码
我们知道在window下记事本,输入联通,然后保存再打开,会出现乱码情况,以前只知道是GB2312和UTF-8编码冲撞了,并没有认真研究底层原理,今天我们就一起来看看真正的原因是什么。
当新建一个文本文件的时候,记事本的编码默认是ANSI编码
ANSI是一种字符编码标准,其实就是各个国家或地区的国标。对于中国地区就是GB2312编码,中国台湾地区就是BIG5编码,对于日本就是JIS编码,对于美国自然就是ASCII码等等。
如果在ANSI的编码输入汉字,那么他实际就是GB2312系列的编码,在这种编码下,”联通“的十六进制和二进制编码是:
C1 1100 0001
AA 1010 1010
CD 1100 1101
AB 1010 1000
如果大家不知道如何获取指定编码字符的二进制,可以通过程序来处理,也可以通过文本编辑器查看。这里我是通过Java程序来获取 “联通” 在GB2312下的二进制
public class Demo {
public static void main(String[] args) throws Exception {
String str = "联通";
byte[] bytes = str.getBytes("GB2312");
for (byte b : bytes) {
System.out.print(Integer.toBinaryString(b) +"\r\n");
}
}
}
输出结果:
1111111111111111111111111 1000001
1111111111111111111111111 0101010
1111111111111111111111111 1001101
1111111111111111111111111 0101000
这里要注意,因为调用的是Integer.toBinaryString(b)
,而一个int
在Java中占4个字节,byte只占一个字节,所以前三个字节全都是1,我们只用关注后面8位即可。
如果你看了 阮一峰 的博客,那么你现在已经知道了UTF-8的编码规则
Unicode符号范围 | 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
记事本保存联通的二进制为:
1100 0001 1010 1010
1100 1101 1010 1000
这刚好和UTF-8第二类编码一致,所以这个时候,记事本就会误以为该文件应该以UTF-8编码的方式打开,然后就按照UTF-8的方式处理这些二进制,因为前两个字节 1100 0001 1010 1010的实际有效位数是00001 101010,转换成十六进制是6a,但是6a却属于第一类,这个时候就产生乱码了。因此,平常开发尽可能少使用txt。
3 UTF-8解码过程
有一个UTF-8编码的文本,文本内容如下:
aá一
分别是英文字母a,发育字母á,中文汉字一
一:获取十六进制编码
通过16进制工具,查看16进制编码(我本人使用的是Sublime 插件 hexViewer)
61 c3 a1 e4 b8 80
二:计算二进制编码
01100001
11000011
10100001
11100100
10111000
10000000
三:根据UTF-8编码规则将字节分组后:
01100001
11000011
10100001
11100100
10111000
10000000
四:重新计算,得出对应的Unicode字符集的二进制编码
00000000 01100001 注意在Unicode中是两个字节表示一个字符,因此这里会补 00000000
00000000 11111001
01001110 00000000
五: 计算机从Unicode中查询出字符集为
a
á
一
4 UTF-16解码过程
aá一 这三个字符,不过这次是用UTF-16 BE保存的,及大头模式,如果对大头和小头方式不清楚的,请先看阮一峰的博客
一:获取十六进制编码
通过16进制工具,查看16进制编码(我本人使用的是Sublime 插件 hexViewer)
fe ff 00 61 00 e1 4e 00
fe ff表示的是UTF-16 大头模式,仅仅用于标记字节顺序,不表示具体的字符。
二:计算二进制编码
00000000
01100001
00000000
11100001
01001110
00000000
三:根据UTF-16编码规则将字节分组后:
UTF-16两个字节表示一个字符,即使是ASCII也是这样。
00000000 01100001
00000000 11100001
01001110 00000000
因为Unicode定义的标准就是两个字节表示一个字符,而UTF-16具体实现刚好就是用两个字节表示一个字符,因此这里就不需要再进行处理了
四:计算机从Unicode中查询出字符集为
a
á
一
Unicode将世界上的文字和二进制一一对应起来,并规定2个字节表示一个字符。
但是没有规定这个二进制代码应该如何存储,并且在网络上如何传输。
而UTF-8和UTF-16定义了这些二进制代码应该如何存储,并且在网络上如何传输。不过最后,程序都会按照UTF-8和UTF-16的规则去解析这些二进制,然后转换成Unicode,然后再通过一一对应,查找到该字符。
无论是UTF-8还是UTF-16编码的字符,最后转换成Unicode字符的二进制都是一样的,因为他们遵循同一个Unicode标准。
5 ISO-8859-1
以前开发JSP的时候,默认就是ISO-8859-1编码。
ASCII只能表示128个字符,而一个字节能够表示256种字符,ASCII编码浪费了一个字节接近一半的空间。
所以ISO-8859-1扩展了ASCII编码,在ASCII编码之上又增加了西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号,它是向下兼容ASCII编码的。
因为ISO-8859-1编码范围使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。ASCII编码是一个7位的容器,ISO-8859-1编码是一个8位的容器。
6 byte范围
在整理字符研究的时候,发现一个问题
public class Demo {
public static void main(String[] args) throws Exception {
String str = "联通";
byte[] bytes = str.getBytes("GB2312");
for (byte b : bytes) {
System.out.println(b);
}
}
}
输出:
-63
-86
-51
-88
刚开始的时候,不是很理解为什么Java程序会输出负值。想了一下,忽然想起在Java中
byte范围 -128-127
而bytes中保存的二进制是
1100 0001
1010 1010
1100 1101
1010 1000
最高位的1,Java中的byte无法处理,只能当做符号位,以补码存储。
参考资料:
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
https://pcedu.pconline.com.cn/empolder/gj/other/0505/616631_all.html#content_page_2
出处:https://www.cnblogs.com/AdaiCoffee/
本文以学习、研究和分享为主,欢迎转载。如果文中有不妥或者错误的地方还望指出,以免误人子弟。如果你有更好的想法和意见,可以留言讨论,谢谢!