计算机中储存的信息都是用二进制数表示的;而我们在屏幕上看到的英文、汉字等字符是二进制数转换之后的结果。通俗的说,按照何种规则将字符存储在计算机中,如'a'用什么表示,称为"编码";反之,将存储在计算机中的二进制数解析显示出来,称为"解码",如同密码学中的加密和解密。在解码过程中,如果使用了错误的解码规则,则导致'a'解析成'b'或者乱码。
对于计算机诞生地的美国人来说,它们一开始要存的字符无非就是英文字母、数字、和一些特殊符号(+-&/?!.等)。所以只需要把这些字符编个号,然后存这些编号就行了。如果用 1 字节的空间来存编号,那么就可以存 2 的 8 次方 (256) 个不同的编号了,即可以用一个字节表示 256 种不同的字符,这足够给每一个字符都找一个自己的二进制表示了。但是随着计算机的普及,各个国家都开始使用计算机,显然这些编号已经不适用了。
什么是字符集
简单的说: 字符集就是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
那么为什么会有那么多字符集标准呢?
很多规范和标准在最初制定时并不会意识到这将会是以后全球普适的准则,或者处于组织本身利益就想从本质上区别于现有标准。于是,就产生了那么多具有相同效果但又不相互兼容的标准了。
最早是美国人使用电脑,所以最初的字符集是ASCII,使用7位bit表示一个字符,总共能表示128种字符。后来,随着欧洲各个国家的计算机普及,人们发现ASCI码对应的128个字符不能够满足需求了,因为欧洲各个国家都有一些特殊的字符。所以ASCII进行了扩容,使用8位bit表示一个字符,总共就能表示256种字符。
而当中国开始普及计算机后,发现256中字符根本没办法满足咱们中国人的日常需求,中华文化上下五千年,汉字的数量多了去了,一个字节根本没法对应一个字符。所以只能继续扩容,两个字节对应一个字符,所以后续的GB2312、GBK等等字符集就都被提出来了。 正因为世界上有各种各样的国家、民族需要使用计算机,各个国家地区都会根据自己的文化特色创造出相对应的字符集标准。
字符集只是一个规则集合的名字,对应到真实生活中,字符集就是对某种语言的称呼。例如:英语,汉语,日语。
什么是字符编码
是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。 对于一个字符集来说要正确编码转码一个字符需要三个关键元素:
- 1)字库表(character repertoire):是一个相当于所有可读或者可显示字符的数据库,字库表决定了整个字符集能够展现表示的所有字符的范围;
- 2)编码字符集(coded character set):即用一个编码值code point来表示一个字符在字库中的位置;
- 3)字符编码(character encoding form):将编码字符集和实际存储数值之间的转换关系。
一般来说都会直接将code point的值作为编码后的值直接存储。例如在ASCII中“ A ”在表中排第65位,而编码后A的数值是 0100 0001 也即十进制的65的二进制转换结果。
看到这里,可能很多读者都会有和我当初一样的疑问: 字库表和编码字符集看来是必不可少的,那既然字库表中的每一个字符都有一个自己的序号,直接把序号作为存储内容就好了。为什么还要多此一举通过字符编码把序号转换成另外一种存储格式呢?
其实原因也比较容易理解: 统一字库表的目的是为了能够涵盖世界上所有的字符,但实际使用过程中会发现真正用的上的字符相对整个字库表来说比例非常低。例如中文地区的程序几乎不会需要日语字符,而一些英语国家甚至简单的ASCII字库表就能满足基本需求。而如果把每个字符都用字库表中的序号来存储的话,每个字符就需要3个字节(这里以Unicode字库为例),这样对于原本用仅占一个字符的ASCII编码的英语地区国家显然是一个额外成本(存储体积是原来的三倍)。算的直接一些,同样一块硬盘,用ASCII可以存1500篇文章,而用3字节Unicode序号存储只能存500篇。于是就出现了UTF-8这样的变长编码。在UTF-8编码中原本只需要一个字节的ASCII字符,仍然只占一个字节。而像中文及日语这样的复杂字符就需要2个到3个字节来存储。
常见字符集名称: ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。
ASCII 码
它是一个很小的字符集,但也是最常见的字符集。它收录了 128 个字符,并用 0-127 的代码点表示它们,他只有一种编码方式,也很简单,就是直接写出代码点的二进制就行了,所以一个 ASCII 字符只占用一个字节,字节的第一位永远是 0。比如数字 "0" 的代码点是 48(如下图所示),那么它编码后的的二进制表示就是 0b00110000 (0b 表示二进制)、解码时如果读取到的字节是 00110000,我们也就知道了这个字节表示的是代码点为 48 的字符,即字符 "0"。不过我们在书写 1 个字节时,通常为了方便阅读,会写成 2 个 十六进制数,比如刚才那个字节的十六进制表示就是 0x30(0x 表示十六进制)。同样的,像大写字母 A ,它的代码点是 65 ,编码后的字节就是 0x41、小写字母 a ,它的代码点是 97,字节就是 0x61了。
ASCII 编码是一种非常节省空间的编码方式,任何一个 ASCII 字符都只需要一个字节就足以表示了。
GBK 字符集
对于中国人来说,只包含 128 个字符的 ASCII 字符集显然不够,汉字千千万,于是我们就搞了自己的字符集还有对应的字符编码方式。GBK 字符集收录了 2 万多个汉字,以及所有的 ASCII 字符,最重要的是所有 ASCII 字符的代码点在 GBK 字符集和 ASCII 字符集里都是一样的。由于收录的字符很多,所以 1 字节的空间不足以表示 1 个字符,于是 GBK 编码采用 2 个字节来表示一个汉字,但为了避免浪费空间,ASCII 字符只会占 1 个字节。
那么解码时如何知道某个字节是在代表一个 ASCII 字符,还是需要再组合一个字节来代表一个汉字呢?GBK 编码采用了一种很巧妙的方式,即 2 字节的字符对应的二进制第一位永远是 1,这样解码的时候,如果遇到 1 开头的字节,就连读 2 个字节按一个字符进行解码,如果遇到 0 开头的字节,这知道这个是 ASCII 字符了,不需要再继续读一个字节放在一起解了。由于这种机制,GBK 实际上无法完全利用 2 的 16 次方 (65536) 大小的空间,因为 2 字节字符的第一位的 1 是固定的,可修改的 bit 只有 15 个,但 3 万多的空间,也足够放下大部分汉字了。
Unicode 字符集
各个公司,各个国家地区都发挥了自己的聪明才智,搞出了各种花里胡哨的字符集和字符编码方式,但互联网的发展要求大家必须得有一个统一的字符集和编码方式,不然怎么在一个网页里同时展示中文、英文、日文、韩文、阿拉伯文。。。总不能来来回回切换字符集和字符编码方式吧?于是就有组织跳出直接搞了一个 Unicode 字符集,这个字符集的代码点足够多,有一百多万个(从 0x000000 到 0x10FFFF),这么一来即使全世界所有语言的字符都放进来也还绰绰有余。既然可以表示这么多字符,那岂不是每个字符也需要更大的空间,不然岂不是会重复?确实如此,需要 4 个字节来表示。但 Unicode 提供了多种字符编码方式,有些编码方式可以帮我们节省空间,比如大家最常听到的 UTF-8,接下来我们就一起了解一下 Unicode 最常见的三种编码方式:
UTF-32 编码
采用 UTF-32 编码的一个 Unicode 字符固定就是用 4 个字节大小。4 个字节的内容就是代码点的二进制。
UTF-16 编码
采用 UTF-16 编码的一个 Unicode 字符可能是 2 字节大小,也可能是 4 字节大小。这是因为它的 代码单元(code unit) 是 2 字节,上面也提到过 Unicode 字符的代码点都位于 0x000000 到 0x10FFFF,即总的有 65535 * 17 个代码点,这些代码点被分成了 17 个区(Plane,有些直译叫平面,我习惯叫区),如下图所示:
UTF-8 编码
即使是 UTF-16,每个字符最少也依然需要 2 个字节表示,这依然存在空间浪费。于是终极版的 UTF-8 编码它来了!UTF-8 编码的 code unit 是 1 字节,也就是说一个字符最少只需要一个字节表示就够了,当然代码点大的还是需要 4 个字节。UTF-8 编码规则是把所有的代码点(0x000000 到 0x10FFFF)分成了 4 个区域:
| 区域 (HEX) | 区域 (DEC) | 二进制编码(0 或 1 的 bit 已固定,x 表示可用的 bit) | 字节数 | 可用 bit 数 | 区域内最大代码点所需 bit 数 |
|---|---|---|---|---|---|
0x000000 到 0x00007F | 0 到 127 | 0xxxxxxx | 1 | 7 | 7 |
0x000080 到 0x0007FF | 128 到 2047 | 110xxxxx 10xxxxxx | 2 | 11 | 11 |
0x000800 到 0x00FFFF | 2048 到 65535 | 1110xxxx 10xxxxxx 10xxxxxx | 3 | 16 | 16 |
0x010000 到 0x10FFFF | 65536 到 1114111 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 4 | 21 | 21 |
有了上面的分区表,UTF-8 的编码过程表述起来也就很简单了,我概括就是 3 步:
- 第一步:找区域。还是以 "😀" 为例,代码点是 128512,属于第四个区域。
- 第二步:算 bit。把代码点转成 2 进制,如果二进制的结果不足 21 位,前面就补 0。
(128512).toString(2).padStart(21,0)的结果就是000011111011000000000 - 第三步:塞进去。把得到的 21 个 bit 从左到右依次替换掉 x 的部分,结果就是
0b11110000_10011111_10011000_10000000,即0xf0_9f_98_80。
更详细的说,UTF-8编码具有以下几点优点:
- ASCII是UTF-8的一个子集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。
- 它可以通过向前回朔最多2个字节就能确定当前字符编码的开始字节的位置。它也是一个前缀编码,所以当从左向右解码时不会有任何歧义也并不需要向前查看
- 没有任何字符的编码是其它字符编码的子串,或是其它编码序列的字串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰
- 同时UTF8编码的顺序和Unicode码点的顺序一致,因此可以直接排序UTF8编码序列.
概括一下: Unicode:包容万国,优点是字符->数字的转换速度快,缺点是占用空间大。 UTF-8:精准,对不同的字符用不同的长度表示,优点是节省空间,缺点是:字符->数字的转换速度慢,因为每次都需要计算出字符需要多长的字节才能够准确表示。 所以一般在内存中使用的编码是unicode,用空间换时间,为了快。因为程序都需要加载到内存才能运行,因而内存应该是尽可能的保证快。但硬盘中或者网络传输用utf-8,网络I/O延迟或磁盘I/O延迟要远大与utf-8的转换延迟,而且I/O应该是尽可能地节省带宽,保证数据传输的稳定性。因为数据的传输,追求的是稳定,高效,数据量越小数据传输就越靠谱,于是都转成utf-8格式的。
实际项目中遇到的问题
Chrome默认的编码是Unicode(UTF-8)
一次完整的Web请求会有4次编解码转换,如下所示。
第一次:客户端(通常为浏览器)将字符转换成TCP字节流发向服务器。
这里有一次字符到字节的转换。
第二次:服务器读取客户端发来的TCP字节流,转换成字符串。
这里是一次字节到字符的转换。
第三次:服务器将结果字符串换成TCP字节流发向客户端。
这里又有一次字符到字节的转换。
第四次:客户端读取服务端发过来的响应字节流。转换成字符串显示。
一个完整的Web请求就结束了。
在平常的请求中,乱码一般出现在请求路径url上和请求返回的内容中,路径或者返回内容有中文就会出现乱码,(英文字符为什么没乱码? 因为采用ASCII码,绝大部分字符集对英文的编码都一样的)浏览器将数据编码并提交上来,但是服务器并不知道编码规则,或者服务器返回的编码格式和浏览器的编码不一样,浏览器在不知道服务器响应内容的编码情况下会按照当前操作系统的默认编码去执行 中文操作系统默认是GBK
当你在记事本中编写HTML代码时,即使你没有添加meta标签,用浏览器打开时也不会出现乱码,这是因为记事本的编码格式为ANSI,即自动编码格式,根据系统的语言自动进行编码。
在以后的开发过程中,推荐使用UTF-8字符集。
浏览器确定html文件编码的优先级
stackoverflow.com/questions/1…
1.如果用户指定了编码,则按用户指定的编码进行解码。
2.如果HTTP响应头中存在编码信息,则按响应头中的编码进行解码。
3.先按浏览器的编码选择算法选择一个编码进行预解析,如果解析出标签中存在字符集设置,则按标签中的字符集进行解码。
4.按浏览器的默认编码。