在程序员生活的01世界中有两大Boss级难题,分别是缓存失效和命名问题,对比这两大难题来说,编码和解码只能算是小妖儿了,只不过这两个小妖儿出镜率很高,有时确实很磨人的,得多花些时间捋顺一下。
编码问题不仅仅出现在计算机中,广义的说,编码问题涉及到人类社会的方方面面,比如古人规定指定长度是一寸,然后规定十寸为一尺,其实就是当时人们对长度的一种编码,但是由于每个地方的编码不统一,导致人们在交流的时候出现了很多问题,直到秦始皇统一了文字、度量衡,相当于统一了描述当时社会的编码,使得知识和文明得以快速传播。
今天想说的编码和解码特指计算中使用的编码和解码,通俗点说:编码是给计算机看的,解码是为了让人能看懂的。可能大家对这句话还不太理解,不过没关系,这个说法本身不太严谨,也可以举出一些反例,但是大部分情况下确实是这样的。
为什么要编码,想想谍战片里近代社会中的发电报过程,滴答、滴滴答、滴滴滴答答就这个样子,怎么来表达“敌人发动进攻了”,这时候就用到了编码,提前约定好“滴答”代表“敌”,“滴滴答”代表“人”,这样在收到“滴答、滴滴答”你就知道了“敌人”这个信息,那个密码本记录的内容和规则其实就是对所有电传信息的一种编码。
计算机中的编码也是一样的,从我们开始接触到计算机的时候就听说过计算机只认识0和1,虽然现代计算机技术发展迅速,但是计算机只认识0和1这一点一直未变,所以你想让他看懂你的信息,保存你的数据,就要把这些信息和数据编码成0和1,计算机才能进行处理和存储。
所以计算机中为什么要对数据进行编码,这里可以给一个狭义的理解:计算机编码是为了让数据便于传输、存储和处理。
那有为什么要进行解码呢?其实就是为了人能看懂,给你一串二进制 01010111100011111111...
,相信你即使有最强大脑也不能迅速把所有数据解开,这可能是一篇优美的散文、一幅美丽的图画,或者是一部励志的电影,这一切都需要解码后才能知道。
本来想画一幅“编码”和“解码”这两个小妖的画像,但是作为灵魂画手的我还没构思好,此处留空,后面补充。。。
自从接触计算机就开始接触编码问题,比如你抄同学发来的作业文档,打开后却发现是一堆乱码,那时仅仅知道是编码错了,但是不知道怎么解决,或者直接让同学再发一份算了,后来在工作中需要做游戏多语言版本时才真正开始处理编码问题。
解决第一个编码问题大概是14年,当时做上线游戏的多语言版本、配置文件中的中文保存为 ANSI
编码,相同的配置文件放到日韩的系统上居然变成了其他的含义,查询解决方案决定使用 UTF-8
编码来保存配置文件,所以当时利用工具将所有的配置文件转换成了UTF-8编码,也是那个时候第一次接触到了Python,转换之后将其中的中文翻译成日韩的语言,从此知道了 UTF-8
这个编码方式,也清楚了在中日韩、越南、缅甸这个圈做产品,千万要远离 ANSI
编码。
其实 ANSI
并不是某一种特定的字符编码,而是一个编码集合,在不同的系统中,可以表示不同的编码,比如英文系统中的 ASCII
编码,简体中文系统中的 GBK
编码,韩文系统中 EUC-KR
编码等等
计算机是美国人发明用于科学计算的,所以他们也是第一批考虑编码的,而英文只有26个字母,所以他们发明了ASCII码,只使用了0-127这128个空间就表示了所有可能用的字符,但后来计算机技术飞速发展,已经不仅仅用于科学计算,已经融入到社会的方方面面,并且迅速在全球流行。
随着计算机火遍全球,其它国家发现自己国家经常使用的字符,在 ASCII 码中找不到啊,于是就有人想啊,ASCII 码中的一个字节中不是才用了一半吗,我们使用这个最高位来扩展把,于是很多国家就开始用最高位来扩展这个 ASCII 编码以便能够表示自己国家的一些字符,但是对于我博大精深的中国文化来说,这一个字节远远不够啊,我们的汉字那可就有好几万个,你就给我一个字节,我肯定不干。
既然一个字节搞不定,那我们就用两个字节好了,我们规定一个小于等于127的字符的意义与原来相同,此处为了兼容ASCII码,但两个大于127的字符连在一起时,就表示一个汉字,前一个字节从0xA1用到0xF7,后面一个字节从0xA1到0xFE,我们将常用的6000多汉字在这个范围内定义代码点,并将这种编码方式称为 GB2312
。
在 GB2312 这种编码中我们考虑了数学符号、希腊字母、全角标点等等,但是只有简体字没有繁体字啊,这下对面海岸的同胞们不乐意了,自己搞了一套 Big5
编码,用来处理繁体字。
后来随着电脑深入各个领域,常用汉字已经不能满足使用需求了,所以又把 GB2312 编码中没有使用的位置拿出来又进行代码点定义,处理了20000多个汉字,发明了 GBK
编码,但没过多久(2000年)发现还是不够用,又提出了变长的 GB18030
编码,每个字符占用1、2、4个字节。
刚刚简单提到了在中日韩这个圈里,每个国家都对 ASCII
编码进行了扩充,也就是对 ANSI
编码进行了自己的定义,通常是用两个字节来表示一个文字和符号,这样就出现了一种情况,相同的两个字节在不同的系统上显示了不同的文字,如果每个国家的人只使用自己的语言也是没问题的,但是当中日韩文字混排的时候就出现了问题,这两个字节到底应该转换成中日韩哪个国家的符号呢?
为了解决这种混乱的局面,大佬们设计了一种名为 Unicode
的字符集,又称万国码或者统一码。Unicode 的诞生是为整合全世界的所有语言文字。理论上任何字符在Unicode中都对应一个值,这个值被称为代码点,通常写成 \uABCD
的格式。
起初使用两个字节来表示代码点,其取值范围为 \u0000~\uFFFF,这种文字和代码点之间的对应关系被描述为UCS-2,也就是 Universal Character Set Coded in 2 octets 的缩写,最多可以记录65536个字符的代码点。
后来为了能表示更多的文字,人们又提出了UCS-4,即用四个字节表示代码点。它的范围为 \u00000000~\u7FFFFFFF,其中 \u00000000~\u0000FFFF和UCS-2是一样的。
从这里可以看出 UCS-4 与 UCS-2 只是一种扩展的关系,UCS-4 是兼容 UCS-2 的,在 UCS-2 的每个代码点加入两个值为0的字节就变成了 UCS-4。
这里的 LE
和 BE
指的是计算机中常提到的小端字节序和大端字节序,因为 UCS-4 是 UCS-2 的扩展,所以 UCS-4 也存在大端和小端的问题。
小端字节序,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,而大端字节序,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这和我们平时的阅读习惯一致。
如果没接触过大端和小端可能会有点懵,举个例子就明白了,C++中一个int类型的数字通常占4个字节,假如一个int类型的变量值是256,那么他再内存中是怎样表示的呢?我们知道计算机中除了1就是0,这在计算机中对应一个bit,而计算机中表示数据的单位是字节,每个字节有8个bit大小,那么int变量值 256 翻译成二进制是 00000000 00000000 00000001 00000000
一共占用4个字节。
对照前面大端和小端的定义,这4个字节在内存中如果从高到低排列,就是小端字节序,如果这4个字节在内存中如果从低到高排列,就是大端字节序。因为UCS-2是两个字节表示一个代码点,所以在表示的时候存在字节排列顺序问题,对于值为 256 的这个代码点,可以是0x0100,也可以是0x0001。
Unicode 是一个字符集,这一点应该很好理解,它表示的是字符和代码点的对应关系,比如简体字“汉”对应的Unicode代码点是 \u6C49
,而 UCS-2 究竟是一种字符集还是一种编码方式呢?
我个人偏向于它是一种编码方式,因为它存在大端、小端这种说法,如果是一种字符集只会考虑对应关系,不会考虑字节序,这只是我个人观点,有些软件上确实是这样标注的,但有些文章也会把UCS-2当成一种字符集,这样也能说的通,不用太纠结这里的区别。
其实 UCS-2 编码对应的字符集是UCS,这些是历史原因导致的,一方面国际标准化组织(ISO)于1984年创建ISO/IEC JTC1/SC2/WG2工作组,试图制定一份通用字符集(Universal Character Set,简称UCS),并最终制定了ISO 10646标准。
而另一方面统一码联盟,也很想做这个统一编码的武林盟主,由Xerox、Apple等软件制造商于1988年组成,并且开发了Unicode标准。
然后1991年左右,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码。ISO也承诺,ISO 10646将不会替超出\u10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都独立存在,并独立地公布各自的标准。不过由于Unicode这一名字名字起的好,比较好记,因而它使用更为广泛。
从这段历史我们可以看到,虽然 UCS-4 将 UCS-2 从2个字节扩展成了4个字节,但是范围并没到使用到 \u00000000~\uFFFFFFFF,而是将范围集中到 \u000000~\u10FFFF 内,保证了 UCS 和 Unicode 各个字符代码点的统一,也奠定了UTF-8实现标准Unicode时最多需要4个字节的基础。
按理说 Unicode 已经给世界范围内的所有字符定义了代码点,无论是什么字符,使用4个字节都能表示出来,为什么要搞出一个UTF-8呢?是因为使用者发现,对于ASCII码范围内的字符,本来1个字节就能正确表示,现在居然要4个字节表示,即使使用 UCS-2编码,占用的空间也扩大了1倍,有些太浪费了。
为了解决这种空间浪费问题,就出现了一类变长的通用转换格式,即UTF(Universal Transformation Format),常见的UTF格式有:UTF-7,UTF-7.5,UTF-8,UTF-16 以及 UTF-32。
这类格式中最常见的就是 UTF-8 编码了,UTF-8 是针对于 Unicode 字符集中各个代码点的编码方式,是一种 Unicode 字符的实现方式,采用变长字节来表示Unicode编码,最长使用4个字节来表示标准的Unicode代码点,在有些资料中可能会看到5、6个字节的编码方式,这些都是非标准的Unicode代码点,根据规范,这些字节值将无法出现在合法 UTF-8序列中。
UTF-8在对标准Unicode字符编码时最多使用4个字节,其代码点范围与UTF-8编码后的形式对应如下:
Unicode/UCS-4(十六进制) | 字节数 | UTF-8编码格式(二进制) |
---|---|---|
000000-00007F | 1 | 0xxxxxxx |
000080-0007FF | 2 | 110xxxxx 10xxxxxx |
000800-00FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
010000-10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
只看上面这种对应关系,可能还不太清楚是怎样表示,接下来可以举一个例子试一下,比如一个常用的简体中文字——“好”,查询它的Unicode代码点是 \u597D
,对照上面的表格发现在 000800-00FFFF 这个范围,应该采用3个字节的表现形式。
先把这个数值翻译成二进制为 0101100101111101
,然后按照3个字节的形式分成3组,0101
、100101
和 111101
,把这些内容天填充到xxx这样的空位中就得到了“好”这个字的UTF-8编码—— 11100101 10100101 10111101
,表示成十六进制就是 0xE5A5BD
。
这个过程还是比较简单的,其他编码要转换成UTF-8编码都要经过Unicode这一步中转,先通过转换表查到其他编码对应字符的Unicode编码,然后再转换成UTF-8的表示格式。
根据 UTF-8 的编码规则,任何一个 byte 漏传,多传,传错只影响当前字符,前后字符都不受影响,而 Unicode 如果从一个字的中间截断会导致接下来所有的字符解析都是错的,这使得UTF-8编码的数据在不够可靠的网络传输中是有利的。
兼容ASCII,并且是字节顺序无关的。它的字节顺序在所有系统中都是一样的,因此它实际上并不需要BOM,不过在文件开头常常保存 0xEFBBBF 三个字节来表明文件编码是UTF-8。
缺点是因为UTF-8是一种变长编码,无法从直接从Unicode字符直接判断出UTF-8文本的字节数。除了ASCII字符集内的字符,其他情况实际上都增加了固定的头数据,占用了无效空间。
编码和解码在网站页面和数据库存储时用的非常多,一不小心就搞出一堆乱码,这种编码和解码操作在Python3中很直观,Python2中 string 和 bytes 混合在一起,编码和解码操作不太明显,而在python3中 string 和 bytes 是完全不同的两个类型,string编码成bytes,而bytes解码成string。
相比于python3中的编码、解码对应两个类型,C++中的编码和解码操作的前后都是字符串,这在一定程度上会给人造成误解,接下来我们使用Python3来简单测试一下编码和解码操作。
编码通常是把人类可以理解的字符转换成计算机可以认识二进制数据,这个过程在python3中对应的是把string转化成bytes,测试如下:
word = '好好'
print(type(word), word)
result = word.encode('utf-8')
print(type(result), result)
运行结果如下:
好好
b'\xe5\xa5\xbd\xe5\xa5\xbd'
解码操作通常是把计算机中存储和传输的数据转换成人类能看懂的字符,这个过程在python3中对应的是把bytes转化成string,测试如下:
data = b'\xe5\xa5\xbd\xe5\xa5\xbd'
print(type(data), data)
result = data.decode('utf-8')
print(type(result), result)
运行结果如下:
b'\xe5\xa5\xbd\xe5\xa5\xbd'
好好
从上面的两个例子来看编码和解码非常简单,那怎么还能出现乱码呢?计算机说到底还是一种工具,你在把可见字符编码后交给计算机存储和传输时,你要记住这些二进制的编码方式,在你想看这些数据时还要用相反的方式进行解码,否则就会出现乱码,比如下面这种使用 utf-8 编码,却使用 gbk 这种方式来解码,就得不到你想要的数据。
word = '好好'
print(type(word), word)
result = word.encode('utf-8')
print(type(result), result)
new_word = result.decode('gbk')
print(type(new_word), new_word)
运行结果如下:
好好
b'\xe5\xa5\xbd\xe5\xa5\xbd'
濂藉ソ
虽然结果是可以看得见的字符,但是这不是我们想要的数据,所以 濂藉ソ
对于我们来说也是一种乱码,在处理字符编码时我们必须清楚知道要用什么方式来进行编码和解码,如果编码和解码的方式不一致,那么就会产生乱码现象。
Unicode
是一种字符集,描述了人类范围内用于交流的所有字符的代码点,给与唯一的数字进行对应Unicode
规定的代码点范围是 \u000000-\u10FFFF,这与 UCS-4 规定的范围达成了统一,共定义了17个PlanUTF-8
是Unicode字符集的一种实现,采用变长的方式,标准规范最多使用4个字节表示一个Unicode字符对未知的事物充满恐惧,过于保守的看待当下的一切,有时候太稳反而会失去很多~