字符编码那些事儿

身为一名要冲出国门的国际化码农🙃,字符编码是必备课题。小拽本文依次介绍下字节,ASCII,GB2312,GBK,GB18030,UNICODE,UTF8,UTF16,ICU 等到底是什么鬼?最后理论结合实际,研究下网站中经常出现的“锟斤拷,��,烫烫烫烫烫,屯屯屯屯屯屯”是什么神兵利器O(∩_∩)O?

一、二进制和字节

大概一百多年前,贝尔实验室制造了世界上的第一个晶体管,晶体管具有开合(0和1)的状态,这种01状态的变化被称为二进制位(bit)

过了几年,英特尔把八个可以开合的晶体管组合,做为一个基本的记录单元,这个单元被称作字节(byte),所以一个byte由8个bit构成,可以表达256种状态

又过几年,祖师爷冯诺依曼设计了一台可以存储和处理(冯诺依曼体系)字节变动的机器ENIAC,后来这个机器被称作计算机

二、标准ASCII

计算机运行是二进制,如何用二进制位来标识人类语言,就需要和计算机有一套约定关系。例如约定,0100 1111代表O,0100 1011代表K,那么存储为01001111 01001011的两个字节就代表OK,这套约定关系被称作字符编码

冯祖师爷是的德国人,二战去了美国设计了第一台计算机。起初,只有美国人能用计算机,山姆大叔就根据英语习惯设计了一套映射约定

  • 0-31 标识控制字符,例如换行[LF],删除[DEL],确认[ACK]等
  • 32-47 标识符号例如!@#$等
  • 48-57 标识0-9是个阿拉伯数字
  • 65-122 标识大小写字母

大家都按着这个约定来,交流表达起来没啥问题,呵呵,都挺好。于是这个方案就一致通过了,山姆大叔也给这个约定起了个名字ASCII编码(American Standard Code for Information Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文字符。

计算机一个标准字节8bit本身可以标识256个符号,但标准的ASCII的最高位去掉用做奇偶校验,用剩余7位标识128个符号,如下图

ASCII

三、ASCII 扩展字符集

随着计算机的发展,欧洲人开始逐步接触计算机了。

英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号

IBM牵头扩充了ASCII编码128-256位的标识字符,主要是一些欧洲的常用符号,这部分扩展映射被称为ASCII扩展字符集

四、GB2312

雄关漫道真如铁,而今迈步从头越,美帝国主义万万没有想到,二战后,大量第三世界的人民站起来了,逐步开始使用计算机。但问题是256个字符已经没啥可利用的字节状态来表示汉字了,更何况中华文明有6000多个常用汉字需要保存呢。

但是这难不倒智慧的中国人民,面对帝国主义的压迫,我们毫不客气的做了两件事情,并在1980年发表了这个声明

  • 互相尊重:尊重标准ASCII 规范中0-127位表示的标准字符。
  • 平等互利:ASCII的128-256位,我们用来标识中文,由于中文太多,我们要使用两个字节来表示一个中文^_^

中国人民觉的这个声明还不错,毕竟当时计算机的使用范围也不大,基本满足需求,于是就把这种汉字方案叫做 GB2312编码GB2312 是对 ASCII 的中文扩展

1
2
3
4
5
6
7
8
9
10
11
非专业人士可以忽略: GB2312如何组合,能表示多少个?
GB2312中用两个字节来标识一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA10xFE,简单计算
0xA110*16 + 1 = 161
0xF715*16 + 7 = 247 => 247-161 = 86
0xFE15*16 + 14= 254 => 254-161 = 93
因此GB2312可以标识约86*93=7998 个汉字
实时上 GB2312 标准共收录 6763 个汉字,其中一级汉字 3755 个,二级汉字 3008 个。
同时收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的 682 个字符。
几乎覆盖了大陆常用的99.75%汉字

这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的全角字符,而原来在127号以下的那些就叫半角字符了。

五、GBK

满足了基础和常用的汉字需求后,但依然会有很多人的生僻字名字打不出来,屌丝还好,但是一旦牵涉伟人名字打不出来那就坑爹了!改改改,抓紧改!

GB2312的编码,使用两个字符,每个都只用了后128位,不合理呀,干脆我们把低字节127号之后的内码我们也用了,不浪费

说干就干,于是扩展之后的编码方案被称为GBK标准(不知道K是不是扩展的缩写K^_^),GBK包括了GB2312的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号

1
2
3
4
5
6
7
8
9
10
非专业人士可以忽略: GBK如何组合,能表示多少个?
第一个字节的值从 0x810xFE保持不变,第二个字节的值扩展从 0x400xFE
0xFE-0x81254 - 8*16+1 =125
0xFE-0x40254 - 4*16 =190
因此,GBK约标识了 125*190 = 23750
GBK 共收入 21886 个汉字和图形符号,包括:GB2312 中的全部汉字、非汉字符号;BIG5中的全部汉字;ISO10646 相应的国家标准GB13000 中的其它 CJK 汉字
以上合计 20902 个汉字; 其它汉字、部首、符号,共计 984 个。

六、GB18030

中华民族大团结,后来少数民族也要用电脑了,于是我们需要再次扩展,又加了几千个新的少数民族的字,GBK扩成了GB18030。从此之后,中华民族的文化就可以完美的在计算机时代中传承了。

1
2
3
4
非专业人士忽略:
这一系列汉字编码的标准通为 `DBCS`(Double Byte Charecter Set 双字节字符集)。
在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,
必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了

从ASCII到GB2312,再到GBK,而后GB18030,中华民族终于完成了全量中文字符的编码,简单总结下

  • 第一阶段:中国人民通过对 ASCII 编码的中文扩充改造,产生了GB2312 编码,可以表示6000多个常用汉字。
  • 第二阶段:汉字实在是太多了,包括繁体和各种字符,于是产生了 GBK 编码,它包括了 GB2312 中的编码,同时扩充了很多。
  • 第三阶段:中国是个多民族国家,各个民族几乎都有自己独立的语言系统,为了表示那些字符,继续把 GBK 编码扩充为 GB18030 编码,完成全量中文字符编码。

六、UNICODE

之后的世界,百花齐放,百家争鸣,各国纷纷制造自己的编码规范,同时互相不去理解对方规范,即使同一种语言也区别巨大,例如台湾地区中文采用big5的繁体编码,名字也牛逼大了,叫大五码

各自为政引来了大量的问题,各个语言互不兼容,此时,一堆大佬看不下去了,勇敢的站了出来,着手解决这个问题,他们成立了一个类似于TC的组织,叫做ISO(International Organization for Standardization 国际标准化组织)。

他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE“。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码

UNICODE统一了各国,成为了实事上的大一统的编码规范。这种编码非常大,大到可以容纳世界上任何一个文字和标志。所以只要电脑上有 UNICODE 这种编码系统,无论是全球哪种文字,只需要保存文件的时候,保存成 UNICODE 编码就可以被其他电脑正常解释。

1
2
3
4
5
6
7
8
非专业人士忽略:unicode 编码
unicode开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。
于是ISO就直接规定:
1:必须用两个字节,也就是16位来统一表示所有的字符
2:对于ASCII里的那些“半角”字符,unicode包持其原编码不变,只是将其长度由原来的8位扩展为16位,
3:其他文化和语言的字符则全部重新统一编码。
由于”半角”英文符号只需要用到低8位,所以其高8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。

七、UTF,UTF8,UTF16

UNICODE很好的解决了不同语言统一编码的问题,但同样也不完美,有两个主要问题,

  • 字符识别:如何才能区别unicode和ascii?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
  • 存储浪费:我们已经知道,英文字母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储空间来说是极大的浪费,文本文件的大小会因此大出二三倍

此时UTF(unicode transfer format)标准出现了,顾名思义,是UNICODE在传输和存储过程中的格式化标准,其中使用最广的是utf8和utf16

UTF-16相对好理解,就是任何字符对应的数字都用两个字节来保存!我们通常对Unicode的理解就是把Unicode与UTF-16等同了。但是很显然如果都是英文字母这做有点浪费,明明用一个字节能表示一个字符为啥整两个啊。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,当字符在ASCII码的范围时,就用一个字节表示,保留了ASCII字符一个字节的编码做为它的一部分,注意的是unicode一个中文字符占2个字节,而UTF-8一个中文字符占3个字节)。从unicode到utf-8并不是直接的对应,而是要过一些算法和规则来转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
非专业人士直接忽略:unicode 如何转换成utf-8
以小拽的"拽"字为例
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
—————————————————————–
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
中文一般三个字节,前置标识位
举个栗子,中文:拽 unicode是25341
25341 十进制 unicode
0x62fd 十六进制
0110 0010 1111 1101 二进制
### 套上模板
0110 001011 111101 二进制 25341
1110xxxx 10xxxxxx 10xxxxxx 模板第三行
11100110 10001011 10111101 utf8 二进制
e 6 8 b b d utf8 十六进制【一切为了节省】
最终utf8拽对应的就是0xe68bdb

UTF-8就是在互联网上使用最广的一种unicode的实现方式,这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

简单对比下GB系列,UTF8,UTF16

  • UTF16:不推荐使用utf16,因为utf16最初能表示的字符数有6万多,看起来很多,但是实际上目前 Unicode5.0 收录的字符已经达到99024个字符,其实不够,有可能出现乱码
  • UTF8:国际化编码首推UTF8,兼容全量,唯一的问题是空间略有浪费!
  • GB系列:GB系列都是双字节字符集,相对节省空间,如果只是国内使用GB18030完全可以兼容所有

八、“锟斤拷��” 是什么

通过上面介绍,可以看出来,各个编码规则是不一样的,目前互联网浏览器默认传输和解析方式是UTF8,但是部分老的网页采用GB系列,就会出现传输过程UTF8解析不了,展示GB错乱问题。

UNIDCODE规定:当unicode遇到解释失败的字时,会尝试用 「U+FFFD」 来代替,「U+FFFD」乃是 unicode 的一个占位符, 显示为 �

而utf8识别为异常的传输字符后,传到页面转为双字节展示的GB会怎么样呢?

1
2
3
4
5
6
7
➜ xiaozhuai ✗ python
Python 2.7.10 (default, Aug 17 2018, 19:45:58)
[GCC 4.2.1 Compatible Apple LLVM 10.0.0 (clang-1000.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> s = (u'\uFFFD'.encode('utf8')*2)
>>> print(s.decode('gbk'))
锟斤拷

也就产生了,传说中的”锟斤拷”神器!

另外还有几个神器:”烫烫烫烫烫,屯屯屯屯屯屯”
“烫” 主要出没于 windows 平台下,ms 的 vc++ 编译器中, 当你在栈内开辟新内存时, vc 会使用 0xcc 来初始化填充, 很多个 0xcc 连起来就成了 烫烫烫烫烫 同理在堆内开辟新内存时, 会用 0xcd 填充,这便是 屯屯屯屯屯屯
不管是 “锟斤拷” 还是 “烫” 都要求最后是用GB码输出。

九、ICU

在unicode的统治下,世界各国的基本编码不会出现乱码等异常。但当中华民族逐步强大,准备冲出中国统一世界的时候,发现各国的货币,时间,数字等表示灰常不统一,例如数字1234.5,英文表示1,234.5,葡语表示确是1.234,5,很是苦恼。

此时IBM站了出来,叫上google,apple等小伙伴,遵循”IBM公共许可证”,开源了一套基于unicode的国际化组件ICU(International Component for Unicode
)。根据各地的风俗和语言习惯,实现对数字、货币、时间、日期、和消息的格式化、解析,对字符串进行大小写转换、整理、搜索和排序等功能,ICU4C提供了强大的BIDI算法,对阿拉伯语等BIDI语言提供了完善的支持。

ICU成为了目前国际化组件的实事标准,底层依赖UNICODE和CLDR,官方提供了C/C++和JAVA的SDK,ICU4C和ICU4J,同时,各个语言在此基础上开发了各个语言的版本,例如php的intl组件。

十、实事标准

字符编码的从产生,发展,到国际化一步一步走来,逐步形成了下列实事标准

  • 字符集:UNICODE
  • 字节编码:UTF8
  • 国际化:ICU

需要注意的是,mysql的utf8并不完全兼容标准的utf8编码,后续推出了utf8mb4完全兼容,所以推荐采用utf8mb4

参考网站:

崔小拽 wechat
欢迎您扫一扫上面的微信公众号,订阅小拽的博客!