技术的发展就是在不停的解决问题和引入新的问题。
说来惭愧,对字符编码一直都是似懂非懂的,昨天组内大佬又给讲了一遍。本文就是作为一个初学者对字符编码的一个学习笔记。
在步入正题之前首先明确这么几点:
- 计算机由逻辑电路组成,逻辑电路有两种状态开和关,可以使用使用 1 和 0 表示,说白了就是计算机只认识 0 和 1;
- 我们人类使用的各种文字、数字、标点等计算机都不认识,这种 文字、数字、标点就称为字符,多个字符就是字符集;
- 需要想办法把字符和 0、1 对应起来,在对应之前需要给每一个字符加一个唯一标识,类似于唯一 ID,这个就称为码位,与字符对应起来的这种方式就是字符编码;
ASCII 码
计算机是美国人发明的,他们的字符集就是英文字母、数字和其他符号等,总共 128 个,这就是 ASCII 字符集,直接简单的从 0 到 127 进行编号,这个编号就是码位。字符集和码位都有了,ASCII 码就是对应的码位的二进制。
扩展ASCII码(EASCII)
后来计算机传到了欧洲,欧洲人也有自己的字符集,此时 ASCII 码就不够用了,于是就要进行扩展,从 128 扩展到了 255,又新增了 128 个字符。新增的 128 个字符就叫扩展 ASCII 码字符集。对应的 ASCII 码就叫扩展 ASCII 码,即 EASCII 码。
EASCII 码同样属于单字节编码,向下兼容 ASCII 码。最高位为1的编码称为扩展ASCII码,范围是128~255。
GB2312
后来中国也开始使用计算机,在网上查了下,常用汉字都有 1000 个,EASCII 码才 8 位,肯定不够,于是就采用 16 位进行存储。
但是汉字太多了,设计字符集的时候不能简单的编号,于是使用了一种分区的方式:
01-09 区收录除汉字外的682个字符。
10-15 区为空白区,没有使用。
16-55 区收录3755个一级汉字,按拼音排序。
56-87 区收录3008个二级汉字,按部首/笔画排序。
88-94 区为空白区,没有使用。
每个汉字及符号以两个字节来表示。第一个字节称为“高位字节”(也称“区字节)”,第二个字节称为“低位字节”(也称“位字节”)。
以 16 区为例:
16 0 1 2 3 4 5 6 7 8 9
0 啊 阿 埃 挨 哎 唉 哀 皑 癌
1 蔼 矮 艾 碍 爱 隘 鞍 氨 安 俺
2 按 暗 岸 胺 案 肮 昂 盎 凹 敖
3 熬 翱 袄 傲 奥 懊 澳 芭 捌 扒
4 叭 吧 笆 八 疤 巴 拔 跋 靶 把
5 耙 坝 霸 罢 爸 白 柏 百 摆 佰
6 败 拜 稗 斑 班 搬 扳 般 颁 板
7 版 扮 拌 伴 瓣 半 办 绊 邦 帮
8 梆 榜 膀 绑 棒 磅 蚌 镑 傍 谤
9 苞 胞 包 褒 剥
比如“版“字属于 16 区 7 行 0 列,那么版的码位就是 1670。高位 16 的十六进制是 10,低位 70 的十六进制是 46,再将每个区位都加上 0xA0。最终十六进制结果就是 B0E6。
可以用 Java 代码验证一下:
@org.junit.Test
public void test1() throws UnsupportedEncodingException {
System.out.print(bytesToHexFun1("版".getBytes("GB2312")));
}
final static char[] HEXS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static String bytesToHexFun1(byte[] bytes) {
char[] buf = new char[bytes.length * 2];
int a = 0;
int index = 0;
for (byte b : bytes) {
if (b < 0) {
a = 256 + b;
} else {
a = b;
}
buf[index++] = HEXS[a / 16];
buf[index++] = HEXS[a % 16];
}
return new String(buf);
}
输出结果:
b0e6
但是汉字中还会有一些罕见字和繁体字,于是微软利用 GB2312 中未使用的编码空间收录 GB13000 制定了 GBK。但是 GBK 中不包含很多少数民族的字符,后来我国政府又推出了 GB18030。
Unicode
随着发展,计算机在很多国家和地区都开始使用,但是各个国家和地区的字符都不相同,如果都自己定制自己的编码标准,那么就会很混乱,而且无法互通。于是 ISO 组织就提出了 Unicode标准(包含了字符集和编码规则),说白了就是想把世界上所有的字符都搞到一起,统一给它们编上号。
UCS-2 和 UCS-4
一开始 Unicode 使用的是 UCS-2 字符集,这个就是把所有的字符放在一起给他们编上号,直接将码位转化为二进制进行存储。一共可以表示 2 的 16 次方,也就是 65535 个字符。但是后来发现还是不够,于是就有了 UCS-4,它使用的是四个字节,可以表示 20 亿以上的字符。
UTF-8
UCS-4 要占用 32 位空间,太大会影响网络传输效率,如果传输 7 位 ASCII 字符,会浪费很大的空间。于是就有了 UTF-8。UTF-8 每次传输 8 位数据,是一种可变长的编码格式。UTF-8 将 UCS-4 字符集的码位划分成 4 个区间:
编码区间 | 编码样式
0x0000 0000 到 0000 007F | 0xxxxxxx
0x0000 0080 到 0000 07FF | 110xxxxx 10xxxxxx
0x0000 0800 到 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0x0001 0000 到 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
这样计算机碰到 0 开头的字节就知道这是一个 ASCII 字符,碰到 110 就知道还要继续往后找一个字节,碰到 1110 就知道还要再往后再找两个字节 …
比如“慧”在 UCS-4 中就是 0x000 6167,属于第三区间。转化为二进制是 00000000 00000000 01100001 01100111,插入到第三区间的编码样式中,结果就是 11100110 10000101 10100111,转化为十六进制就是 e685a7。
同样地再用 Java 代码测试一下:
@org.junit.Test
public void test1() throws UnsupportedEncodingException {
System.out.print(bytesToHexFun1("慧".getBytes("UTF-8")));
}
final static char[] HEXS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static String bytesToHexFun1(byte[] bytes) {
char[] buf = new char[bytes.length * 2];
int a = 0;
int index = 0;
for (byte b : bytes) {
if (b < 0) {
a = 256 + b;
} else {
a = b;
}
buf[index++] = HEXS[a / 16];
buf[index++] = HEXS[a % 16];
}
return new String(buf);
}
输出结果:
e685a7
总之
字符编码就是要将人类用到的字符存储到计算机里面,而计算机只知道 0、1,所以就要有个人类字符和 0、1 的对应关系,于是就有了字符的唯一 ID 码位和与 0、1 的对应关系编码方式。过于细节的点个人觉得没必要太纠结。而我们常见的乱码问题就是编解码的方式不一致所造成的。
总的来说,字符编码的发展就是不停地补充遗漏字符和兼容优化的过程。
References
- https://v.zhaosw.com/b/TwmVv4eE.html
- https://baike.baidu.com/item/%E4%BF%A1%E6%81%AF%E4%BA%A4%E6%8D%A2%E7%94%A8%E6%B1%89%E5%AD%97%E7%BC%96%E7%A0%81%E5%AD%97%E7%AC%A6%E9%9B%86/8074272?fromtitle=GB2312&fromid=483170&fr=aladdin#3
- https://www.qqxiuzi.cn/zh/hanzi-gb2312-bianma.php
- https://tool.chinaz.com/tools/unicode.aspx