简单聊聊字符编码
01.简单聊聊字符编码
几乎人人都遇过乱码问题,程序们更是经常被乱码弄得抓狂,那么乱码产生的原因是什么呢?字符是如何在计算机里存储的呢?本系列文章我们就来聊聊这个主题。
字符编码是计算机世界里最基础、最重要的一个主题之一,如果你是计算机行业工作者,很有必要掌握。如果不彻底搞懂字符编码,在工作中就容易时不时被其困扰,本人花费了一两周时间才稍稍入门,在工作中算是够用了。
字符编码的基础性、重要性,主要体现在它涉及面广。向下涉及到计算机的底层技术,甚至是硬件实现;向上几乎跟所有的操作系统、编程语言、应用程序都密切相关。
前言
本文首先介绍什么是字符编码,以及常见的字符编码,意在使读者对字符编码有基本的认知。
学习之前,读者应具有基本的数字电路或计算机组成原理的知识,知道什么是二进制、位、字符和内存等,如果有相关的编程基础就更好了。
为了照顾部分读者,我们还是简单复习下吧:
-
什么是位:位是数据存储的最小单位,一个位存储一个二进制数,要么是 0,要么是 1
-
什么是字节:8 个位构成一个字节
-
什么是字符:简单来说,字符就是各国所使用的文字。例如 26 个英文字母是字符,中国的几万个汉字也是字符,同理还有日文、法文等。
-
特别注意:"字符"和"字节" 是两个不同的概念,“字节”是一个 8 位的物理存储单元,而“字符”则是一个文化相关的符号。
-
什么是编码:编码就是将信息转换为另一种格式或形式,例如将语音信息转为电信号(我们平时打电话就是这样传输信息的),又例如将文字信息用手语表达出来(这样聋哑人就可以看懂了),都是将一种信息转为另一种信息。这就是编码。解码就是编码的逆过程。更多请参考我的另一篇博客:编码与原码、补码、反码
-
字符集:顾名思义,就是字符的集合,是一个自然语言系统中各种文字和符号的总称,含文字,数字,字母,标点符号,音节,图形符号等等。例如中文就是一个字符集,汉字有 10w 个左右,除此之外还有拼音符号等;有的国家的语言还有音节,日文中有片假字等,这里不一一展开。
-
什么是字符编码:就是根据一定的编码规则,将字符集中的某个字符,编码为指定的格式。例如将字符‘A’ 转为 二进制中的 01000001 。可以理解为在字符集和指定集合(在二进制中,就是 0 和 1 组成的位串)之间建立一个对应关系(也叫映射关系)的过程。
摩斯电码,也是一种字符编码,SOS 这三个字母可以编码为三长三短的信号。
在计算机中,字符编码的通常是将字符根据某种规则,映射为计算机可以接受的二进制数字,其目的是为了便于字符在计算机中表示、存储、处理和传输(例如在网络中传输)。
为什么我们需要字符编码?专业一点的说法如下:
现代计算机不仅处理数值领域的事情,而且处理大量非数值领域的问题,这样一来,必然要引用文字、字母以及某些专用符号,以便表示文字语言、逻辑语言等信息,例如,人机交换信息时用英文字母、标点符号、十进制数以及诸如 $, % + 等符号。然而数字计算机只能处理二进制数据,因此,上述信息应用到计算机中时,都必须编写出二进制格式的代码,也就是字符信息用数据表示,称为符号数据
-摘自《计算机组成原理》白中英,戴志涛主编 第 2.1.3 节 ,23 页
大白话:人们发现计算机可以做很多很多事情,不仅仅可以用来计算,还可以用来显示字符,打印字符(例如电子邮件,打印机,网站等,都需要显示和处理字符)。但计算机只能保存 0 和 1,因此需要将字符转为二进制,才能被计算机处理。
既然要将字符编码为二进制,那么有没什么统一的标准呢?例如字符‘A’的编码到底是多少?如果没有统一的标准,有的人认为‘A’的编码是 1,有的人认为‘A’的编码是 2,就会造成同一段二进制数字在不同计算机上显示出来的字符不一样的情况,因此必须得定一个统一的、标准的转换规则。
接下来我们就来介绍一下一些常见到标准转换规则。
ASCII 编码
1968 年,ASCII 编码出现了(全称 American Standard Code for Information Interchange,美国信息互换标准代码)。特点如下:
-
用一个字节来存储一个字符(最高位统一是 0,只用了剩下的 7 位来表示字符。27=128)
-
共收录了 128 个字符(也可以理解这 128 个字符组成了 ASCII 字符集),分别如下:
- 基本的英文字母(从 abcd……xyz)
- 阿拉伯数字(0123456789)
- 标点符号(逗号句号等)、特殊符号(感叹号、@,井号等)
- 一些带有控制功能的字符(例如换行符,回车符)
前三类,我们可以称为可见字符,就是可以用肉眼看到的;那为什么有不可见字符(例如控制字符)呢?举个例子,计算机中处理字符,经常需要打印(在早期计算机就是通过纸带来输出的),我们就用控制字符来告诉计算机如何换行,不然全都打印到一行上,就会相互覆盖。
在 ASCII 编码方案中,所有能表示的字符称为 ASCII 字符集,其二进制编码称为 ASCII 码。
如下图就是 ASCII 码表,每个字符前面的数字就是其编号(也叫码点);然后是对应的二进制(橙色方框内):
举个例子,空格“space”的编码是 32(也叫码点,其二进制是 0B00100000),字母 A 的编码是 65(0B01000001)。当我们告诉计算机某个内存单元是字符的时候,计算机会根据 ASCII 码查表,然后知道是这是什么字符,并处理该字符(例如显示在显示器上)。
咱们不一一说明所有 ASCII 码,那不是本章的主题;完整的 ASCII 请参考下一篇文章。
小知识:
- ASCII 由美国国家标准学会 ANSI(American National Standard Institute)制定
- 为什么 ASCII 最高位是 0? 因为 128 个基本上就够当时的计算机使用了,最高位填充 0 便于计算机系统处理,存储和运输(当时计算机通常以字节为单位处理数据,一个字节占 8 位)
- 实际上,最早出现的字符编码标准是 EBCDIC(Extended Binary Coded Decimal Interchange Code,扩展二进制编码的十进制交换码)。EBCDIC 码是由国际商用机器公司(IBM)为大型机操作系统而开发设计的,于 1964 年推出。在 EBCDIC 码中,英文字母不是连续排列的,中间出现多次断续,这带来了一些困扰和麻烦。
- ASCII 码的编码方式参照了 EBCDIC 码,并吸取了其经验教训,将英文字母进行了连续排列,这方便了程序处理。因此,在后来 IBM 的个人计算机和工作站操作系统中并没有采用 EBCDIC 码,而是 ASCII
- ASCII 编码方案虽然不是最早出现的字符编码方案,但却是最基础、最重要、应用最广泛的字符编码方案。后续出现的字符编码方案,基本都要兼容它。就好比目前国内的插座电压都是 220v,如果有人搞个不支持 220v 的插座,想必是没什么人愿意用的,因为不兼容。
- 像 EBCDIC 这样与 ASCII 完全不兼容的编码方案,基本上处于已淘汰或将要淘汰的境地。
只有 ASCII 编码足够吗?
由于计算机最初是西方发明的,因此只考虑了英文字符的情况。
随着计算机的普及,全球各地都开始用计算机了,并且各国也想在计算机上展示、处理本国的字符。
例如中国,就有成千上万个汉字,这些字符在 ASCII 码里都么有,怎么办呢?
很简单,各国再搞一个编码...........
ISO/IEC 8859:一组字符集
计算机出现之后,首先逐渐从美国发展到了欧洲。由于欧洲很多国家所用到的字符中,除了基本的、美国也用的那 128 个 ASCII 字符之外,还有很多衍生的拉丁字母等字符。比如,在法语中,字母上方有注音符号;而欧洲其他国家也有各自特有的字符。
考虑到一个字节能够表示的编码实际上有 256 个,而 ASCII 字符却只用到了一个字节中的低 7 位(在 ASCII 码中最高位总是为 0),编号为 0x00~ 0x7F(这是十六进制表示法,换成十进制就是 0~127)。
也就是说,ASCII 只使用了一个字节所能表示的 256 个编码中的前 128 个编码,而后 128 个编码相当于被闲置了。因此,欧洲各国纷纷打起了后面这 128 个编码的主意,也就是最高位为 1 的话,就是一个新的字符。
这样就在 ASCII 码的基础上,既保证了对 ASCII 码的兼容性,又补充扩展了新的字符,由此得到了很多个字符集。
ISO/IEC 8859 是一组字符集的总称,其下共包含了 15 个字符集,即 ISO/IEC 8859-n,其中 n=1,2,3,...,15,16(其中 12 未定义,所以共 15 个)。
- ISO8859-1 字符集,也就是 Latin-1,是西欧常用字符,包括德法两国的字母
- ISO8859-2 字符集,也称为 Latin-2,收集了东欧字符
- ISO8859-3 字符集,也称为 Latin-3,收集了南欧字符
- ISO8859-4 字符集,也称为 Latin-4,收集了北欧字符
- ISO8859-5 字符集,也称为 Cyrillic,收集了斯拉夫语系字符
- ISO8859-6 字符集,也称为 Arabic,收集了阿拉伯语系字符
- ISO8859-7 字符集,也称为 Greek,收集了希腊字符
- ISO8859-8 字符集,也称为 Hebrew,收集了西伯莱 (犹太人) 字符
- ISO8859-9 字符集,也称为 Latin-5 或 Turkish,收集了土耳其字符
- ISO8859-10 字符集,也称为 Latin-6 或 Nordic,收集了北欧 (主要指斯堪地那维亚半岛) 的字符
- ISO8859-11 字符集,也称为 Thai,它是从泰国的 TIS620 标准字符集演化而来
- ISO8859-12 字符集,目前尚未定义(未定义的原因目前有两种说法:一是原本要设计成一个包含塞尔特语族字符集的“Latin-7”,但后来塞尔特语族变成了 ISO 8859-14 / Latin-8;二是原本预留给印度天城体梵文的,但后来却搁置了)
- ISO8859-13 字符集,也称为 Latin-7,主要函盖波罗的海(Baltic) 诸国的文字符号,也补充一些在 Latin-6 中遗漏的拉脱维亚 (Latvian) 字符
- ISO8859-14 字符集,也称为 Latin-8,它将 Latin-1 中的某些符号换成塞尔特语 (Celtic) 的字符。塞尔特族是指英伦外围的威尔斯人 (Welsh) 和盖尔人 (Gaelic)
- ISO8859-15 字符集,也称为 Latin-9,或者被匿称为 Latin-0,它将 Latin-1 中较少用到的符号删除,换成当初遗漏的法文和芬兰字母;还有,把英镑和日元之间的金钱符号,换成了欧盟货币符号
- ISO 8859-16,正式编号为 ISO/IEC 8859-16:2001,又称 Latin-10,这个字符集设计来涵盖阿尔巴尼亚语、克罗地亚语、匈牙利语、意大利语、波兰语、罗马尼亚语及斯洛文尼亚语等东南欧国家语言
这 15 个字符集大致上包括了欧洲各国所使用到的字符,而且每一个字符集的补充扩展部分(即除了兼容 ASCII 字符之外的部分)都只实际使用了 0xA0 ~ 0xFF (十进制为 160 ~ 255) 这 96 个编码。
GB2312 编码
即使用上 ASCII 的最高位,也才 256 个编码;而常用的汉字就有几千个了,一个字节肯定是不够用的,因此得用多个字节。在 1980 年,中国发布了 GB2312 编码,特点如下:
-
GB2312 使用 2 个字节来存储一个字符
-
GB2312 是对 ASCII 的中文扩展。小于 127 的字符意义与原来相同,这样 ASCII 码原来的字母和标点符号都还保留了;
在一段文本中,如果一个字节是 0~127,那么这个字节的含义同 ASCII 编码,否则,这个字节和下一个字节共同组成汉字(或是 GB 编码定义的其他字符)。
-
两个大于 127 的字符连在一起的时候,就表示一个汉字。
-
ASCII 里原有的数字、标点符号和字母也重新编码(用两个字节表示) ,这样得到的符号是全角符号。而 127 以下的的就是半角符号。
-
为什么会有全角符号?比如中国的逗号和西方的一些标点符号是不同的,例如逗号和感叹号。你可以分别在全角和半角模式下输入逗号和感叹号,观察他们的不同:中文情况下:
,!
英文:,!
,这里不展开
GB2312 一共收录了 7445 个常用汉字,包括 6763 个汉字和 682 个其它符号,足够人们日常使用。
为什么说是对 ASCII 的中文扩展?因为这种编码格式是兼容 ASCII 的。即使用 GB2312 的方式打开 ASCII 编码的文档,也不会乱码。
小知识:
- “GB”为“国标”的汉语拼音首字母缩写,即“国家标准”之意
- 注意,此时对于编程而言,一个汉字算两个英文字符 (一个汉字两个字节,能存储 2 个英文字符)
GBK 编码
中国的汉字实在是太多了,一些人名和生僻字,都无法用 GB2312 编码打出来。因此,在 1995 年,中国发布了 GBK 编码。特点如下:
- GBK 编码不再要求低字节一定是大于 127,只要第一个字节大于 127 就表示是一个汉字的开始
- GBK 包含了 GB2312 的所有内容,同时新增了近 20000 个新的汉字(繁体字)和符号
GB18030 编码
后来,少数民族也要用电脑了,怎么办呢?再次扩展字符编码……
在 2000 年,GB18030 编码发布了,取代了 GBK 编码成为正式的国家标准。
该标准收录了 27484 个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。现在的 PC 平台必须支持 GB18030,对嵌入式产品暂不作要求。所以手机、MP3 一般只支持 GB2312。
感兴趣的同学可以去国家标准全文公开系统去查看更多信息。
中文编码小结
从 ASCII、GB2312、GBK 到 GB18030,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。
区分中文编码的方法是高字节的最高位不为 0。
按照程序员的称呼,GB2312、GBK 到 GB18030 都属于双字节字符集(Double Byte Charecter Set,简称 DBCS)。
这里附上一个示意图:
ANSI 编码
ANSI 全称 American National Standard Institite,是美国国家标准学会(美国的一个非营利组织)指出的概念。
ANSI 不是指某一种特定的编码,而是不同地区扩展编码方式的统称,各个国家和地区所独立制定的兼容 ASCII 编码,但互相不兼容的字符编码,微软统称为 ANSI 编码。 例如,
- GB2312,GBK 就是兼容 ASCII 编码的,是对 ASCII 的扩展
- BIG5 码,是通行于台湾、香港地区的一个繁体字编码方案,俗称“大五码”,也兼容 ASCII 的编码
- 日本则提出了 SHIFT_JIS 编码,也是对 ASCII 的扩展
- 韩国把韩文编到 Euc-kr 里
以上这些各个国家为了兼容 ASCII 而提出的编码,统称 ANSI 编码。
我们可以这样总结:ANSI 编码用 0x00–0x7f (即十进制下的 0 到 127)范围的 1 个字节来表示 1 个英文字符,超出一个字节的 0x80~0xFFFF 范围来表示其他语言的其他字符。
换句话说:ANSI 的前 128 个字符与 ASCII 码相同,之后的字符全是某个国家语言的所有字符,但各国有各国的标准,这些编码之间不能相互转换。
当编码方案多了起来……
目前,我们介绍了常见字符集,有 ASCII 字符集(并介绍了其编码方式)、ISO 8859 系列字符集、GB 系列字符集(GB2312、GBK、GB18030)、BIG5 字符集等。
因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码。这有什么缺点呢?
- 比如大陆用的是 GBK 编码,保存了一个文本文件;而台湾用的是 BIG5 编码,打开 GBK 编码的文件的时候,就会乱码。
- 在电子邮件中,也常常出现乱码,就是因为发信人和收信人的编码不一样。
相信大家平时多多少少也遇到过乱码的问题吧!这就是其原理。这就是标准不统一带来的问题。堪称计算机的巴比伦塔命题。
巴别塔是《圣经·旧约·创世记》第 11 章故事中人们建造的塔。根据篇章记载,当时人类联合起来兴建希望能通往天堂的高塔;为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。此事件,为世上出现不同语言和种族提供解释。----来自百度百科
Unicode
为了统一标准,ISO (国际标谁化组织)开始制定一套新的规范,其目的是要在一套字符集内,囊括地球上所有文化、所有字母和符号!并且不兼容所有的地区性编码(例如 GBK 和 BIG5,但兼容 ASCII,因为 ASCII 出现的太早了,很多地方都在用,完全舍弃的话要改造的工作量很大)。
因此,Unicode 标准出现了,其包含了字符集和编码规则。
Unicode 使用的是字符集称为 Universal Multiple-Octet Coded Character Set,简称 UCS。
最早使用的是 UCS-2 字符集,其编码规则比较简单,跟 ASCII 一样,将用到的字符列出来,然后其二进制就是其编码,使用了 2 个字节存储一个字符,能存储 216 = 65536 个字符
但 6w 多个字符还是太少了,光汉字就有近 10w 个了,UCS-2 无法表示地球上的所有字符;因此后面出现了 UCS-4 字符集,其使用 4 个字节表示一个字符,我们就可以组合出 21 亿个不同的字符出来(最高位有其他用途),这应该够用到银河联邦成立吧……
其实从 UCS-2 过渡到 UCS-4 是很曲折的,这里不展开
但是,这样一来,新的问题出现了;本来 ASCII 只用一个字节就可以存储一个字符,汉字一般只需 2 个字节就可以存储了,UCS-4 使用 4 个字节,对于 ASCII 字符的话,其编码规则是前 3 个字节补 0,可以说非常浪费存储空间了。所以,Unicode 标准出来后,并没有被各国接受。
UTF 编码
随着计算机网络的发展,各国之间信息交流更频繁了,不得不重新考虑编码的问题。因此,出现了 UTF 的编码方式。
UTF,全称 Unicode Transformation Format,意为把 Unicode 字符转换为某种格式。UTF 系列编码方案有 UTF-8、UTF-16、UTF-32,均是由 Unicode 编码方案衍变而来,以适应不同的数据存储或传递。
换句话说,UTF8,UTF16 和 UTF32 都是 Unicode 的实现,可以理解为将某个具体的 Unicode 字符,转为具体的某种格式(例如 3 个字节存储的格式,4 个字节存储的格式)。
一般来说,Unicode 编码之间可以相互转换,例如 UTF8 可以转换为 UTF16。
在计算机内存中,统一使用 Unicode 编码,当需要保存到硬盘或者需要传输的时候,就转换为 UTF-8 编码。用记事本编辑的时候,从文件读取的 UTF-8 字符被转换为 Unicode 字符到内存里,编辑完成后,保存的时候再把 Unicode 转换为 UTF-8 保存到文件。
看不太懂?没关系,现在只是简单介绍而已,后续几篇文章还会展开来讲。
UTF8
UTF-8 的编码规则很简单,只有 2 个:
- 对于一个字节的符号,和 ASCII 的符号表一样(字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码)
- 对于多字节的符号,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头
举个莉子:
- 0xxxxxxx:单字节编码形式,这和 ASCII 编码完全一样,因此 UTF-8 是兼容 ASCII 的
- 110xxxxx 10xxxxxx:双字节编码形式(第一个字节有两个连续的 1)
- 1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(第一个字节有三个连续的 1)
- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(第一个字节有四个连续的 1
如果一个字符的 Unicode 码点可以用 2 个字节存储,那么就会用第 2 种方式编码;如果要用 3 个字节,那么就用第 3 种方式编码
使用 Unicode 有什么好处呢?
- 乱码问题得以解决,目前最常用的编码是 UTF8,UTF16 也比较常用,基本上所有系统都支持 Unicode。
- 对于一些多语言的软件,例如操作系统,邮件系统,都不用为字符集头疼了。从 Windows NT 开始,MS 趁机把它们的操作系统改了一遍,把所有的核心代码都改成了用 Unicode 方式工作的版本,从这时开始,WINDOWS 系统终于无需要加装各种本土语言系统,就可以显示全世界上所有文化的字符了。
- 对于网络上传输字符也很方便,只需用 UTF 的标准来传输就完事。
- 此时不管是英文字母还是中文字母,都只算做一个符号。再次提醒:"字符"和"字节"两个术语的不同,“字节”是一个 8 位的物理存储单元,而“字符”则是一个文化相关的符号。
- 目前,百分之 90 的网站都使用了 UTF8。
后面我们再具体来深入 Unicode,本篇我们只是简单的介绍字符编码的概念,意在使读者对字符编码有基本的认知。
实践
我们来简单试试字符编码吧!以加深读者们对编码的认识,也请读者跟着笔者一起动手实践。
我们以 Windows 为例,Mac 和 Linux 操作也是类似的。
我们打开记事本,里面输入 HelloWorld,然后保存
可以看到,默认是 UTF8 编码,我们修改为 ASCII 编码(ANSI)。
然后,我们打开文件属性,可以看到是 10 个字节,因为我们存储的是 10 个字符:
打开文件,也能看到是 ANSI 格式:
转换格式
由于 Windows 自带的记事本,暂不支持文件格式的转换,因此,我们得用第三方的文本编辑器(可以理解为高级一点、功能更多的记事本),这里以 VSCode 为例,我们用 VSCode 打开该文件,然后删除全部内容。(其他小伙伴也可以用 notepad++,Sublime,notepad3 等其他编辑器,甚至 IDE 都可以完成 一样的操作)
然后点击编码,选择通过 GB2312 保存,结果如下:
然后我们输入你好,世界 可以看到占用了 10 个字节(一个汉字两个字节,包括中文逗号):
同理,我们试着用 UTF8 保存,可以看到占用 15 个字节了:
Linux 下关于编码的一些操作
在 Linux 下如何查看一个文件的编码?可以用 file 命令。比如我们上传刚刚的 HelloWorld.txt 到服务器上:
$ file HelloWorld.txt
HelloWorld.txt: UTF-8 Unicode text, with no line terminators
用 vim 查看编码:在命令模式下输入 一下命令
:set fileencoding
用 vim 转换编码:在命令行里输入
:set fileencoding=utf-8
在 Linux 下,还有一个专门用来处理编码的工具:enca,感兴趣的读者可以去搜索下相关介绍。
小结
什么是字符:字符就是一个文字的符号,例如汉字,例如英文字母。
什么是字符编码:告诉计算机如何保存与处理字符的机制。
常见的字符编码:
- ASCII 码
- ANSI 编码
- UTF 编码
这里附上一个示意图
接下来几篇博客,再来详细说说各种编码的原理,乱码产生的原因以及解决方法,数据库编码等、URL 编码、Base64编码等。