思考一下, 为什么有字符编码这种东西?
当然是为了让计算机 "听话" 呗. 我们知道, 计算机的世界只有 01 这两个字符, 而我们现实世界有成千上万的字符. 如何用 01 的组合去和现实中的字符一一对应呢? 这就是需要制定相应的编码规则来实现了. 明白了这点, 我们正式开始编码的讲解.
ASCII 码
我们知道, 在计算机内部, 所有的信息最终都表示为一个二进制的字符串. 每一个二进制位 (bit) 有 0 和 1 两种状态, 因此八个二进制位就可以组合出 256 种状态(-128~127), 这被称为一个字节(byte). 也就是说, 一个字节一共可以用来表示 256 种不同的状态, 每一个状态对应一个符号, 就是 256 个符号, 从 0000000 到 11111111.
上个世纪 60 年代, 美国制定了一套字符编码, 对英语字符与二进制位之间的关系, 做了统一规定. 这被称为 ASCII 码, 一直沿用至今.
ASCII 码一共规定了 128 个字符的编码, 比如空格 "SPACE" 是 32(二进制 00100000), 大写的字母 A 是 65(二进制 01000001). 这 128 个符号(包括 32 个不能打印出来的控制符号), 只占用了一个字节的后面 7 位, 最前面的 1 位统一规定为 0.
ASCII 码用了 1 个字节, 1 个字节可以表示 256 种状态, 但 ASCII 码只用了 128 种, 也就是一个字节的后七位, 最前面的 1 位都是 0.
非 ASCII 编码
英语用 128 个符号编码就够了, 但是用来表示其他语言, 128 个符号是不够的. 比如, 在法语中, 字母上方有注音符号, 它就无法用 ASCII 码表示. 于是, 一些欧洲国家就决定, 利用字节中闲置的最高位编入新的符号. 比如, 法语中的 é 的编码为 130(二进制 10000010). 这样一来, 这些欧洲国家使用的编码体系, 可以表示最多 256 个符号.
但是, 这里又出现了新的问题. 不同的国家有不同的字母, 因此, 哪怕它们都使用 256 个符号的编码方式, 代表的字母却不一样. 比如, 130 在法语编码中代表了 é, 在希伯来语编码中却代表了字母 Gimel (ג), 在俄语编码中又会代表另一个符号. 但是不管怎样, 所有这些编码方式中, 0-127 表示的符号是一样的, 不一样的只是 128-255 的这一段.
至于亚洲国家的文字, 使用的符号就更多了, 汉字就多达 10 万左右. 一个字节只能表示 256 种符号, 肯定是不够的, 就必须使用多个字节表达一个符号. 比如, 简体中文常见的编码方式是 GB2312, 使用两个字节表示一个汉字, 所以理论上最多可以表示 256x256=65536 个符号.
中文编码的问题需要专文讨论, 这篇笔记不涉及. 这里只指出, 虽然都是用多个字节表示一个符号, 但是 GB 类的汉字编码与后文的 Unicode 和 UTF-8 是毫无关系的.
Unicode 编码
正如上一节所说, 世界上存在着多种编码方式, 同一个二进制数字可以被解释成不同的符号. 因此, 要想打开一个文本文件, 就必须知道它的编码方式, 否则用错误的编码方式解读, 就会出现乱码. 为什么电子邮件常常出现乱码? 就是因为发信人和收信人使用的编码方式不一样.
可以想象, 如果有一种编码, 将世界上所有的符号都纳入其中. 每一个符号都给予一个独一无二的编码, 那么乱码问题就会消失. 这就是 Unicode, 就像它的名字都表示的, 这是一种所有符号的编码.
Unicode(统一码, 万国码, 单一码)是计算机科学领域里的一项业界标准, 包括字符集, 编码方案等. Unicode 是为了解决传统的字符编码方案的局限而产生的, 它为每种语言中的每个字符设定了统一并且唯一的二进制编码, 以满足跨语言, 跨平台进行文本转换, 处理的要求.
Unicode 是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案. 目前的 Unicode 字符分为 17 组编排, 0x0000 至 0x10FFFF, 每组称为平面(Plane), 而每平面拥有 65536 个码位, 共 1114112 个. 然而目前只用了少数平面. UTF-8,UTF-16,UTF-32 都是将数字转换到程序数据的编码方案.
Unicode 当然是一个很大的集合, 现在的规模可以容纳 100 多万个符号. 每个符号的编码都不一样, 比如, U+0639 表示阿拉伯字母 Ain,U+0041 表示英语的大写字母 A,U+4E25 表示汉字 "严". 具体的符号对应表, 可以查询 unicode.org, 或者专门的汉字对应表.
Unicode 的问题
需要注意的是, Unicode 只是一个符号集, 它只规定了符号的二进制代码, 却没有规定这个二进制代码应该如何存储. 比如, 汉字 "严" 的 unicode 是十六进制数 4E25, 转换成二进制数足足有 15 位(100111000100101), 也就是说这个符号的表示至少需要 2 个字节. 表示其他更大的符号, 可能需要 3 个字节或者 4 个字节, 甚至更多.
这里就有两个严重的问题: 1. 如何才能区别 unicode 和 ascii? 计算机怎么知道三个字节表示一个符号, 而不是分别表示三个符号呢? 2. 我们已经知道, 英文字母只用一个字节表示就够了, 如果 unicode 统一规定, 每个符号用三个或四个字节表示, 那么每个英文字母前都必然有二到三个字节是 0, 这对于存储来说是极大的浪费, 文本文件的大小会因此大出二三倍, 这是无法接受的.
它们造成的结果是:
出现了 unicode 的多种存储方式, 也就是说有许多种不同的二进制格式, 可以用来表示 unicode.
unicode 在很长一段时间内无法推广, 直到互联网的出现.
UTF-8
互联网的普及, 强烈要求出现一种统一的编码方式. UTF-8 就是在互联网上使用最广的一种 unicode 的实现方式. 其他实现方式还包括 UTF-16 和 UTF-32, 不过在互联网上基本不用. 重复一遍, 这里的关系是, UTF-8 是 Unicode 的实现方式之一.
UTF-8 最大的一个特点, 就是它是一种变长的编码方式. 它可以使用 1~4 个字节表示一个符号, 根据不同的符号而变化字节长度.
UTF-8 的编码规则很简单, 只有二条:
对于单字节的符号, 字节的第一位设为 0, 后面 7 位为这个符号的 unicode 码. 因此对于英语字母, UTF-8 编码和 ASCII 码是相同的.
对于 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 编码.
已知 "严" 的 unicode 是 4E25(100111000100101), 根据上表, 可以发现 4E25 处在第三行的范围内(0000 0800-0000 FFFF), 因此 "严" 的 UTF-8 编码需要三个字节, 即格式是 "1110xxxx 10xxxxxx 10xxxxxx". 然后, 从 "严" 的最后一个二进制位开始, 依次从后向前填入格式中的 x, 多出的位补 0. 这样就得到了,"严" 的 UTF-8 编码是 "11100100 10111000 10100101", 转换成十六进制就是 E4B8A5.
那 unicode 和 UTF-8 有何区别?
Unicode 是「字符集」
UTF-8 是「编码规则」
字符集: 为每一个「字符」分配一个唯一的 ID(学名为码位 / 码点 / Code Point)
编码规则: 将「码点」转换为字节序列的规则
参考: 字符编码笔记: ASCII,Unicode,UTF-8 和 Base64
BCD 码
上面讲的是字符编码, 是指一个字符对应的一个二进制数. 而 BCD 码是计算机在对十进制数做运算或存储时采用的二进制格式.
Binary-Coded Decimal, 简称 BCD, 称 BCD 码或二 - 十进制代码, 亦称二进码十进数. 是一种二进制的数字编码形式, 用二进制编码的十进制代码. 这种编码形式利用了四个位元来储存一个十进制的数码, 使二进制和十进制之间的转换得以快捷的进行.
BCD 码的优点是效率高: 比如十进制要以二进制的形式在计算机中存储, 十进制直接转换成与之对应的 BCD 码比十进制通过除法取余再转换的效率来的高.
Base64 编码
定义: Base64 是网络上最常见的用于传输 8Bit 字节码的编码方式之一, Base64 就是一种基于 64 个可打印字符来表示二进制数据的方法.
为什么会有 base64?
由于 HTTP 协议是文本协议, 所以在 HTTP 协议下传输二进制数据需要将二进制数据转换为字符数据. 然而直接转换是不行的. 因为网络传输只能传输可打印字符.
问: 什么是 "可打印字符" 呢?
答: 在 ASCII 码中规定, 0~31,128 这 33 个字符属于控制字符, 32~127 这 95 个字符属于可打印字符, 也就是说网络传输只能传输这 95 个字符, 不在这个范围内的字符无法传输.
问: 那么该怎么才能传输其他字符呢?
答: 其中一种方式就是使用 Base64.Base64 一般用于在 HTTP 协议下传输二进制数据.
base64 实现原理
Base64 的索引与对应字符的关系如下表所示:
也就是说, 如果将索引转换为对应的二进制数据的话需要至多 6 个 Bit(2^6=64). 然而 ASCII 码需要 8 个 Bit 来表示, 那么怎么使用 6 个 Bit 来表示 8 个 Bit 的数据呢? 6 个 Bit 当然不能存储 8 个 Bit 的数据, 但是 46 个 Bit 可以存储 38 个 Bit 的数据啊
可以看到 "Son" 通过 Base64 编码转换成了 "U29u". 这是刚刚好的情况, 3 个 ASCII 字符刚好转换成对应的 4 个 Base64 字符. 但是, 当需要转换的字符数不是 3 的倍数的情况下该怎么办呢? Base64 规定, 当需要转换的字符不是 3 的倍数时, 一律采用补 0 的方式凑足 3 的倍数, 具体如下表所示:
每 6 个 Bit 为一组, 第一组转换后为字符 "U", 第二组末尾补 4 个 0 转换后为字符 "w". 剩下的使用 "=" 替代. 即字符 "S" 通过 Base64 编码后为 "Uw==". 这就是 Base64 的编码过程.
好了, 原理懂了, 那么如果要进行 base64 编码, 我们该怎么做呢? 自己撸一个方法? 找一个库? 都行, 但是 html 规范中已经规定了 base64 转换的 API,Windows 对象上可以访问到 base64 编码和解码的方法, 直接调用即可.
- Windows.atob() // 对 base64 编码过的字符串进行解码
- Windows.btoa() // 对 ASCII 编码的字符串进行 base64 编码(不支持汉字, 汉字可通过 URIencode 预处理后再编码)
base64 有哪些应用场景
前端将较小的 icon 编码为 base64 直接在文档中加载, 减少 http 请求
电子邮件传输二进制文件时, 通常用 base64 编码后再传
注意
base64 编码后的数据量是要比编码前大的, 所以 base64 不能用于减少数据量.
base64 不能用于加密数据, 即使使用私有的索引表也是不安全的.
关于转中文出错: btoa("中文") // The string to be encoded contains characters outside of the Latin1 range. 意思就是超出支持范围, ASCII.
但是, 如果你非要使用 btoa 来 base64 转码中文, 也不是不行, 就是略微蛋疼. 如下:
- btoa(escape("中文")) // "JXU0RTJEJXU2NTg3"
- unescape(atob("JXU0RTJEJXU2NTg3")) // "中文"
- btoa(encodeURI("https://zweizhao.com / 文章 / JS 常用转码 URI 与 Base64.md"))
- // "aHR0cHM6Ly96d2Vpemhhby5jb20vJUU2JTk2JTg3JUU3JUFCJUEwL0pTJUU1JUI4JUI4JUU3JTk0JUE4JUU4JUJEJUFDJUU3JUEwJTgxVVJJJUU0JUI4JThFQmFzZTY0Lm1k"
- decodeURI(atob("aHR0cHM6Ly96d2Vpemhhby5jb20vJUU2JTk2JTg3JUU3JUFCJUEwL0pTJUU1JUI4JUI4JUU3JTk0JUE4JUU4JUJEJUFDJUU3JUEwJTgxVVJJJUU0JUI4JThFQmFzZTY0Lm1k"))
- // "https://zweizhao.com / 文章 / JS 常用转码 URI 与 Base64.md"
- btoa(encodeURIComponent("https://zweizhao.com / 文章 / JS 常用转码 URI 与 Base64.md"))
- // "aHR0cHMlM0ElMkYlMkZ6d2Vpemhhby5jb20lMkYlRTYlOTYlODclRTclQUIlQTAlMkZKUyVFNSVCOCVCOCVFNyU5NCVBOCVFOCVCRCVBQyVFNyVBMCU4MVVSSSVFNCVCOCU4RUJhc2U2NC5tZA=="
- decodeURIComponent(atob("aHR0cHMlM0ElMkYlMkZ6d2Vpemhhby5jb20lMkYlRTYlOTYlODclRTclQUIlQTAlMkZKUyVFNSVCOCVCOCVFNyU5NCVBOCVFOCVCRCVBQyVFNyVBMCU4MVVSSSVFNCVCOCU4RUJhc2U2NC5tZA=="))
- // "https://zweizhao.com / 文章 / JS 常用转码 URI 与 Base64.md"
参考:
从 base64 到 atob 和 btoa 的一些理解 https://segmentfault.com/a/1190000016379916
JS 常用转码 URI 与 Base64
MIME 类型
每个 MIME 类型由两部分组成, 前面是数据的大类别, 例如声音 audio, 图象 image 等, 后面定义具体的种类.
常见的 MIME 类型(通用型):
超文本标记语言文本 .HTML text/HTML
xml 文档 .xml text/xml
XHTML 文档 .xhtml application/xhtml+xml
普通文本 .txt text/plain
RTF 文本 .rtf application/rtf
PDF 文档 .PDF application/PDF
Microsoft Word 文件 .Word application/msword
PNG 图像 .PNG image/PNG
GIF 图形 .gif image/gif
JPEG 图形 .jpeg,.jpg image/jpeg
au 声音文件 .au audio/basic
MIDI 音乐文件 mid,.midi audio/midi,audio/x-midi
RealAudio 音乐文件 .ra, .ram audio/x-pn-realaudio
MPEG 文件 .mpg,.mpeg video/mpeg
AVI 文件 .avi video/x-msvideo
GZIP 文件 .gz application/x-gzip
TAR 文件 .tar application/x-tar
任意的二进制数据 application/octet-stream
URI 编码解码
URL 传输过程?
HTTP 协议中参数组件的传输是 key=value 键值对的形式, 如果要传输多个参数就需要用 "&" 符号对键值对进行分隔. 例如? name1=value1&name2=$value2, 这样在服务器收到这种字符串的时候, 会用 "&" 分隔出每一个参数, 然后再用 "=" 来分隔出参数值.
针对 name1=value1&name2=value2 我们来说一下客户端到服务器端的概念上解析过程:
上述字符串在计算机中用 ASCII 码 (16 进制) 表示为: 6E616D6531 3D 76616C756531 26 6E616D6532 3D 76616C756532
服务器端在接收到该数据后就可以遍历该字节流, 首先一个字节一个字节的读取, 当读到 3D 这个字节的时候, 服务器端就知道前面读到的字节串表示一个 key, 继续读取, 如果遇到了 26, 表示从刚才读到的 3D 到 26 字节之间的字节串是上一个 key 的 value, 按照此方法就可以解析出客户端传过来的参数.
现在又这样一个问题: 如果我的参数值中就包含 = 或者 & 这样的特殊子字符的时候, 该怎么办. 比如说 name1=value1, 其中 value1 的值是 va&lu=e1, 那么在传输过程中就会变成 name1=va&lu=e1. 用户传输的本意是只有一个键值对, 但是服务器端会解析成两个键值对, 这样就自然的产生了歧义.
如何解决上述问题带来的歧义呢? 解决之法就是对 URL 进行编码!!!
URL 编码只是简单的在特殊字符的各个字节 (16 进制) 前加上 "%" 即可. 例如, 我们对上述会产生歧义的字符 ("va&lu=e1") 进行编码后的结果: name1=va&lu=, 这样服务器会把紧跟在 "%" 后的字节当成普通的字节, 不会把它当成各个参数或键值对的分隔符.
另外一个问题是, 为什么要用 ASCII 码传输, 可不可以用别的编码?
因为一些历史的原因 URL 设计者使用 US-ASCII 字符集表示 URL.(原因比如 ASCII 比较简单; 所有的系统都支持 ASCII). 当然可以用别的编码, 你可以自己开发一套编码然后自己进行解析. 就像大部分国家都有自己的语言一样. 但是国家之间要怎么进行交流呢, 用英语吧, 英语的使用范围最广.
通常如果一样的东西需要编码, 就说明这样的东西并不适合传输. 至于原因有多种多样, size 过大, 包含隐私数据等等. 对于 URL 来说, 之所有要进行编码, 是因为 URL 中有些字符会引起歧义.
例如, URL 参数字符串中如果包含 "&" 或者 "%" 势必会造成服务器解析错误, 所以需要对其进行编码.
又如, URL 的编码格式采用的是 ASCII 码而不是 Unicode, 这也就是说你不能在 URL 中包含任何非 ASCII 字符, 比如中文. 否则如果客户端浏览器和服务器端浏览器支持的字符集不同的情况下, 中文可能会造成问题.
URL 编码的原则就是使用安全的字符 (没有特殊用途或者特殊意义的可打印字符) 去表示那些不安全的字符.
哪些字符需要编码
RFC3986 文档规定, URL 中只允许包含英文字母(a-zA-Z), 数字(0-9),- _ . ~4 个特殊字符以及所有的保留字符. RFC3986 文档对 URL 的编码解码问题做出了详细的建议, 指出了哪些字符需要被编码才不会引起 URL 语义的转变, 以及对为什么这些字符需要编码做出了相应的解释.
US-ASCII 字符集中没有对应的可打印字符: URL 中只允许使用可打印的字符. US-ASCII 码中的 10-7F 字节全都表示控制字符, 这些字符不能直接出现在 URL 中. 同时对于 80-FF 字节, 由于已经超出了 ASCII 码定义字符的范围, 因此也不能放在 URL 中.
保留字符: RUL 可以划分为干了组件, 协议, 主机, 路径等. 有一些字符 (: / ? # [ ] @) 是用作分隔不同组件的. 例如: 冒号用于分隔协议和主机组件, 斜杠用于分隔主机和路径, 问号用于分隔路径和查询参数, 等等. 还有一些字符 (! $ & * + , ; =) 用于在每个组件中起到分隔作用, 如等号用于表示查询参数中的键值对,& 符号用于分隔查询多个键值对. 当组件中的普通数据包含这些特殊字符时, 需要对其进行编码.
RFC3986 中指定了以下字符为保留字符: ! * ' ( ) ; : @ & = + $ , / ? # [ ]
不安全字符: 还有一些字符, 当他们直接放在 URL 中的时候, 可能会引起解析程序的歧义. 这些字符被视为不安全的字符, 原因有很多.
空格: URL 在传输的过程, 或者用户在排版的过程中, 或者文本处理程序在处理 URL 的过程, 都有可能引入无关紧要的空格, 或者将那些有意义的空格给去掉.
引号 以及 <>: 引号和尖括号通常用于在普通文本中起到分隔 URL 的作用.
警号 #: 通常用于表示书签或者锚点.
%: 百分号本身用作对不安全的字符进行编码是使用的特殊字符, 因此本身需要编码.
{ } | ^ [ ] ' ~: 某一些网关或者传输代理会篡改这些字符
需要注意的是, 对于 URL 中的合法字符, 编码和不编码是等价的, 但是对于上边提到的这些字符, 如果不经过编码, 那么它们可能会造成 URL 语义的不同. 因此对于 URL 而言, 只有普通英文字符和数字, 特殊字符 $ - _ . + ! * ' ( )还有保留字符, 才能出现在未经编码的 Url 中, 其他字符均需要编码之后才能出现在 URL 中.
但是由于历史原因, 目前尚存在一些不标准的编码实现, 例如对于 "~" 符号, 虽然 RFC3986 文档规定, 对于波浪号~ 不需要进行 URL 编码, 但是还是有很多老的网关或者传输代理会进行编码.
如何对 URL 中的非法字符进行编码?
URL 编码通常也被称为百分号编码, 是因为它的编码方式非常简单, 使用 % 加上两位字符 ---[0-9A-F]--- 代表一个字节的十六进制的形式. URL 编码默认使用的字符集是 US-ASCII 码, 例如 a 在 US-ASCII 码中对应的字节值是 0x61, 那么 URL 编码之后得到的就是 a, 我们在地址栏中输入 http://g.cn/search?q=abc, 实际上就等于在 google 中搜索 abc. 又如 @符号在 ASCII 字符集中对应的字节为 0x40, 经过 URL 编码之后得到的就是 @.
对于非 ASCII 字符, 需要使用 ASCII 字符集的超集进行编码得到相应的字节, 然后对每个字节执行百分号编码. 对于 Unicode 字符, RFC 文档建议使用 utf-8 对其进行编码得到相应的字节, 然后对每个字节执行百分号编码. 如 "中文" 使用 UTF-8 编码得到的字节是 0xE4 0xB8 0xAD 0xE6 0x96 0x87, 经过 URL 编码之后得到 中文.
如果某个字符对应的 ASCII 字符集中的某个非保留字符, 则此字节无需使用百分号表示. 例如 "Url 编码", 使用 UTF-8 编码得到的字节是 0x55 0x72 0x6C 0xE7 0xBC 0x96 0xE7 0xA0 0x81, 由于前三个字节对应着 ASCII 中的非保留字符 "Url", 因此这三个字节可以用非保留字符 "Url" 表示. 最终 "Url 编码" 经过编码之后得到的是 Url编码, 当然, 如果你用 Url编码 也是可以的.
由于历史原因, 有一些 Url 编码实现并不完全遵循这样的原则. JS 中提供 3 个函数对 URL 进行编码和解码: escape/unescape,encodeURI/decodeURI,encodeURIComponent/decodeURIComponent.
区别
这三对函数的安全字符 (即不需要编码的字符) 范围也不同, 如下所示:
- escape(69 个):/@+-._0-9a-zA-Z
- encodeURI(82 个):!#$&'()+,/:;=?@-._~0-9a-zA-Z
- encodeURIComponent(71 个):!'()*-._~0-9a-zA-Z
现在对比 encodeURI 和 encodeURIComponent, 从名称上可看出 encodeURI 是针对整个 URI 进行编码, 我们以特殊的 URI--URL 来说明下.
对于 URL 为 http://www.baidu.com 而言, 如果用 encodeURI 编码, 返回的仍是 "http://www.baidu.com"; 如果用 encodeURIComponent 编码, 返回的为 "http://www.baidu.com".
encodeURI 所针对的是整个 URI, 并不会对分隔符如 /,?,= 符号进行编码, 否则破坏了 URI 的原有含义, 而 encodeURIComponent 则是针对 URI 的
某一部分进行编码, 如查询字符串部分的 & 会被转义.
参考: 为什么要进行 URL 编码
结尾彩蛋
关于字符编码, 来点有意思的 emoji 图标:, , , ♂, .
看看这些可爱的小图标, 放在上个世纪, 这只能用图片做, 但现在都这些都是一个个真实的字符. 感兴趣的可以研究下 Emoji 与 Unicode http://zablog.me/2017/09/18/emoji/ , 从 Emoji 的限制到 Unicode 编码 https://www.jianshu.com/p/64ec0f6b6245
来源: https://www.cnblogs.com/chenwenhao/p/11823695.html