Unicode utf8等编码类型的原理
文章介绍了ASCII编码,Unicode,UTF-8 的基本知识
预备知识
二进制 比特 字节 16进制
1、ASCII码
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,
规定了128个字符的编码,比如空格“SPACE”是32(二进0010 0000),大写的字母A是65(二进制0100 0001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。
问题:
128个符号是不够的(对于非英语国家的),例如一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。
比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示256x256=65536个符号。
中文编码的问题需要专文讨论,这篇笔记不涉及。这里只指出,虽然都是用多个字节表示一个符号,但是GB类的汉字编码与后文的Unicode和UTF-8是毫无关系的。
2、Unicode
每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字表示的,这是一种所有符号的编码。是一种符号集
通过上一部分的问题:世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。电子邮件出现乱码
Unicode现在的规模可以容纳100多万个符号,具体的符号对应表,可以查询unicode.org
问题
Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
如何区别unicode和ascii?
计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
存储方式问题
如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
3、UTF-8
UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。
特点
它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
实现
1)对于单字节的符号,字节的第一位(字节的最高位)设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。多出的位补0
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
以下两个问题是在V2EX上发现的
问题一
第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10
这样设计的好处:
- 保证了一个 ASCII 字符或子字符串永远不会匹配到一个多字节编码的字符中间,明确了开头和结尾.
it knows to skip bytes (or go backwards) until the byte starts with
0
or11
; this prevents garbage values when a single byte gets corrupted.直接在一个 UTF-8 编码的字符串中搜索 UTF-8 编码的非 ASCII 的子字符串——无需关注码位边界。(没看懂)
问题二
UTF-8 不用完所有的有效 bit 呢?
- 单字节可以表示 [0, 0x7f]这些码点. 这和 UTF-8 一致
- 两个字节有可以表示 2^11 个码点, 范围从”单字节能表示的最大码点的后一个码点”开始, 码点范围也就是 [0x80, 0x80+2^11-1] = [0x80, 0x87F]
- 四个字节可以表示 2^21 个码点(实际上已经用不了那么多了), 范围从”三字节能表示的最大码点的后一个码点”开始, 到 Unicode 的最后一个码点结束, 也就是[0x10880, 0x10FFFF]
以汉字’你’为例. 编码过程如下:
- ‘你’字的码点是 0x4F60, 它落在了3个字节的范围
- 用它的码点减去”三字节码点的第一个码点 0x880”, 得到’你’在三字节里的偏移量: 0x4F60 - 0x880 = 0x46E0, 用前导 0 补齐 16 bit 的有效载荷. 得到 0100 011011 100000
- 填充到3字节的有效bit中, 得到最终的表示: 11100100 10011011 10100000 , 即 0xE49BA0
对多字节 0xE49BA0 的解码过程如下:
- 首字节高位有3个bit 的 1 , 表示这是三个字节组成的一个码点
- 取出有效 bit 位: 0100 011011 100000 === 0x46E0
- 加上”三字节码点的第一个码点 0x880”的偏移量, 即 0x46E0 + 0x880 = 0x4F60, 也就得到了’你’的码点
以上的推演应该没有弄错哪里吧(我经常弄错一些简单的东西但不自知)… 如果没弄错的话, 它有一些微弱的优势:
- 两字节能比 UTF-8 多表示 128 个码点, 这部分码点在 UTF-8 里需要用 3个字节表示
- 三字节能比 UTF-8 多表示 2048 个码点, 这部分码点在 UTF-8 里需要 4 个字节表示
解释
有一种针对中文 GB 编码的 SQL 注入方法,称为半字符注入或者宽字符注入,UTF-8 的这种设计则会避免这种攻击方式。(现在还不了解)
靠位移就能解码,按照你这种设计的话还要涉及到数学运算。先不说写程序更容易出 Bug。光说 CPU 运算效率,直接位移速度要快很多,甚至还能直接做成 CPU 指令集,或者利用已有的 SIMD 进行快速处理。你可以搜一下 UTF-8 SIMD 看看别人的快速实现
4、Little endian和Big endian
Unicode码可以采用UCS-2格式直接存储。以汉字”严“为例,Unicode码是4E25,需要用两个字节存储,一个字节是4E,另一个字节是25。存储的时候,4E在前,25在后,就是(第一个字节在前)Big endian方式;25在前,4E在后,就是Little endian方式。
Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做”零宽度非换行空格“(ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1
参考
文章主要参考了开始的 Unicode utf8等编码类型的原理 文章介绍的比较详细 有对于各种编码方式的实践操作 在本文中没有涉及.
关于utf-8部分参考了V2EX上的讨论 很详细 本文作者理解能力有限 有些地方理解不了 可以去看看