简单聊聊字符编码
# 01.简单聊聊字符编码
几乎人人都遇过乱码问题, 程序🐵们更是经常被乱码弄得抓狂,那么乱码产生的原因是什么呢?字符是如何在计算机里存储的呢?本系列文章我们就来聊聊这个主题。
字符编码是计算机世界里最基础、最重要的一个主题之一,请读者们务必掌握。如果不彻底搞懂字符编码,在工作中就容易是不是被其困扰,本人花费了一两周时间才稍稍入门,在工作中算是够用了。
字符编码的基础性、重要性,主要体现在它涉及面广。向下涉及到计算机的底层技术,甚至是硬件实现;向上几乎跟所有的操作系统、编程语言、应用程序都密切相关。
# 前言
本文首先介绍什么是字符编码,以及常见的字符编码,意在使读者对字符编码有基本的认知
学习之前,读者应具有基本的数字电路或计算机组成原理的知识,知道什么是二进制、位、字符和内存等,如果有相关的编程基础就更好了。
为了照顾部分读者,我们还是简单复习下吧:
- 什么是位:位是数据存储的最小单位,一个位存储一个二进制数,要么是0,要么是1
- 什么是字节:8个位构成一个字节
- 什么是字符:简单来说,字符就是各国所使用的文字。例如26个英文字母是字符,中国的几万个汉字也是字符,同理还有日文、法文等。
- 特别注意:"字符"和"字节" 是两个不同的概念,“字节”是一个8位的物理存储单元,而“字符”则是一个文化相关的符号。
- 什么是编码:编码就是将信息转换为另一种格式或形式,例如将语音信息转为电信号(我们平时打电话就是这样传输信息的),比如将文字信息用手语表达出来,都是将一种信息转为另一种信息。这就是编码。解码就是编码的逆过程。更多请参考:编码与原码、补码、反码 (opens new window)
- 字符集:顾名思义,就是字符的集合,是一个自然语言系统中各种文字和符号的总称,含文字,数字,字母,标点符号,音节,图形符号等。例如中文就是一个字符集,汉字有10w个左右,除此之外还有拼音符号等;有的国家的语言还有音节,日文中有片假字等,这里不一一展开。
- 什么是字符编码:就是根据一定的编码规则,将字符集中的某个字符,编码为指定的格式。例如将字符‘A’ 转为 二进制中的01000001 。可以理解为在字符集 和 指定集合(在二进制中,就是0和1组成的位串)之间建立一个对应关系(也叫映射关系)的过程。 摩斯电码,也是一种字符编码,SOS这3个字母 可以 编码为 三长三短的信号。 在计算机中,字符编码的通常是将字符根据某种规则,映射为计算机可以接受的二进制数字,其目的是为了便于字符在计算机中表示、存储、处理和传输(包括在网络中传输)。。
为什么我们需要字符编码?专业一点的说法如下:
现代计算机不仅处理数值领域的事情,而且处理大量非数值领域的问题,这样一来,必然要引用文字、字母以及某些专用符号,以便表示文字语言、逻辑语言等信息,例如,人机交换信息时用英文字母、标点符号、十进制数以及诸如 $, % + 等符号。然而数字计算机只能处理二进制数据,因此,上述信息应用到计算机中时,都必须编写出二进制格式的代码,也就是字符信息用数据表示,称为符号数据
-摘自《计算机组成原理》白中英,戴志涛主编 第2.1.3节 ,23页
大白话:人们发现计算机可以做很多很多事情,不仅仅可以用来计算,还可以用来显示字符,打印字符(例如电子邮件,打印机,网站等,都需要显示和处理字符)。但计算机只能保存0和1,因此需要将字符转为二进制,才能被计算机处理
既然要将字符编码为二进制,那么有没什么统一的标准呢?例如字符‘A’的编码到底是多少?如果没有统一的标准,有的人认为‘A’的编码是1,有的人认为‘A’的编码是2,就会造成同一段二进制数字在不同计算机上显示出来的字符不一样的情况,因此必须得定一个统一的、标准的转换规则。
接下来我们就来介绍一下一些常见到标准转换规则。
# ASCII编码
1968 年,ASCII编码出现了(全称American Standard Code for Information Interchange,美国信息互换标准代码)。特点如下:
用一个字节来存储一个字符(最高位统一是0,只用了剩下的7位来表示字符。2^7 ^=128)
共收录了128个字符(也可以理解这128个字符组成了ASCII字符集),分别如下:
- 基本的英文字母(从abcd到……xyz)
- 阿拉伯数字(0123456789)
- 标点符号(逗号句号等)
- 特殊符号(感叹号、@,井号等)
- 一些带有控制功能的字符(例如换行符,回车符)
前3类,我们可以称为可见字符,就是可以用肉眼看到的;那为什么有控制字符呢?举个例子,计算机中处理字符,经常需要打印(在早期计算机就是通过纸带来输出的),我们就用控制字符来告诉计算机如何换行,不然全都打印到一行上,就会相互覆盖。
在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码原来的字母和标点符号都还保留了;
- 两个大于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。
感兴趣的同学可以去国家标准全文公开系统 (opens new window)去查看更多信息。
# 中文编码小结
从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里
以上这些各个国家为了兼容而提出的编码,统称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个字节存储一个字符,能存储2^16^ = 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
2
用vim查看编码:在命令模式下输入 一下命令
:set fileencoding
用vim转换编码:在命令行里输入
:set fileencoding=utf-8
在Linux下,还有一个专门用来处理编码的工具:enca,感兴趣的读者可以去搜索下相关介绍。
# 小结
什么是字符:字符就是一个文字的符号,例如汉字,例如英文字幕
什么是字符编码:告诉计算机如何保存与处理字符的机制。
常见的字符编码:
- ASCII
- ANSI编码
- UTF编码
这里附上一个示意图