基本名词
编码 是信息从一种形式或格式转换成另一种形式的过程。 解码 是编码的逆过程
以上是维基百科上对编码与解码的解释,可以说这是一个通俗的解释,但是这个地方要是放在计算机编码上可以说是有些歧义。
编码(Encoding)专门是指从字符转变成字节(位元组),解码(Decoding)则是从字节(位元组)转换成字符(此句话存疑,因为Base64是字符到字符的编码,但是Base64是否算作是我们所讲的编码, 这点待定。)
感觉改成上述的应该会比较准确, 以上是作为动词说明的, 作为名词就代表着一个具体的实现方式,比如 ASCII编码, GBK编码 等等
字符集 多个字符的集合(Unicode字符集, ASCII 字符集, GBK字符集。。。)
字符编码 按照何种规则存储字符
下图说明了字符集和字符编码的关系
拉丁字母 也叫罗马字母,是当今世界上使用最广的字母系统,基本的拉丁字母就是我们见到的ABCD等26个字母, 原先是欧洲那边使用, 后来扩展到美洲等地。总的来说,现在欧洲多数国家,美洲,澳洲,非洲多数国家都是用的拉丁字母,其中欧洲很多国家,是对已有的26个基本的拉丁字母加上连字,变音字符,转换成衍生拉丁字母,但是还是属于拉丁字母
码元(编码单元)能用于处理或交换编码文本的最小比特组合, 编码后的最小单位
字符 是指字母,数字,标点, 表意文字,符号,或者其他文本形式的书写“原子”
抽象字符 抽象字符就是抽象的字符,像a这样的字符是有形的, 但是在计算机中, 有很多的字符都是空白的, 甚至是不可打印的。比如ASCII中的NULL,就是一个抽象字符
ps: \x00, \000, NULL, 0 这些写法都只是这个抽象字符的某种表现形式, 而不是这个抽象字符本身
现代编码模型
在现代编码模型里面要知道一个字符如何映射成计算机里面的比特, 需要经过如下几个步骤
知道一个系统需要支持哪些字符, 这些字符的集合被称为字符集(character repertoire), 正式名称叫抽象字符集(Abstract Character Repertoire)ACR 它具有无序性 –这个字符集同python中的set类似
给字符表里的抽象字符编上一个数字(这个数字称作码位(Code Point)), 也就是字符集合到一个整数集合的映射。这种映射称为编码字符集(CCS: Coded Character Set)unicode属于这一层的概念, unicode跟计算机里面的进制没有任何关系, 它是完全字符抽象的 –跟python中的dict类似 (key是字符, value是码位)
- 这里有一个问题, 既然CCS已经将字符转成整数了, 为什么不将这个整数直接存储到计算机的内存或者硬盘里面?
因为CCS比如Unicode , 是一个开放的字符集, 未来可能有更多的符号加入, 这个可能是无限的。但是计算机整形能表示的范围是有限的,必须有一种算法进行调和, 这个解决方案就是CEF
3. 将CCS里的字符对应的整数转成 有限长度的比特值(将码位映射成码元序列), 便于以后计算机使用一定长度的二进制形式表示该整数。这个对应关系被称为字符编码表(CEF: Character Encoding Form) UTF-8, UTF-16都是属于这层
对于CEF得到的比特值如何在计算机中进行存储,传输, 因为存在大小端的问题, 这就会跟具体的操作系统相关了这种解决方案被称为字符编码解决方案(CES: Character Encoding Scheme)
传输编码语法是现代编码模型的最顶层, 通过CES, 我们已经可以将一个字符表示为一个字节序列。但是有时候, 字节序列表示还不够, 比如在HTTP协议中, 在URL里面, 一些字符是不允许出现的, 这个时候就需要再次对字节进行编码
编码的历史
首先第一个出现的编码就是ASCII码,全称(American Standard Code for Information Interchange)美国信息交换标准代码,这套编码只支持基本的拉丁字母。这种编码只有128个字符, 占用了一个字节中的后七位. 这时因为计算机只在美国国内使用, 所以这些字符已经够了。其中 0-31和 127这33个字符属于控制字符(Control character)剩下的32-126这95个字符属于可打印字符(printable characters)包含数字,大小写字母, 常用符号等等 这里详细说一下Base64编码 为什么叫Base64呢?因为这种算法只支持64个可打印字符, 2^6 == 64 , 每6bit为一个单元, 对应某个可打印字符, 正常三个字节有24bit, 对应Base64编码就是4个Base64单元, 即4个可打印字符表示3个字节, (这样无形之间增加了字符的数量, 多占用了1/3 的字节长度, 以此为代价得到了更好的兼容性) 它可以将ascii 码里面的控制字符(不可打印字符)甚至是ASCII码之外的字符都转换成可打印的6bit字符
base64 != url_encode(百分号编码), 这是两种不同的编码方式,
> base64 == ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=
> url_encode == %21%23%24%26%27%28%29%2A%2B%2C%2F%3A%3B%3D
提出一个疑问 所有的http传输都会将byte数组进行base64编码, 因为http协议是文本协议,并不同于二进制协议(例如Trift), 那么直接把Byte数组转换成String不就可以了吗?为什么非要用Base64?url中有一些特殊字符, 类似 ‘/’, ‘&’ 等字符, 如果直接使用String, 某种情况下势必会掺入这些特殊字符,这种方式大部分是用urlencode来解决的
有一种先用base64 编码之后再使用urlencode 进行双重编码的方式,主要原因大概是因为urlencode 编码之后英文部分是不变的, 为了使参数不可读, 在urlencode之前,先进行base64编码
首先base64最早是用于发送邮件的, 在MIME格式的电子邮件中,原本只能传输ascii的文本数据,但是如果这样,一些类似于图片附件的数据(大于127)就变得无法传输。 base64可以用来 将二进制字节序列数据 编码成ASCII字符序列构成的文本,
之后随着计算机的推广到欧洲, 人们发现127位的字符已经不够用了, 所以在这个基础上将最后一位用上了, 扩展到了255位,用上了衍生拉丁字母,这样的标准一共有两个一个是EASCII,一个是ISO 8859 1. 从128~255这一页的字符集被称作扩展字符集。整体被称作(EASCII Extend ASCII)目前很少用 2. ISO 8859 是一系列规则的总称, 它包含了15个字符集,ISO/IEC 8859-n 其中n = 1~11, 13~16. 不同字符集之间除了前127个之外没有交集, 互相之间不可转换, 这么做避免了使用两个字节存储, 节约了空间, 缺点就是无法同时兼容两个字符集的字符(ISO 8859-1 & ISO 8859-2 之间不兼容)
ps ISO 8859 与 ISO-8859 不是一个东西。。。。。简单说, ISO-8859包含ISO 8859
到了中国, 人们发现256位的字符已经不够了, 于是我们便将原先的一个字符扩展到两个字符, 前127个字符保持不变, 127号之后的扩展码全部不要, 并且规定一个小于127的字符的意义与原先相同, 但是两个大于127的字符连接在一起的时候, 就表示一个汉字(即汉字的高字节的首位和低字节的首位必须是1),前边的一个字节(称作为高字节)和后边的一个字节(低字节)组合便可组成大约7000多汉字了, 原先ASCII码里面存在的字符(数字,符号,字母)重新编码成为两个字节长的编码(就是我们说的全角字符), 原先127位之下的便叫做半角字符,起了个名字叫GB2312 后来发现,还是不够,就不再要求低字节一定是127号之前的编码, 只要第一个字节大于127便表示这个是一个汉字的开始(高字节的首位要求是1, 但是低字节的首位可以不为1), 对于这次扩展起了一个新的名字叫GBK(k == kuozhan)之后又进行了扩展变成了GB18030, 这些都是用两个字节长的编码一个汉字等于两个英文字符
这些区域性的编码在互联网未出现之前可以满足需求, 但是在互联网兴起之后,大家发现必须有一个统一的编码来支持不同国家之前的数据传输,接下来就到了大一统时代 首先, 国际标准组织ISO 定义了对应得编码标准 ISO/IEC 10646 简称为ISO 10646 此标准所定义的字符集,称作通用字符集(Universal Character Set)UCS, 我们平时会看到的 UCS-2, UCS-4 就是对应的ISO 10646标准中所定义的,用两个字节或者四个字节去表示同一个字符 接下来就是多语言软件制造商组成的统一码联盟开发的 unicode 到后期两者开始合并,为了创建一个单一的编码表而协同工作 一个包含地球上所有字符的字符集(只是字符集)对于我们来说
unicode不支持地方的编码, 但是唯一支持ASCII。所以unicode 与 ASCII码不必特意区分, Unicode的编码方式与上面提到的ISO 10646的UCS概念相对应,目前实际应用的Unicode版本对应UCS-2, **使用两个字节的UCS字符集, 使用16位编码空间**, 理论上最多可以表示2^16=65536个字符, 基本上满足各种语言的使用。这种16位Unicode字符构成了基本多文种平面(Basic Multilingual Plane)BMP 最新的Unicode使用了定义了16个辅助平面, 两者结合至少占据21位的编码空间,比三个字节较少, 但是理论上辅助平面是占用4字节的编码空间的与UCS-4保持一致 Unicode = ISO 10646编码标准 = 标准所制定的UCS字符集
UTF(Unicode Transformation Format) 是unicode里面的编码方式, 其中最常用的就是utf-8, **utf-8**(每个码元8位)和 **utf-16** (每个码元16位)是变长的编码方式ps: unicode 是一个字符集, 每个字符有对应的唯一的一个码位例如 ‘\u597d’,这个代码值不可以直接使用,所以我们需要对这个代码进行编码 这个代码可以转成任何编码(gbk, utf)真正存储的时候需要用到多少个字节是由具体的编码格式决定的(既unicode没有长度), 比如: 字符(A)用UTF-8的格式编码来存储就只占用一个字节, 用UTF-16就占用2个字节, 而用UTF-32就占用4个字节
这里简单的说一下UTF-8的编码方式这里的分别采用了1,2,4字节的码元, 正好对应了计算机中最常见的三种整形长度
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的
- 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的Unicode码
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 | 11110xxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
BOM
byte-order mark 字节顺序标记
这里有一个困扰多年的问题, 为什么UTF-8没有字节序
1. 编码单元(码元)与编码单元在网络中传输的顺序是确定的,即使是多字节编码方案,在网络层传输是没有问题的, 比如a, b, c 分别代表三个字节, 发送时是a, b, c 接收的时候一样是a, b, c 这个顺序不会乱。我们经常会想utf8是多字节编码, 怎么会不存在字节序的问题, 这一条就很好的解答了这个问题
2. 字节序指的是编码单元内部的字节顺序,utf8是变长编码, 而且单字节为编码单元, 不存在谁高位, 谁低位的问题, 所以不存在顺序问题。 utf16, utf32是定长编码, 这里拿utf16举例, 总是以两个字节为编码单元,编码单元和编码单元之间是有序的, 但是以两个字节为编码单元,编码单元内部的字节与字节之间的顺序无法保证
3. 这些都是属于第四步的内容, 平时不会接触到
由于硬件CPU的不同, 编码单元内部字节与字节的顺序不确定
所以就有了BOM
python中的编码
python有一种通用的UnicodeError异常,其中答题包含两类
- UnicodeEncodeError (将str转换成二进制序列)
UnicodeDecodeError (将二进制序列转成str)
大部分非UTF编码器(ISO 8859 & gbk)都只能处理小部分Unicode字元, 如果将文字转成字节的时候,编码方法没有定义字元,就会出现UnicodeEncodeError
当位元组并不属于UTF-8 or UTF-16 的时候(包含ASCII),从二进位序列转换成文字的时候,发现意外的字节的时候,就会报UnicodeDecodeError
载入代码,内含未知编码的时候, 会产生SyntaxError
在 Python 2 中: str = 字节流, unicode = 字符串
在Python2.6 也有加入 bytes, 但是那只是 str形态的别名而已。
在 Python 3 中: bytes = 字节流, str = unicode = 字符串
"测试".encode()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 0: ordinal not in range(128)
这块会有一个问题,明明是自己在做encode()操作, 为什么报了一个UnicodeDecodeError 这是因为 python在调用 str.encode()的时候, 实际上的操作是
"测试".decode().encode() # python 中存在很多这样的操作
本文参考了
2018.3.8 更新:
更新了base64 & urlencode 相关的信息