在我之前的编程生涯中,总是对一些关键概念糊里糊涂不求甚解,比如进程、线程,比如堆、栈、编码。我对这很多概念的理解,都来自于各式样的博客。
最近几年看书多些,了解到最好的学习方式是将获得的知识以自己熟悉的模式进行整理(那许多的博客,是他人对自己知识的整理;当然,有一些文章,是将知识融会贯通后发明的"新招式"),于是逼着自己一个月整理一篇技术博客。
最近正阅读的技术书籍有Effective Python、《编码》,它们都提到编码、字符集、ASCII、Unicode、UTF-8、str、bytes......等相关关键字。
在我理解"编码"时,再一次地产生晦涩感,于是4月的技术整理,是"我对编码的理解"。(我承认五一假期一直在玩耍,这篇文章,该在上周就整理完毕的。)
一、计算机展示文字的方式
这个世界,是先有文字然后才有计算机的。所以计算机展示文字(以"嘟"字举例)的方式,简单点说,可以是:将一张"嘟"字的图片放到屏幕上展示。
这句话可以拆成两个问题:
- 如何拿到图片?
- 如何在屏幕上展示图片?
"如何在屏幕上展示图片"并非理解编码的重点,我只提供两个思考方向:
- 一是在《编码》中看到的。视频适配板有一个字符生成器(character generator),在生产时就包含了所有的ASCII码字符的8x8像素图。每当需要展示字符(以"A"举例)时,先找到"A"的ASCII码
0x41
,再通过0x41
找到存储在字符生成器中的像素图,再依据像素图将对应位置的像素点进行点亮。 - 二是来自OpenGL学习文档中的FreeType:使用矢量图像构造字形。大家感兴趣可以查看后面的引用链接。(该文档中"位图字体"与《编码》中思想一致,都是直接展示图片。)
本篇文章的重点是如何拿到图片?
二、字符集
如何拿到图片呢?
为回答这个问题,我们首先得知道"图片"是什么?
如上所说,图片是"文字",是人类用来纪录特定事物、简化图像而成的书写符号(后文称"书写符号"为"字符")。
全球有近200个国家,约6000种语言,人类主要使用的文字种类有:语素文字、音节文字、拼音文字等。其中以英语、汉语、印地语、西班牙语、法语、阿拉伯语、孟加拉语......等使用人数最多。
按照我从网上搜到的统计数字:全世界所有的字符数量是超过10万个的。
如果计算机需要将这10万多个字符都展示出来,则相应的需要10万多张图片?
这些图片如何管理,字符"A"是排在字符"嘟"前面还是后面?中文、英文、法语、俄语甚至日语需要放在一起么?我是中国人,看不懂也用不上俄语,需要去设置去关心俄语图片么?
计算机起源于美国,最初的科学家确实是只关注英文的,于是设定了ASCII(American Standard Code for Information Interchange)标准------26个字母、阿拉伯数字,再加上些标点符号,够了!
随着计算机向全球普及,ASCII收录的字符显然是不够用的,于是各个国家出台自己的标准------比如Shift-JIS(日本工业标准)、GB2312(中国标准简体中文字符集)等------来表示本国的文字。
同一事物的标准越多,则代表没有标准;多标准一起存在,乱码处处可见。
为了统一标准,1988年,几大著名计算机公司合作研究出一种用来替代ASCII码的编码系统,取名为Unicode(统一化字符编码标准) ,它采用16位编码,每一个字符需要2个字节,可以表示65535个不同字符。全世界所有的人类语言,都使用同一个编码标准。(后来,Unicode再次扩容,分为17个面,即一共可以表示17 * 65535 = 1114095
个字符。一般情况下只需要0号平面就够了。)
到此处,则可以回答节首的问题:如何拿到图片呢?
从Unicode中拿!
Unicode是什么呢?
Unicode是字符集,是一种标准,是收录全世界字符图片并为之配上一个编号 的标准。比如字符"A"的编号是0x41
,"嘟"的编号为0x561F
(本文的编号,都是16进制)。
二、UTF-8是什么?
如上所说,现在我们知道了文字字符,是一个编号对应一张图片,这所有的对应关系合在一起成为了Unicode字符集。
在计算机在进行数据传输与存储时,为了节省空间,是只会传输和存储字符编号的。
如何存这个编号?是我们需要关注的重点。
对ASCII码来说,一个字符,一个字节就够了,挨着存就好:比如Hello
的存储形式为48 65 6C 6F 2C
。
当标准切换为Unicode时,一个字符的编号需要两个字节长度来存储,以Hello
加个 嘟嘟
举例,它的存储形式将变成00 48 00 65 00 6C 00 6F 00 2C 00 20 56 1F 56 1F
(空格字符的编号为0x20
)。
我们可以看到,中间是有很多空间被浪费的,很多的00
不需要被存下来。
为了去掉这些额外空间,大佬们又为Unicode编码发明了UTF-8(8-bit Unicode Transformation Format)
,一种针对Unicode的可变长度字符编码。
具体的UTF-8规则此文不再阐述,我只在Python3中向大家展示一下如何将UTF-8编码转回Unicode编号:
python
# 1. 原字符串
>>> s = 'Hello 嘟嘟'
# 2. 将其以UTF-8格式进行编码(encode函数,默认参数是utf-8)
>>> b = s.encode()
>>> b
b'Hello \xe5\x98\x9f\xe5\x98\x9f'
# 3. 将编码出来的bytes对象以16进制展示
>>> b.hex()
'48656c6c6f20e5989fe5989f'
# 4. 将"嘟"字的UTF-8码以二进制形式展示出来
>>> dudu = '嘟'
>>> [bin(byte) for byte in bytes(dudu, 'utf-8')]
['0b11100101', '0b10011000', '0b10011111']
可以看到,上面实例第3点,当将Hello 嘟嘟
的UTF-8展示形式以16进制展示出来时,是没有额外存储0x00
的。
末尾"嘟"的UTF-8存储形式为e5989f
,在我将其以二进制形式展示出来后,套入UTF-8的规则进行逆解。
Unicode与UTF-8之间的转换关系表
由上面的表格可以看到,只需要将第1个字节的后4位0101
、第2个字节的后6位011000
、第3个字节的后6位011111
进行拼接,即能得到Unicode的编码,见证奇迹的时刻到了......
拼接之后的二进制码为:
perl
# 1. 将拼接好的二进制码赋值给int
>>> b1 = int('01010110', 2)
>>> b2 = int('00011111', 2)
# 2. 将int转成16进制展示
>>> hex(b1)
'0x56'
>>> hex(b2)
'0x1f
可以看到,将"嘟"字转回为Unicode编号之后,与我们查表得来的0x561F
是一致的。
所以,UTF-8是什么呢?UTF-8只是一种存储Unicode编号的方式。
三、总结
综上所述,我算是对与文字相关联的那些关键字------字符集、ASCII、Unicode、UTF-8、str、bytes------有些了解了。
文字是什么? 文字是一张张图片,是人类用来纪录特定事物、简化图像而成的书写符号。
字符集是什么? 字符集是一种标准,一种规范,一种协议,一种整理统计文字图片及图片所对应编号的一种集合。ASCII、Unicode、GBK等都是字符集。
UTF-8是什么? 是存储Unicode字符集里面文字编号的一种字节编排方式。
str、bytes是什么? 是Python3中的两种类型,可以很笼统地将str与Unicode对应,将bytes与UTF-8对应。
四、额外说说GBK
GBK,国家标准扩展。在Python3里面可以直接以gbk
形式对str对象进行编码,这理解起来会简单些:
shell
# 将str以gbk编码转为bytes
>>> s = 'Hello 嘟嘟'
>>> s.encode('gbk')
b'Hello \xe0\xbd\xe0\xbd'
可以看到"嘟"字的存储形式为0xE0BD
,占用2个字节,看下方截图,直接就找到映射关系了。
"嘟"在哪里?
五、引用链接
下面链接中"%"符号后面跟着的两个字符,也是Unicode编号以UTF-8形式进行编码的。可以按照上面的逆解方式验证。
1、Unicode码表:zh.wikipedia.org/wiki/Unicod...
2、GBK码表:toolhelper.cn/Encoding/GB...
3、OpenGL的文字渲染:learnopengl-cn.readthedocs.io/zh/latest/0...
4、文字分类:zh.wikipedia.org/wiki/%E6%96...
5、UTF-8的编码方式:zh.wikipedia.org/wiki/UTF-8#...