字符和编码
在这个世界,我们依靠语言文字进行交流。英文是字符,中文也是字符,连表情符号🐶都是字符。
英文一共有26个字母,10个数字;中文常见的就有几千个汉字。而且还有其他的很多语言。
但是计算机的语言是二进制,也就是0和1,那么怎么用0 1 表示这些自然语言呢?
ASCII编码和扩展
在计算机被发明并使用之后,急切的需要识别我们现实世界的文字,比如怎么存储字符a呢?
所以最开始计算机科学家发明了一种编码,来存储这些字母、数字和常见的符号。
- 因为这些常见的符号不多,可以使用一个字节来表示一个字符
- ASCII编码使用了128个编码。 但是ASCII编码只能识别英文,汉子和其他国家语言都没法表达。
后来,为了表达一些欧洲的语言文字,扩展了 ASCII编码。
Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。 ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。ISO-8859-1收录的字符除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。 另外, 其他一些国家也使用256位的编码,并向下兼容ASCII编码。
中文编码GB系列
ASCII编码的扩展到了中国就不是那么一回事了,因为汉字实在太多。常见的汉子就有3000多个。 所以为了中文,又发展了一些新的编码。 用两个字节表示中文:
- GB2312一共收录了7000多个字符,包括6000多个汉字和几百个符号,兼容ascii
- Big5 用于繁体
- GBK: 收录了20000多个符号,兼容GB2312
- 但是又因为兼容了ASCII,所以他们也有单字节,那怎么知道哪个字符是单字节
- 中文的最高位不为0
- ASCII编码只有7位,最高位是0
Unicode
随着更多的人和国家用上计算机,编码也越来越多。这样就存在了不少问题。 如下图:
- 各个编码的自然语言不互通
- Latin-1编码不识别中文,必须安装GB相关的编码才能展现中文
- GBK不识别其他国家的语言,也必须安装对应的编码集才行 但是,有没有一种编码可以识别世界上所有的字符,所有的自然语言呢? 对于个人来说这个很难实现,但是对于计算机来说,这个不是难事。
于是制定规范,要统一各种符号和语言,这样的编码字符集称为Unicode。
字符集
字符和二进制的对应关系就是字符集。 就像上面描述的,有
- ASCII字符集
- 扩展ASCII编码
- GB字符集 Unicode也规定了各种语言字符、特殊字符和二进制的关系。Unicode有两种字符集:
- UCS2(universal character set2) 占用2个字节,一共能表示65536个字符,一般来说够用了。
- UCS4(universal character set4) 占用4个字节,兼容UCS2,可以表示更多的符号,比如表情符号。
UTF-16编码
UTF-16编码是UCS2字符集的一种编码实现。
UTF-16使用两个字节来表示一个字符,跟其他超过一个字节的数据类型一样,都要考虑大端存储还是小端存储的问题。
下图是数字1537的两种存储方式: 1537在编码的时候假设需要两个字节,高字节是00000011,低字节是00000001.
我们按照地址增长的方向去读取一个内容,先读低的地址,再去读高的地址。
- utf-16编码的大端标志是在字符前面加上FEFF,这是符合人类使用方式的
- 我们念一个数字的时候,比如说1537,总是习惯先说高位。
- utf-16编码的小端标志是在字符前面加上FFFE
同理,UTF-32编码是UCS4字符集的一种编码实现,一个字符需要4个字节存储,也要设置标记来区分是大端还是小端。
UTF-8编码
ASCII 码只占用了一个字节,使用UTF-16编码需要占用两个字节,这非常浪费存储、网络空间。
于是设计了一种变长的编码来表示Unicode,这就是UTF-8编码。可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8 的编码规则很简单,为:
1)对于单字节的符号,字节的第一位设为0
,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于大于1字节的符号(一共占n个字节),第一个字节的前n
位都设为1
,第n + 1
位设为0
,后面字节的前两位一律设为10
。剩下的没有提及的bit,为这个符号的 Unicode编码。
比如知这个Unicode字符的UTF-8编码,
-
Unicode编号:
U+77E5
-
对应的二进制:01110111 11100101
-
但是发现实际存储的却是
# echo 知 |hexdump -C
00000000 e7 9f a5 0a |....|
00000004
0a是换行符,实际的二进制存储e7 9f a5 ,不是77 E5了,这是为什么呢?
这个是因为77 E5被UTF-8编码就是e7 9f a5。
而且因为这样的编码,UTF-8不需要大端小端的标记了。
UTF-8是目前计算机和网络上使用的最广泛的字符编码了。
使用字符的环境
我们会在计算机的各个环境使用字符,而且在各个环境都有不少细节需要注意。
操作系统和shell
现在一般来说,Linux和mac的默认编码是utf-8,中文windows系统的默认编码是gbk。
- 我们通过键盘输入法输入的内容被就可以被转化为一个个的编码了,这个时候用的就是操作系统的编码。
- 一般来说,终端也就是shell的默认编码和操作系统是一致的。
- 除了编码,操作系统还有locale属性,和编码相关
做个小实验
在两台系统分别输入date命令:
LANG=zh_CN.UTF-8
$ date
2021年12月18日 星期六 07时00分40秒 CST
LC_ALL=en_US.UTF-8
LANG=zh_CN.UTF-8
# date
Sat Dec 18 07:00:56 CST 2021
- 前面的LANG=zh_CN.UTF-8
- 后面的LC_ALL=en_US.UTF-8 LANG=zh_CN.UTF-8
- 都有UTF-8,为什么还是要分zh_CN和en_US,这个zh_CN、en_US就是locale,表示语言和国家的标志。
- locale就是某一个地域内的人们的语言习惯和文化传统和生活习惯,时间 货币单位、符号、文字等
- 对使用简体中文的中国人来说,date展示2021年12月18日 星期六 07时00分40秒 CST比较符合我们的习惯
- 对使用英文的美国人来说,date展示Sat Dec 18 07:00:56 CST 2021比较符合他们的习惯
设定locale就是设定12大类的locale分类属性,即12个LC_。除了这12个变量可以设定以外,为了简便起见,还有两个变量:LC_ALL和LANG。它们之间有一个优先级的关系:LC_ALL > LC_ >LANG 。 可以这么说,LC_ALL是最上级设定或者强制设定,而LANG是默认设定值,也就是LC_没设置的就是还有LANG。
文件和程序代码
查看
file 命令
file命令查看文本文件的时候也可以显示
# file test.js
b.js: ASCII text
# file IAOYUANGUI/*
IAOYUANGUI/IP.png: PNG image data, 287 x 68, 8-bit/color RGBA, non-interlaced
XIAOYUANGUI/fs.excalidraw: JSON data
- 可以发现,file不一定能看到字符编码,即使有也不一定准。
- 显示b.js为 ASCII text
- 这个test.js实际是utf-8编码的
- 虽然文件名通常包括主文件名和后缀名两部分,它是为了方便让人类理解文件的内容和类型而存在的,但是很多Linux不要求文件名有后缀
- 事实上,有一个东西叫做魔法数字Magic Number。它是文件的头一个或几个字节的内容。计算机可以根据它的值判断文件的类型
你可以用十六进制编辑器打开图片文件,然后对照魔法数字表来确定文件类型。
# hexdump -C Documents/XIAOYUANGUI/fs6.png |head
00000000 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 |.PNG........IHDR|
png文件的Magic Word 就是89 50 4e 47
在Vim中输入命令:set fileencoding
即可显示文件编码格式,比如 fileencoding=utf-8
。
编码转换
因为不同的系统使用的编码不一样,有时候为了防止乱码,需要做一些转换。
有些命令可以帮助文件编码转换:
- iconv命令
iconv -f UTF-8 -t GBK a.txt > test2.txt
- -f 源编码 -t 目标的编码
- 通过vim test2.txt可以看到fileencoding变为了gb18030。
vim
在Vim中输入命令:set fileencoding
即可显示文件编码格式,比如 fileencoding=utf-8
。
- 除了文件编码fileencoding ,vim还有encoding和termencoding等参数
- encoding是vim程序本身的字符编码
- termencoding是展示文件的字符编码,一般和encoding一致
- fileencodings,这是vim的解码列表
当我们用vim打开一个文件的时候,文件编码的自动识别,是通过设置fileencodings实现的。 fileencodings是一个用逗号分隔的列表,列表中的每一项是一种编码的名称。 当我们打开文件时,vim按顺序使用fileencodings中的编码进行尝试解码,
- 如果成功的话,就使用该编码方式进行解码,并将fileencoding设置为这个值;
- 如果失败的话,就继续检验下一个编码。
- 图上表示gb18030编码的文件可以解码,不在这个列表当中的编码格式展示的文件是无法用vim正常展示的。
vim编码相关的处理如下图所示:
- 如果实际的文件编码为utf8,第一次探测就成功了,fileencoding设置为utf-8,文件内容再通过encoding编码到vim内部
- 如果如果实际的文件编码为gb18030,第3次探测才成功,fileencoding设置为gb18030,文件内容再通过encoding也就是utf-8编码到vim内部
- ucs-bom是有bom的Unicode编码的文件,比如utf-16 utf-32或者utf-8 bom,他们都有自己的不同的标记。
- BOM(Byte Order Mark),字节顺序标记,出现在文本文件头部,Unicode编码标准中用于标识文件是采用哪种格式的编码
- 一定要把严格的编码方式比如UTF-8(有严格的格式要求)放在前面,
- 把宽松的编码方式放在后面。例如,latin1是一种非常宽松的编码方式,任何一种编码方式得到的文本,用latin1进行解码,都不会发生解码失败。当然,解码得到的结果也很可能会是乱码。
- 因此,如果你把latin1放到fileencodings的第一位,那么打开任何中文文件都会显示乱码了。
代码的编码
代码都是文本,程序解析变量的方式,一般都是用UTF-8。
- 一般像xml或Python也都字符编码的标记。
- 但是注意windows系统开发的时候,utf-8文件都是带有bom标记的,这个和Linux mac系统不同。
下面是mac下intellj idea开发Java程序的file encoding一个截图:
- 可以看出,源码都是使用的UTF-8编码
- 默认设置UTF-8文件都不带BOM
网络协议的编码
http协议
http协议是一个文本协议,使用UTF-8编码。
url编码
RFC 1738做了规定:只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-_.+!*'(),"[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。所以当其他文字字符想要用于URL编码的时候需要经过编码,比如使用UTF-8编码,在编码字符前面加上%来展示。
mysql的编码
mysql支持utf8编码来存储字符,但是这里有些容易犯错的地方。MySQL的utf8编码并不是我们说的UTF-8编码实现
- 因为MySQL的utf8编码只有三个字节,
- 而真正的UTF-8 是每个字符最多四个字节。emoji符号占4个字节,一些较复杂的文字、繁体字也是4个字节,所以MySQL支持不了像emoji这样的符号。
- 如果写入这样的emoji符号,解码会失败,所以会写入失败。
为了支持这些额外的符号,需要使用MySQL的utf8mb4编码。
emoji
😅(统一码:U+1F605)是一个Emoji表情,🐶的Unicode编码是1F436。
比如下面就是iPhone支持的表情符号输入:
总结
本文详解了字符编码的发展、统一历史,也详解了一些应用会遇到的编码问题,尤其当我们用到一些软件或工具时也要注意编码相关的,比如MySQL和Vim。 Unicode支持的字符在变多,甚至emoji和火星文都能够支持。我们在使用的时候尽量使用UTF-8编码,既能够支持各种字符,又能够节省存储空间。