问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

深入理解“字符编码模型”

创作时间:
作者:
@小白创作中心

深入理解“字符编码模型”

引用
1
来源
1.
https://www.cnblogs.com/zhe-si/p/16631919.html

字符编码是计算机处理文本的基础,它将人类可读的字符转换为计算机可以存储和处理的二进制数据。本文将深入探讨字符编码模型的各个层次,帮助读者理解这一看似简单却至关重要的技术细节。

前言

最近在处理后端文档生成时遇到了一些字符编码问题,这促使我深入研究了字符编码模型。如果你对Unicode、UTF-8、UTF-16、GBK等编码方式感到困惑,或者经常遇到乱码问题,这篇文章将为你提供全面的解答。

简述字符编码

相信大家都遇到过类似的情况:打开一个文本文件时出现乱码。这通常是因为字符编码设置不正确导致的。例如,一个采用UTF-8编码的文本文件,如果用错误的编码方式打开,就会出现乱码。

解决方案很简单,但在简单背后蕴含着丰富的知识体系,这就是“字符编码”。一个字符类型(char)长度为1字节,由多个char组成的数组(约定以\0结尾)就是字符串。但一个字节只能表示(2^8)(256)个数字,如何表示百倍于它的汉字呢?这就是字符编码要解决的问题。

字符编码就是将字符(包括英文字母、汉字等)编码为计算机可以存储与解析的字节流形式,同时也支持从字节流解析回字符的形式。这是对现实生活中用到的文字与符号的建模,将它们用一种计算机可以理解的方式表示,来方便计算机处理。

为了标准化字符编码的过程,人们对编码设计的过程进行划分,提出了字符编码的抽象架构模型,共有5层,分别解决了字符编码流程中的五个具体细节问题。

字符编码模型

设计字符编码,根据先后顺序可以分为以下五个步骤:

  1. 定义字符集:解决包含的字符范围的问题,声明都有哪些字符
  2. 编码字符集:解决如何用数字信号唯一的表示字符集中的每个字符
  3. 设计计算机保存字符编码用哪种数据类型以哪种规则保存:解决如何用某种数据类型描述字符编码后的数字信号
  4. 确定保存字符编码所用的数据类型如何映射到字节序列:解决数据类型(用来描述字符编码的数字信号)在计算机中(用字节序列)的表示方法
  5. 选择传输时合适的字节序列编码与压缩方案:解决描述字符串的字节序列在传输过程中的编码与压缩问题

基于上述五个步骤,定义:

[字符编码模型 = 抽象字符表+编码字符集(CCS)+字符编码表(CEF)+字符编码方案(CES)+传输编码语法 ]

五层模型

1. 抽象字符表(Abstract character repertoire)

抽象字符表定义了当前的字符编码所支持的所有抽象字符的集合。抽象字符是指人从视觉上认为不同而从含义逻辑上认为相同的一组实际字符的集合,可认为该集合中的字符表示的含义相同。一层含义是,一个汉字有楷、行、草、隶等多种形体,但都表示同一个汉字,如下图。另一层含义是,在Unicode中西班牙语的ñ由n和~两个字符组成,虽然看上去是一个,但是两种不同的含义。

抽象字符表有些标准是封闭性的,抽象字符集合不会改变(包括:ASCII、ISO 8859系列等);有些标准是开放性的,可以不断将新的字符添加到标准中(比如:Unicode)。

2. 编码字符集(CCS: Coded Character Set)

编码字符集在第一层抽象字符集的基础上,为每一个字符分配一个唯一的数字编码,让抽象的字符通过数字的方式表示出来。编码字符集是一个映射过程,将抽象字符集中的每一个字符一对一的映射到一个坐标(若是一维就是单个整数)上,而每一个映射到的坐标(也就是数字编码)称为码位(也称码点),每个字符所占的码位称为码位值。所以,也可以称:编码字符集就是把抽象字符集中的每个抽象字符映射为码位值。

用来表示码位的坐标空间的维度称之为编码空间,可用一组数字、存储单元尺寸或者一些特殊形式表示。例如:GB 2312汉字编码空间可表示为94×94;ISO-8859-1的编码空间可表示为8比特或256;Unicode采用行、列、面的三维描述表示码位值。

这里特别讲解一下Unicode(统一码)的编码字符集。每个Unicode字符编码可以表示为:U+6个十六进制数字,比如:'0'表示为'\U000030'。Unicode采用平面+16-bit编码方式,每个平面的编码空间为2^16(用'\U000030'的后四位表示,使用两个字节),共17个平面(用'\U000030'的前两位表示,使用一个字节),理论上能表示的字符数=平面数(17)×平面编码空间大小(2^16)=1114112。17个平面编号为0-16(0x00-0x10),如下图。

日常中常用的字符都定义在0号平面,该平面的码点表示时可以省略前两个十六进制位的平面号。平面中不是每个位置都定义了对应的字符,还有不少空间保留或作特殊用途。

每个抽象字符在Unicode中采用唯一且不可变的字符名称来表示,如:拉丁字母K在Unicode中的字符名称是“Latin Capital Letter K”,码点是004B。

3. 字符编码表(CEF: Character Encoding Form)

字符编码表将数字表示的码位值转换为整型值序列(由多个固定有限长度的整形数据类型组成)表示。用来表示码位的有限长度整形,是计算机表达字符编码(码位值)的单位,称为编码单元,简称码元。

定义字符编码表有两步:

  1. 定义码元
  2. 定义如何使用多个码元表示码点值的规则

定义码元通常采用8bit(字节)的倍数。码元的存在,规整了表示不同字符的存储方式,避免在一串字符中用各种长度的整形混合表示。在计算机中采用字节的倍数存储与传输也匹配其存储、传输和处理的单位,对应计算机中的数据类型。

定义用码元表示码位值的规则,分为定长编码和变长编码。定长编码就是自身到自身的映射,如ASCII的编码0-127,对应7bit,直接用1字节表示。UTF-32是Unicode对应的定长编码方案,字节内容一一对应码点。

变长编码基于某种规则将码位值根据需要映射到不同个数的码元序列上。

以Unicode最通用的字符编码表UTF-8为例,UTF-8是Unicode的一种边长编码,码元为8bit,采用1-4个码元(字节)表示一个字符,根据字符码位值的不同变换表示长度。编码规则如下:

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
  1. 对于单个字节的字符,第一位设为0,后面的7位对应这个字符的Unicode码点。因此,对于英文中的0-127号字符,与ASCII码完全相同。这意味着UTF-8完全兼容过去用ASCII编码的文档。
  2. 对于需要使用N个字节来表示的字符(N>1),第一个字节的前N位都设为1,第N+1位设为0,剩余的N-1个字节的前两位都设位10,剩下的二进制位则使用这个字符的Unicode码点来填充。

UTF-16则采用16bit(两个字节)码元,编码规则为:基本平面的字符占用2个字节,辅助平面的字符占用4个字节。而确定是用一个码元还是两个码元是通过基本平面中U+D800-U+DFFF的编码留白实现的。

辅助平面的字符位共有2^20个,因此表示这些字符至少需要20个二进制位。UTF-16将这20个二进制位分成两半,前10位映射在U+D800到U+DBFF(空间大小2^10),称为高位(H),后10位映射在U+DC00到U+DFFF(空间大小2^10),称为低位(L)。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。

第二层的编码字符集和该层字符编码表是多对多关系,一种编码方式也可应用多种字符集,如:EUC编码方式可以用于GB 2312,也可以用于JIS X 0208(一种日语字符集编码标准);一种字符集何以对应多种编码方式,如:Unicode对应UTF-8、UTF-16、UTF-32等编码方法,如下图所示。

例如,“汉字”这两个中文字符的Unicode码位值是0x6C49和0x5B57,可用码元对应整数类型的数组表示为

4. 字符编码方案(CES: Character Encoding Scheme)

字符编码方案将码元映射到字节序列。抽象字符的码位值可以通过具体数据类型的码元表示了,但由于这些数据类型可能需要多个字节才能表示,我们还没有解决码元如何用字节序列表示。码元映射为字节序列,也就是将特定的整数类型映射到对应的字节序列。一般讲的就是字节序,也就是大端和小端(当然,还有一些更复杂的)。

大端:低位地址存放高位数据,高位地址存放低位数据。与人的一般书写习惯一致,网络字节序要求使用大端。

小端:低位地址存放低位数据,高位地址存放高位数据。

如:数字0x0102,大端存储为[0x01, 0x02],小端存储为[0x02, 0x01]。

在编程中,我们大多时候无需关系字节序,而是直接使用具体的数据类型,字节序作为操作系统或硬件的内部实现对用户透明。但是文本不仅需要在本地内存中读写,还要再磁盘中存储并在多个异构系统中传阅,这就需要保证字节序一致或者读取到文本所使用的字节序。因此,为了表示码元的字节序列在读写时的一致性,需要定义字符编码方案。

解决字节序问题,一般有两种方案:

  1. 强制规定使用某种字节序。如网络传输强制要求网络字节序使用大端序。
  2. 使用字节序标记说明当前使用的字节序。字符集编码一般采用这种方案。Unicode编码方案中有个叫BOM(Byte Order Mark)的东西,就是用来做这事的。

当然,对于码元为单字节的情况下,不存在字节序问题,如UTF-8,这也是UTF-8广泛使用的原因之一。但一些UTF-8文件也存在BOM头,但这不是必须的,只是用来标识该文件采用UTF-8编码。

5. 传输编码语法(transfer encoding syntax)

传输编码语法用于处理第四层字符编码方案提供的字节序列,主要包括变换传输形式和压缩字节序列。

变换传输形式指将字节序列的值映射到一套更受限制的值域内,来满足传输环境限制。如:Email传输采用Base64或者quoted-printable,都是把8位的字节编码为7位长的数据。

压缩字节序列就是指一些无损字节序列压缩技术。如:LZW或者行程长度编码。

模型综述

从整体上看,字符编码模型是对人类理解的抽象字符到计算机实际表示、存储和传输字符的数据形式的建模过程。

第一层抽象字符表是对人类理解的抽象字符的总结,明确了抽象字符范围。每个抽象字符可能字形不同(写法不同),在不同语境下字符表示的含义不同,但从字符本身的角度逻辑相同,并采用字符名称等方式唯一的标识该字符。

第二层编码字符集则为抽象字符编号,将抽象字符表示成数学形式,类似模电和数电的关系,因为只有数字才能进一步保存到计算机。但注意,这一层并不涉及计算机,数学编号也是人类意义上的编号,但将形式上的符号抽象为数学编号表示,是用计算机建模现实事务的关键一步。

第三层字符编码形式是真正用计算机表示字符的第一步,这里采用计算机的抽象数据类型(码元)来表示人类对字符的抽象描述(数学编码)。

第四层字符编码方案则进一步将用计算机的抽象数据类型表示的字符映射到计算机真正的底层表示——字节流上。到这一层,字符已经完全转化为计算机的表示方式,计算机可以基于上述模型栈(其顺序处理的形式可以理解为栈)对字符进行读写或其他操作,并在计算机底层表示和人类的抽象字符间相互转化。

第五层传输编码语法是对计算机底层数据流额外的附加处理,来提升传输效率或满足传输要求。

上述字符编码模型可以进一步总结为一种计算机建模的通用思想:明确现实事物、建模事物、用计算机数据类型表示、用计算机底层字节序列表示、对字节序列的优化处理。

字符与字形

在前面的学习中,我们已经知道了通过字符编码模型将抽象字符转换为计算机底层数据结构的过程,好像已经圆满了。但请你重新审视你正在读的文字中的字符,并回忆刚刚所学,字符编码模型是否是完整的一条从你所见的字符到计算机底层表示的链路?

没错,缺少了字形。在抽象字符集中我们强调,字符集中的字符是逻辑上的抽象字符,而不是我们直接看到的字符,每个字符在不同的书写方式下都有多种字形表示。那么,现在是如何表示字形的呢?

字形描述,就是字体。字体描述了字符的形状,告诉了计算机如何“画出”某个字符,描述方式一般有散点和矢量。

由于本文重点在字符编码模型,所以在此不进行更详细的介绍,好奇的小伙伴可以看笔者的另一篇文章以TrueType为例谈字形描述。

举个实践例子

s := "hi你好 "
fmt.Println("runes: ")
for _, r := range s {
    fmt.Printf("%v ", r)
}
fmt.Println("\nbytes: ")
for i := 0; i < len(s); i++ {
    fmt.Printf("%v ", s[i])
}
fmt.Println("\n\nlen(s): ", len(s))

提问,上述go代码的输出结果是什么?

runes:
104 105 20320 22909 32             
bytes:                             
104 105 228 189 160 229 165 189 32 
                                   
len(s):  9 

这就是一个字符的码位(rune)和字节序列的对比使用场景。for-range遍历的是字符串中每一个字符的码位值。而字符串实际采用byte数组存储,通过len函数获取长度已经根据下标的索引都是读取字符底层的字节序列表示。也就是说,go中字符串本质上就是个byte数组,正好存储字符的底层字节表示,但提供了一个解析byte数组为字符的视图,让我们可以遍历读取字符串中的字符。

python中,也可以通过编码和解码,在字符串和bytes(字节数组)间转换。

后记

字符编码是电子文档的基础,也是编程的基础。只有了解了字符编码,才能对最常用的数据类型之一——字符串使用的游刃有余。之后会继续研究电子文档,并写两篇pdf、word、xlsx等场景文档的生成、修改和底层格式设计的文章。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号