0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Unicode和UTF-8、UTF-16到底有什么不同?

Linux爱好者 来源:Linux开发那些事儿 作者:LinuxThings 2021-06-11 16:14 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

由于字符编码不同,计算机在不同国家之间的交流变得很困难,经常会出现乱码的问题,比如:对于同一个二进制数据,不同的编码会解析出不同的字符。

当互联网迅猛发展,地域限制打破之后,人们迫切的希望有一种统一的规则, 对所有国家和地区的字符进行编码,于是 Unicode 就出现了。

Unicode 简介

Unicode 是国际标准字符集,它将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。

Unicode 字符集的编码范围是0x0000 - 0x10FFFF, 可以容纳一百多万个字符, 每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫码点, 比如:汉字"中"的 码点是0x4E2D, 大写字母A的码点是0x41, 具体字符对应的 Unicode 编码可以查询Unicode字符编码表。

字符集和字符编码

字符集是很多个字符的集合,例如 GB2312 是简体中文的字符集,它收录了六千多个常用的简体汉字及一些符号,数字,拼音等字符

字符编码是 字符集的一种实现方式,把字符集中的字符映射为特定的字节或字节序列,它是一种规则

比如:Unicode 只是字符集,UTF-8、UTF-16、UTF-32 才是真正的字符编码规则

Unicode 字符存储

Unicode 是一个符号集, 它只规定了每个符号的二进制值,但是符号具体如何存储它并没有规定

前面提到, Unicode 字符集的编码范围是0x0000 - 0x10FFFF,因此需要 1 到 3 个字节来表示

那么,对于三个字节的 Unicode字符,计算机怎么知道它表示的是一个字符而不是三个字符呢 ?

如果所有字符都用三个字节表示,那么对于那些一个字节就能表示的字符来说,有两个字节是无意义的,对于存储来说,这是极大的浪费,假如 , 一个普通的文本, 大部分字符都只需一个字节就能表示,现在如果需要三个字节才能表示,文本的大小会大出三倍左右

因此,Unicode 出现了多种存储方式,常见的有 UTF-8、UTF-16、UTF-32,它们分别用不同的二进制格式来表示 Unicode 字符

UTF-8、UTF-16、UTF-32 中的 "UTF" 是 "Unicode Transformation Format" 的缩写,意思是"Unicode 转换格式",后面的数 字表明至少使用多少个比特位来存储字符, 比如:UTF-8 最少需要8个比特位也就是一个字节来存储,对应的, UTF-16 和 UTF-32 分别需要最少 2 个字节 和 4 个字节来存储

UTF-8 编码

UTF-8: 是一种变长字符编码,被定义为将码点编码为 1 至 4 个字节,具体取决于码点数值中有效二进制位的数量

UTF-8 的编码规则:

对于单字节的符号,字节的第一位设为0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的, 所以 UTF-8 能兼容 ASCII 编码,这也是互联网普遍采用 UTF-8 的原因之一

对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码

下表是Unicode编码对应UTF-8需要的字节数量以及编码格式

Unicode编码范围(16进制) UTF-8编码方式(二进制)
000000 - 00007F 0xxxxxxxASCII码
000080 - 0007FF 110xxxxx10xxxxxx
000800 - 00FFFF 1110xxxx10xxxxxx10xxxxxx
01 0000 - 10 FFFF 11110xxx10xxxxxx10xxxxxx10xxxxxx

表格中第一列是Unicode编码的范围,第二列是对应UTF-8编码方式,其中红色的二进制"1"和"0"是固定的前缀, 字母x表示可用编码的二进制位

根据上面表格,要解析 UTF-8 编码就很简单了,如果一个字节第一位是0,则这个字节就是一个单独的字符,如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节

下面以"中"字 为例来说明 UTF-8 的编码,具体的步骤如下图, 为了便于说明,图中左边加了1,2,3,4的步骤编号

pYYBAGDDHVmASHtDAADDy18oYWE750.jpg

首先查询"中"字的 Unicode 码0x4E2D, 转成二进制, 总共有 16 个二进制位, 具体如上图 步骤1 所示

通过前面的 Unicode 编码和 UTF-8 编码的表格知道,Unicode 码0x4E2D对应000800 - 00FFFF的范围,所以,"中"字的 UTF-8 编码 需要3个字节,即格式是1110xxxx10xxxxxx10xxxxxx

然后从"中"字的最后一个二进制位开始,按照从后向前的顺序依次填入格式中的x字符,多出的二进制补为0, 具体如上图 步骤2、步骤3 所示

于是,就得到了"中"的 UTF-8 编码是111001001011100010101101, 转换成十六进制就是0xE4B8AD, 具体如上图 步骤4 所示

UTF-16 编码

UTF-16 也是一种变长字符编码, 这种编码方式比较特殊, 它将字符编码成 2 字节 或者 4 字节

具体的编码规则如下:

对于 Unicode 码小于0x10000的字符, 使用2个字节存储,并且是直接存储 Unicode 码,不用进行编码转换

对于 Unicode 码在0x10000和0x10FFFF之间的字符,使用4个字节存储,这4个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前6位二进制固定为110110,后面两个字节的前 6 位二进制固定为110111, 前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去0x10000的结果

大于0x10FFFF的 Unicode 码无法用 UTF-16 编码

下表是Unicode编码对应UTF-16编码格式

Unicode编码范围(16进制) 具体Unicode码(二进制) UTF-16编码方式(二进制) 字节
0000 0000 - 0000 FFFF xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 2
0001 0000 - 0010 FFFF yy yyyyyyyy xx xxxxxxxx 110110yy yyyyyyyy110111xx xxxxxxxx 4

表格中第一列是Unicode编码的范围,第二列是 具体Unicode码的二进制 ( 第二行的第二列表示的是 Unicode 码 减去0x10000后的二进制 ) , 第三列是对应UTF-16编码方式,其中红色的二进制"1"和"0"是固定的前缀, 字母x和y表示可用编码的二进制位, 第四列表示 编码占用的字节数

前面提到过,"中"字的 Unicode 码是4E2D, 它小于0x10000,根据表格可知,它的 UTF-16 编码占两个字节,并且和 Unicode 码相同,所以"中"字的 UTF-16 编码为4E2D

我从Unicode字符表网站找了一个老的南阿拉伯字母, 它的 Unicode 码是:0x10A6F, 可以访问https://unicode-table.com/cn/10A6F/查看字符的说明, Unicode 码对应的字符如下图所示

poYBAGDDHVGAeIyfAABh4fE239A398.jpg

下面以这个 老的南阿拉伯字母的 Unicode 码0x10A6F为例来说明 UTF-164字节的编码,具体步骤如下,为了便于说明,图中左边加了1,2,3,4 、5的步骤编号

pYYBAGDDHUqAKliRAACwduskwHY587.jpg

首先把 Unicode 码0x10A6F转成二进制, 对应上图的 步骤 1

然后把 Unicode 码0x10A6F减去0x10000, 结果为0xA6F并把这个值转成二进制00 0000001010 01101111,对应上图的 步骤 2

然后 从二进制00 0000001010 01101111的最后一个二进制为开始,按照从后向前的顺序依次填入格式中的x和y字符,多出的二进制补为0, 对应上图的 步骤 3、 步骤 4

于是,就计算出了 Unicode 码0x10A6F的 UTF-16 编码是11011000 0000001011011110 01101111, 转换成十六进制就是0xD802DE6F, 对应上图的 步骤 5

UTF-32 编码

UTF-32 是固定长度的编码,始终占用 4 个字节,足以容纳所有的 Unicode 字符,所以直接存储 Unicode 码即可,不需要任何编码转换。虽然浪费了空间,但提高了效率。

UTF-8、UTF-16、UTF-32 之间如何转换

前面介绍过,UTF-8、UTF-16、UTF-32 是 Unicode 码表示成不同的二进制格式的编码规则,同样,通过这三种编码的二进制表示,也能获得对应的 Unicode 码,有了字符的 Unicode 码,按照上面介绍的 UTF-8、UTF-16、UTF-32 的编码方法 就能转换成任一种编码了

UTF 字节序

最小编码单元是多字节才会有字节序的问题存在,UTF-8 最小编码单元是一字节,所以 它是没有字节序的问题,UTF-16 最小编码单元是 2 个字节,在解析一个 UTF-16 字符之前,需要知道每个编码单元的字节序

比如:前面提到过,"中"字的 Unicode 码是4E2D,"ⵎ"字符的 Unicode 码是2D4E, 当我们收到一个 UTF-16 字节流4E2D时,计算机如何识别它表示的是字符"中"还是 字符"ⵎ"呢 ?

所以,对于多字节的编码单元,需要有一个标记显式的告诉计算机,按照什么样的顺序解析字符,也就是字节序,字节序分为 大端字节序 和 小端字节序

小端字节序简写为 LE( Little-Endian ), 表示 低位字节在前,高位字节在后, 高位字节保存在内存的高地址端,而低位字节保存在内存的低地址端

大端字节序简写为 BE( Big-Endian ), 表示 高位字节在前,低位字节在后,高位字节保存在内存的低地址端,低位字节保存在在内存的高地址端

下面以0x4E2D为例来说明大端和小端,具体参见下图:

poYBAGDDHUOAHvDhAABQAX9JQcg147.jpg

数据是从高位字节到低位字节显示的,这也更符合人们阅读数据的习惯,而内存地址是从低地址向高地址增加

所以,字符0x4E2D数据的高位字节是4E,低位字节是2D

按照大端字节序的高位字节保存内存低地址端的规则,4E保存到低内存地址0x10001上,2D则保存到高内存地址0x10002上

对于小端字节序,则正好相反,数据的高位字节保存到内存的高地址端,低位字节保存到内存低地址端的,所以4E保存到高内存地址0x10002上,2D则保存到低内存地址0x10001上

BOM

BOM 是 byte-order mark 的缩写,是 "字节序标记" 的意思, 它常被用来当做标识文件是以 UTF-8、UTF-16 或 UTF-32 编码的标记

在 Unicode 编码中有一个叫做 "零宽度非换行空格" 的字符 ( ZERO WIDTH NO-BREAK SPACE ), 用字符FEFF来表示

对于 UTF-16 ,如果接收到以FEFF开头的字节流, 就表明是大端字节序,如果接收到FFFE, 就表明字节流 是小端字节序

UTF-8 没有字节序问题,上述字符只是用来标识它是 UTF-8 文件,而不是用来说明字节顺序的。"零宽度非换行空格" 字符 的 UTF-8 编码是EF BB BF, 所以如果接收到以EF BB BF开头的字节流,就知道这是UTF-8 文件

下面的表格列出了不同 UTF 格式的固定文件头

UTF编码 固定文件头
UTF-8 EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE 00 00 FE FF

根据上面的 固定文件头,下面列出了"中"字在文件中的存储 ( 包含文件头 )

编码 固定文件头
Unicode 编码 0X004E2D
UTF-8 EF BB BF4E 2D
UTF-16BE FE FF4E 2D
UTF-16LE FF FE2D 4E
UTF-32BE 00 00 FE FF00 00 4E 2D
UTF-32LE FF FE 00 002D 4E 00 00

常见的字符编码的问题

Redis 中文key的显示

有时候我们需要向redis中写入含有中文的数据,然后在查看数据,但是会看到一些其他的字符,而不是我们写入的中文

pYYBAGDDHTyAUA9FAAA9FlkDs7c977.jpg

上图中,我们向redis 写入了一个 "中" 字,通过 get 命令查看的时候无法显示我们写入的 "中" 字

这时候加一个 --raw 参数,重新启动 redis-cli 即可,也即 执行 redis-cli --raw 命令启动redis客户端,具体的如下图所示

poYBAGDDHTOADwqeAABSHh9Q28I081.jpg

MySQL 中的 utf8 和 utf8mb4

MySQL 中的 "utf8" 实际上不是真正的 UTF-8, "utf8" 只支持每个字符最多 3 个字节, 对于超过 3 个字节的字符就会出错, 而真正的 UTF-8 至少要支持 4 个字节

MySQL 中的 "utf8mb4" 才是真正的 UTF-8


下面以 test 表为例来说明, 表结构如下:

mysql> show create table testG *************************** 1. row *************************** Table: test Create Table: CREATE TABLE `test` ( `name` char(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 1 row in set (0.00 sec)

向test表分别插入"中"字 和 Unicode 码为0x10A6F的字符,这个字符需要从https://unicode-table.com/cn/10A6F/直接复制到 MySQL 控制台上,手工输入会无效,具体的执行结果如下图:

pYYBAGDDHSuAZP0jAAC80Xc5gO4372.jpg

从上图可以看出,插入"中"字 成功,插入0x10A6F字符失败,错误提示无效的字符串,xF0X90XA9xAF正是0x10A6F字符的 UTF-8 编码,占用4个字节, 因为 MySQL 的 utf8 编码最多只支持3个字节,所以插入会失败

把test表的字符集改成utf8mb4, 排序规则 改成utf8bm4_unicode_ci, 具体如下图所示:

poYBAGDDHSSAMudtAAE2A0yDEDo599.jpg

字符集和排序方式修改之后,再次插入0x10A6F字符, 结果是成功的,具体执行结果如下图所示

poYBAGDDHR6AfRgyAAB3UflKJFw084.jpg

上图中,set names utf8mb4是为了测试方便,临时修改当前会话的字符集,以便保持和 服务器一致,实际解决这个问题需要修改my.cnf配置中 服务器和客户端的字符集

小结

本文从字符编码的历史介绍了 Unicode 出现的原因,接着介绍了 Unicode 字符集中 三种不同的编码方式:UTF-8、UTF-16、UTF-32 以及它们的的编码方法,紧接着介绍了 字节序、BOM ,最后讲到了字符集在 MySQL 和 Redis 应用中常见的问题以及解决方案 。

责任编辑:lq6

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 字符
    +关注

    关注

    0

    文章

    237

    浏览量

    26054
  • Unicode
    +关注

    关注

    0

    文章

    25

    浏览量

    12869
  • UTF-8
    +关注

    关注

    0

    文章

    13

    浏览量

    8068

原文标题:Unicode、UTF-8、UTF-16,终于懂了

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    labview怎么读取UTF-16 LE 文本数据

    各位大佬 请问labview怎么读取UTF-16 LE 文本数据 直接读数据出来会有空格 但是空格无法删除,请问怎么获取UTF-16 LE格式数据
    发表于 10-10 11:23

    魔女开发板stm32F407无法下载程序怎么解决?

    RT-Studio生成程序后编译完成0错误0警告,但是无法下载程序,出现如图(1)警告: 尝试了诸如生成代码是更换控制台调试串口引脚,更新pyocd版本,更改设置编码为(UTF-8),(ASCII)均无法解决该问题,球球各位大佬能不能帮忙解答一下?
    发表于 10-10 07:09

    rt-thread程序中的汉字字符串,如何提取汉字的gb2312编码?

    我希望程序中的汉字字串“欢迎光临”取出来的字节数组是GB2312码:“BB B6 D3 AD B9 E2 C1 D9 00”, 而实际出来的是UTF-8码:“E6 AC A2 E8 BF 8E E5
    发表于 09-29 07:14

    求助各位大佬,关于如何读写编码格式为UTF-16LE的配置文件?

    如题,小白求助各位大佬,要怎么实现对编码为UTF-16LE的配置文件的读写。 使用读配置文件的vi读不出内容。 使用读文本的方式写入异常,文件的编码变成UTF-8且内容也不对。
    发表于 09-14 12:54

    如何使用 UTF-8 编码?

    如何使用 UTF-8 编码?
    发表于 09-04 06:21

    电磁干扰“江湖三兄弟”:EMC、EMI、EMS 到底有啥区别?

    电磁干扰“江湖三兄弟”:EMC、EMI、EMS 到底有啥区别?
    的头像 发表于 08-20 15:16 1833次阅读
    电磁干扰“江湖三兄弟”:EMC、EMI、EMS <b class='flag-5'>到底有</b>啥区别?

    【创龙TL3562-MiniEVM开发板试用体验】8、FreeType显示矢量文字

    !=0) buf.vaddr[ y*buf.width + x] = fcolor; } int utf_8_to_unicode_string(uint8_t *utf_8,uint16
    发表于 08-05 11:54

    求助,关于STM32Cubemx 6.15版本生成工程的文件编码的问题求解

    之前升级6.13和6.14版本的时候重新生成工程会将文件编码强制转换成UTF-8格式,导致中文注释乱码。但是在安装文件夹下面的STM32CubeMX.l4j.ini文件里面添加一行
    发表于 07-29 12:31

    漫画科普 | 功率放大器到底有哪些应用?带你解锁功放经典应用场景!(一)

    漫画科普 | 功率放大器到底有哪些应用?带你解锁功放经典应用场景!(一)
    的头像 发表于 06-20 20:00 779次阅读
    漫画科普 | 功率放大器<b class='flag-5'>到底有</b>哪些应用?带你解锁功放经典应用场景!(一)

    实测 PTR54LS05低功耗到底有多低

    实测 PTR54LS05低功耗到底有多低?
    发表于 04-27 10:57

    TLC2578芯片中FS与SDI到底有什么作用?

    ,还有就是一点不太懂的就是:TLC2578芯片中FS与SDI到底有什么作用。手册看了半天还是不懂!求解!谢谢!
    发表于 01-22 06:51

    RK3506到底有多香?抢先看核心板详细参数配置

    RK3506到底有多香?触觉智能已推出RK3506核心板,抢先了解核心板详细参数配置!
    的头像 发表于 01-18 11:33 3186次阅读
    RK3506<b class='flag-5'>到底有</b>多香?抢先看核心板详细参数配置

    24位或者说高分辨率的AD到底有什么用呢?

    的AD,如24位的AD,其分辨率达到很低的uV级别,我们如何考究其精度?而且AD的精度受到诸多因素的影响,其中参考源的稳定度和供电电源的稳定度对精度影响很大,参考源最低0.05%的精度,那么24位的分辨率所可以达到的精度却是要大打折扣的,请问在这样的情况下,24位或者说高分辨率的AD到底有什么用呢?
    发表于 01-07 06:49

    差分输入和和单端输入在本质上到底有什么区别?

    和和单端输入在本质 上到底有什么区别? 因为,ADC采集的信号说到底是AINP - AINN,不管单端还是差分,采集的信号都是这两个pad的差值。 2:将单端信号接在ADC的差分输入接口上可以用
    发表于 12-23 07:31

    TFP401APZP到底有没有HSYNC输出?

    请教下TI的大牛,TFP401APZP这颗IC到底有没有HSYNC输出?实测发现HSYNC无输出,是要做什么设置么?!
    发表于 12-20 07:28