本篇目的主要是搞清楚UTF-8, GBK, Unicode, ASCII码,Unicode, escape, encodeURIComponent, encodeURI 编码等等的区别和联系。
文章起源
事情起源是,今天在做小程序的时候,发现我请求业务接口时,业务接口返回的是gbk编码。
然后这个在小程序中不支持,会返回内容如:fail response data convert to UTF8 fail
, 在 pc 端及 android 端表现正常,但是在 ios 下会返回此错误码。
于是很不好意思的找到了业务方的同事,看能不能把返回的内容变成标准的 utf8 无 bom编码格式。话说,我遇到的这个同事真的好 nice, 以前跨部门的合作没有1-2天下不来,这个同事效率比较高,每次有问题找到他,都能够很快的反馈给我(感恩)。然后他就跟我说了下面这样一句话:
你请求参数用gbk编码 我返回给你utf8你看如何?
大概是都统一用 utf8 对于他改动比较大,于是他提出我请求的时候用 gbk 编码。于是我把这段话错误理解为了:
你请求的时候,用 gbk 编码请求,我返回给你 utf8。
也就是说我会在请求 header 里加入 charset=gbk
, 他返回给我 utf8 编码内容就好。
但实际上同事这句话的意思是:
把调用他接口的请求参数用gbk编码,而不是在请求头里加 header。我掉了参数两个字。
因为我们组内的前端统一用的都是 utf8, 编码都是用 encodeURI, encodeURIComponent, 以前也没有用过把参数做 gbk 编码的这种方式,导致了我认为是在请求头里请求用 gbk。 并且对 encodeURI 等等的不深入了解,导致了我浪费了同事的时间。 encodeURI, encodeURIComponent 都是针对 utf-8 的编码, 不能够指定编码方式。 gbk 编码在 github 上有这种成熟的 npm。
大概的思路是,如果是 ASCII 码,直接 encode, 判断不是 ASCII 码后,再进行 GBK 的比对编码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15export const encodeUrlGbk = (str) =\> {
return str && str.replace(/./g,function (a) {
var code = a.charCodeAt(0);
if (_isAscii(code)) {
return encodeURIComponent(a);
} else {
var key = code.toString(16);
if (key.length !== 4) {
key = ('000' \+ key).match(/....$/)\[0\];
}
return _charHash.U2Ghash\[key\] || a;
}
});
}
虽然问题解决了,但是发现了自己对这块是不是根本没有弄清楚,今天仔细的查了资料,从新学习和认识下,想把这些内容都了解并且比较清楚。
escape, encodeURIComponent, encodeURI 的区别和联系
如果你想直接看结论,可以跳过下部分引用内容。
escape() 函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。
该方法不会对 ASCII 字母和数字进行编码,也不会对下面这些 ASCII 标点符号进行编码: * @ - _ + . / 。其他所有的字符都会被转义序列替换。他的解码为 unescape。
ECMAScript v3 反对使用该方法,应用使用 decodeURI() 和 decodeURIComponent() 替代它。
encodeURI() 函数可把字符串作为 URI 进行编码。
该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ‘ ( ) 。
该方法的目的是对 URI 进行完整的编码,因此对以下在 URI 中具有特殊含义的 ASCII 标点符号,encodeURI() 函数是不会进行转义的:;/?:@&=+$,#
如果 URI 组件中含有分隔符,比如 ? 和 #,则应当使用 encodeURIComponent() 方法分别对各组件进行编码。
encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。
该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ‘ ( ) 。(这点与 encodeURI 相同)
其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。 (这点与 encodeURIComponent 不同)
关于这三个函数总结下如下:
如果是跟url编码有关系的,先忘掉 escape 这个函数,他跟 url 编码毫无关系,它只是能够达到类似于 URL Encode 这类似的效果,但是它对于非 ASCII 字符使用了一种非标准的实现,比如某些汉字会被编码成 %uxxxx 这种形式, 在小程序中,url使用 escape, 会报错: fail request:fail _invalid url 这类似的消息。并且 W3C 把这个函数废弃了,能不使用尽量不要去使用
对于 url 的编码,就使用 encodeURI 及 encodeURIComponent, 他们都是为了 url 编码而设计,不同的地方在于 encodeURI 用于对完整的 url 进行编码,于是URL中的功能字符,比如&, ?, /, =等等这些并不会被转义, 而 encodeURIComponent 被设计用来对某段 query(某个值) 编码。所以正确用法是 encodeURIComponent 经常用于对每个 key value 进行分别编码。
延伸阅读-escape, encodeURI的编码方式-百分号编码
百分号编码(英语:Percent-encoding), 也称作URL编码(英语:URL encoding), 是特定上下文的统一资源定位符 (URL)的编码机制. 实际上也适用于统一资源标志符(URI)的编码。也用于为”application/x-www-form-urlencoded” MIME准备数据, 因为它用于通过HTTP的请求操作(request)提交HTML表单数据。
对于 escape 以及 encodeURI 都是属于 percent-encoding
。差不多都是把 URI 非法字符转化为合法字符, 转化以后有 %
, 所以我猜才叫 percent-encoding 吧,哈哈。
首先 escape 和 encodeURI 在处理 0xff 以内的内容时,都是相同的。也就是说是 %xx, 这里的 xx 是字符的 16进制 unicode (此时的 unicode === utf8值, 所以 escape 的值也等于 encodeURI 的值)。
但 escape 在处理 0xff 之外的时候, 直接使用字符的 unicode , 并且在前面再加上一个 %u
(不标准) , 而 encodeURI 是先对 unicode 进行 utf-8 编码,再在每个 utf-8 编码前加上 %(标准)。
比如中文的【我】字, 他的 unicode 为 0x6211, 对他进行 utf8 编码变成了 0xe6, 0x88, 0x91, 因此 escape 得到的值为 \u6211
, 但是encodeURI 的结果是 %E6%88%91
。
对 escape, encodeURI 这些其实以前是了解的, 但是这次是真正的总结清楚了,挺开心。上面是不是提到了很多比如 utf8, gbk, unicode 之类的词。 下面对这些名词也做一些简单的归类和总结。
UTF-8, Unicode, ASCII码的区别和联系
ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统,并等同于国际标准ISO/IEC 646。
注意 ASCII 码是 American Standard Code for Information Interchange
的简称, 后面的两个II 并不是罗马数字2哦。
我们知道计算机里面存储所有信息都是通过一个二进制值。每一个二进制位(bit)都有两种状态 0或者1, 规定了8个二进制位为一个字节(byte),可以表示256种状态,包括了 00000000
到 11111111
。
美国🇺🇸人们自然想到了可以他们自己规定一套字符编码,把26英文字母,及一些特殊符号和控制字符统一编码。他们一共规定了 128个字符,比如说 A 是 65(01000001)。128位只需要占用7位,最高位统一为0。
比如遇到0x10 就是换行, 遇到 Ox1b 就打印反白字。这类0x20以下的字节称为控制字节。空格,标点符号,数字,大小写字母用连续的字节状态表示,一直到127号。
所以 ASCII码就是0-127这套编码方案所形成的编码,这套编码方案叫做ANSI。
下面给出两种判断字符是不是 ASCII 码的方案:1
2
3
4
5
6
7isASCII (unicode) {
return ((unicode === 0x20AC) || (unicode <= 0x007F && unicode >= 0x0000));
}
// 另外一种用正则判断
isASCII(str) {
return /^\[\\x00-\\x7F\]*$/.test(str);
}
但是问题来了,英语用128个编码就足够了,但是其他语言用这些字符是远远不够的,于是延伸出了256编码,也就是最高位也被使用上(这称为扩展字符集)。但是不同的国家有不同的字母,同样的编码可能也代表着不同的内容。比如 150,可能法国是一个,俄语又是一个。他们0-127都是相同,但是128-256不一定相同了,这就看不同的国家自己的制定方式了。
但是再想想中国🇨🇳就更可怕了,一共有10几万的文字。显然256种符号是没有办法完全表达的。于是又出现了多字节的形式。比如两个字节,做多表示 256*256 种。大概又6w多符号。最常见的中文编码就是这种2个字节的编码方式,如 GB2312。
下面就介绍下 GB2312, GBK等的区别和联系。
GB2312,GBK,GB18030的区别和联系
中国用到计算机比较晚,那个时候 256个符号已经没有可以用来表达中文文字的了。并且中文10w+个,只能够另辟蹊径了。于是中国人把127号之后的字符都去掉了,并且进行了规定。
每个汉字及符号以两个字节表示。第一个为高字节,第二个为低字节。一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE。
具体这个字符范围的得来,涉及区位等知识,这些我不打算认识,不做介绍。
这种方案称之为GB2312, 他是对ASCII码的中文扩展。这样就 6000+汉字可以得到表示。
GB 2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个字符。 GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。但对于人名、古汉语等方面出现的罕用字和繁体字,GB 2312不能处理,因此后来GBK及GB 18030汉字字符集相继出现以解决这些问题。–wiki
后来GB2312这种方案还是不够用,于是就不再要求低字节一定是127之后的内码, 只要是第一个字节大于127,就表示这是汉字的开始,不用管低字节是否是扩展字符集的内容。这种GB2312被扩展之后的内容称之为 GBK, GBK 包括了GB2312的所有内容,并且新增加了20000个汉字。
后来少数民族也需要用电脑,于是又增加了少数名族的内容,GBK 被扩展为了 GB18030。
也就是说 GB18030 > GBK > GB2312。 这一系列的中文编码,称为 DBCS(Double Byte Character Set 双字节字符集)。
在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。–wiki
于是就出现了后来编程时候的默念: 一个中文字符占两个字节,1个英文字符占用1个字节。
注意这个时候 GBK, GB2312 等,只是中文对于ASCII码的扩展,和 UTF8,Unicode 这些并无联系。下面再来看看 Unicode 及 UTF-8。
Unicode 及 UTF-8
结论: UTF-8 是 Unicode 的实现方式之一。
前面我们说过,世界上存在着很多编码方式,每个国家可能会去规定和制作自己的编码方式。不同的二进制因而有可能会被解释成不同的内容。 因此如果你不知道一个文件的编码,然后随意用编辑器打开了就会出现我们经常出现的乱码现象。
聪明的人们当然意识到了这一点,所以如果有一种编码方式能够把世界所有的符号都纳入其中,那该多好? 再也不会有乱码现象了。
于是 Unicode 出现了(union code ?) , 一定要注意 Unicode 并不是一个编码方式,不是一个算法,而是一个集合,集合了所有的符号,它只是规定了所有符号的二进制代码,并没有一个算法来说怎么存储这个内容。比如U+0041
表示大写字母A。 (其实我在想,它能把所有符号录入进去,已经是很大的工作量了。)
这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。 —- 引用自阮一峰的文章
后来的人们也进行了很多尝试, 发明了 unicode 的多种存储方式(不同的二进制编码方式或者称之为一种算法)。直到 UTF-8 这种编码方式的出现, Unicode 才得以推广。 所以前面才说 UTF-8 是一种是Unicode的实现方式。
UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字的应用中,优先采用的编码。
关键字是可变长度字符编码,也就是说对于不同类型的符号,它占用的字节长度不一样。这样可以节省很多空间。并且比较灵活。一般情况下,它使用1-4个字节来表示。比如:
128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。
其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。
其他极少使用的Unicode 辅助平面的字符使用四至六字节编码(Unicode范围由U+10000至U+1FFFFF使用四字节,Unicode范围由U+200000至U+3FFFFFF使用五字节,Unicode范围由U+4000000至U+7FFFFFFF使用六字节)。 –wiki
utf-8的具体编码实现,我引用了阮一峰老师的文章的部分内容,他总是能把复杂的东西讲的很简单。 地址为:ascii_unicode_and_utf
(以下内容为引用内容)
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0
,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n
字节的符号(n > 1
),第一个字节的前n
位都设为1
,第n + 1
位设为0
,后面字节的前两位一律设为10
。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
下表总结了编码规则,字母x
表示可用编码的位。
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
跟据上表,解读 UTF-8 编码非常简单。如果一个字节的第一位是0
,则这个字节单独就是一个字符;如果第一位是1
,则连续有多少个1
,就表示当前字符占用多少个字节。
总结
好开心,大概的把这些东西都有了一些了解。不再只是停留在认识阶段,而是了解阶段了。算是对这两天的一个交代了。