通俗易懂的理解字符集和字符编码

前言

我们都知道,在计算机中,所有的信息都是使用 0 和 1 组成的二进制值来表示的。数字自然不必说,可以直接转换成二进制进行存储。那计算机中是如何表示字母和文字的呢?答案很简单,那就是对字母和文字进行数字化,既使用一个数字来代表一个字符,每个字符都有一个固定且唯一的数字与其对应。将数字与字符对应关系组成的集合就是我们常说的字符集。计算机想要识别和存储字符,光有字符集还不够,还需要使用字符编码把字符集中每个字符对应的数字编号转换成计算机能理解的二进制代码才行。下面让我们一起进入字符集和字符编码的世界看看吧。

字符集

字符集(Character set),顾名思义就是指多个字符的集合,集合中每个字符都有一个独一无二的编号。如我们常见的 ASCII 字符集,它里面就一共定义了 128 个字符,其中包括 33 个控制字符和 95 个可显示字符。比如大写字母 A 的 ASCII 码是 65(十进制),小写字母 b 的 ASCII 码是 98(十进制)等。

ASCII 字符集:

ASCII 字符集

字符集的种类很多,常见的字符集有:ASCII 字符集、GB2312 字符集、BIG5 字符集、Shift_JIS 字符集、GB18030 字符集、Unicode 字符集等等。

那为什么会有这么多的字符集呢?

答案很简单,因为 ASCII 不够用。我们知道,计算机中存储信息时,通常以字节为单位进行存储。而一个字节是由 8 个二进制位组成,也就是说一个字节,最多可以表示 256(2的8次方) 种不同的状态,每个状态对应一个字符的话,那一个字节最多就可以表示 256 个字符。

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

但是对于其他一些亚洲国家来说,他们使用的符号就更多了。比如我们的汉字就多达 10 万个左右,于是大家便各自进行扩展,也就出现了各种各样的字符集。如简体中文常用的字符集是 GB2312 字符集,对于日文常用的字符集是 Shift_JIS,繁体中文社区中最常用的字符集则是 BIG5。

到这里我们也就明白了为什么会有那么多字符集出现了。看似大家都完美的解决了各自文字编码的问题,但这背后却又引申出了其他的问题,最直接的就是导致文档在不同语言系统下可能会出现乱码。

大家都在为各自的文字进行编码,相互之间没有统一的标准,各不兼容。不同的字符集中相同的数字编号却对应着不同的字符,因此,同一份文档,拷贝到不同语言的机器时,就可能变成了乱码。于是人们就想:我们能不能定义一个超大的字符集,它里面可以容纳全世界所有的文字符号,然后再对它们进行统一编码,这样就不会再有乱码的问题了。

历史上存在两个独立的尝试创立单一字符集的组织:

  1. 国际标准化组织(ISO),他们于1984年创建了 ISO/IEC JTC1/SC2/WG2 工作组,试图制定一份“通用字符集”(Universal Character Set, 简称:UCS),并最终制定了 ISO 10646 标准。

  2. 统一码联盟,他们是由Xerox、Apple等软件制造商于 1988 年组成,并开发了 Unicode 项目。

一开始 ISO 10646 和 Unicode 是不兼容的,直到 1991 年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,他们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从 Unicode 2.0 开始,Unicode 采用了与 ISO 10646-1 相同的字库和字码;ISO 也承诺,ISO 10646 将不会替超出 U+10FFFF 的 UCS-4 编码赋值,以使得两者保持一致。目前两个项目仍都独立存在,并独立地公布各自的标准。

讲到这里,我们对字符集应该有个大致的了解了吧。既然我们已经把字符进行了数字化,那我们将字符集中字符对应的数字编号直接转换成二进制格式存储到计算机中不就可以了吗?为什么还要再进行编码呢?

在回答这个问题之前,我们先来简单的了解一下 Unicode 吧。

Unicode 是一个超大的字符集,它包含了世界上几乎所有现用语言的字符。它的编码范围从 U+0000 到 U+10FFFF,共有 1112064 个码位(code point)可用来映射字符。目前最新的版本为2023年9月公布的 15.1.0,已经收录超过 14 万个字符。Unicode 的编码空间可以划分为 17 个平面(plane),每个平面包含 65536(2的16次方)个码位。17 个平面的码位可表示为从 U+xx0000 到 U+xxFFFF,其中 xx 表示十六进制值从 00(十六进制)到 10(十六进制),共计 17 个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0),其他平面称为辅助平面(Supplementary Planes)。

比如我们的汉字“严”位于 Unicode 的第一个平面中,它的码位是 U+4E25,转换成二进制的话就是这样 0100 1110 0010 0101。而我们的汉字“𪚥”则位于 Unicode 的 2 号平面中,它的码位是 U+2A6A5,转换成二进制的话是这样 0000 0010 1010 0110 1010 0101。可以看到 Unicode 中想要表示一个汉字,至少需要 2 个字节。因为计算机中是通过 0 和 1 组成的二进制来存储和处理信息的,那这样就有两个很严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节都是 0,这对于存储和传输来说是极大的浪费,文本文件的大小也会因此大出二三倍。

看到这里我们也就明白了为什么有了字符集之后仍然需要字符编码,而不能直接存储字符集中的码点到计算机里面了。

字符编码

字符编码(Character encoding)是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8比特或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。简单理解字符编码就是把字符集中每个字符对应的数字编号转换成实际在计算机系统中存储或传输的二进制格式的过程。

字符集是字符的集合,字符编码是字符集的实现。Unicode 的实现方式称为 Unicode 转换格式(Unicode Transformation Format,简称为UTF)。一个字符集可以有多种编码方式,如 Unicode 字符集的编码方式就包括 UTF-7、UTF-8、UTF-16、UTF-32、GB18030 等,不同的编码方式适用于不同的场景和需求。目前常用的实现方式是 UTF-16小端序(LE)、UTF-16大端序(BE)和 UTF-8。

下面我们以最常用的 UTF-8 为例,简单介绍下它是如何实现对 Unicode 编码的。

UTF-8(8-bit Unicode Transformation Format)是一种针对 Unicode 的可变长度字符编码,也是一种前缀码。它可以使用一至四个字节对 Unicode 字符集中的所有有效码点进行编码。

UTF-8 的编码规则很简单,只有两条:

  1. 在 ASCII 码的范围,用一个字节表示,超出 ASCII 码的范围就用多字节表示,这样的好处是当 Unicode 文件中只有 ASCII 码时,存储的文件都为一个字节,所以就和普通的 ASCII 文件无异,读取的时候也是如此,所以能与以前的 ASCII 文件兼容。

  2. 超出 ASCII 码的,就会由上面的第一字节的前几位表示该 Unicode 字符的长度,比如 110xxxxx 前三位的二进制表示告诉我们这是一个 2 字节的 Unicode 字符;1110xxxx 是一个 3 字节的 Unicode 字符,依此类推;xxx 的位置由字符码点的二进制位填入。注意:在多字节串中,第一个字节的开头”1”的数量就是整个串中字节的数量。

通过下面的表格,我们可以更直观的看到 UTF-8 的编码规则:

Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 ):

码点的位数 码点起值 码点终值 字节序列 Byte 1 Byte 2 Byte 3 Byte 4
7 U+0000 U+007F 1 0xxxxxxx
11 U+0080 U+07FF 2 110xxxxx 10xxxxxx
16 U+0800 U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
21 U+10000 U+1FFFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面我们通过几个示例来演示下 UTF-8 编码:

  • 大写字母 A 的 Unicode 编码是 U+0041,它的 UTF-8 编码是 0010 1001,与 ASCII 编码一致。

  • 字符 © 的 Unicode 编码是 U+00A9(二进制是 1010 1001),超出了 ASCII 码。根据上表可以发现 U+00A9 处在 U+0080 - U+07FF 之间,所以 © 的UTF-8 编码需要 2 个字节,即它的编码格式是:1100 0010 1010 1001

  • 我们的汉字“严”的 Unicode 编码是 U+4E25(二进制是 0100 1110 0010 0101),根据上表可以发现 U+4E25 处在 U+0800 - U+FFFF 之间,所以“严”字的UTF-8 编码需要 3 个字节,即它的编码格式是:1110 0100 1011 1000 1010 0101(十六进制是:E4B8A5)。

  • 而我们的汉字“𪚥”它的 Unicode 编码是 U+2A6A5,转换成二进制的话是这样 0000 0010 1010 0110 1010 0101,根据上表可以发现,它的 UTF-8 编码需要 4 个字节,即它的格式是:1111 0000 1010 1010 1001 1010 1010 0101(十六进制是:F0AA 9AA5)。

可以看到,UTF-8 不但能编码 Unicode 标准中定义的所有字符,还能通过变长和 ASCII 完美的兼容。这也使得 UTF-8 成为互联网上使用最广泛的 Unicode 编码方案。

看到这里,想必大家对字符集和字符编码应该有个大致的了解了吧。最后我们简单总结下:字符集定义了字符和它们的唯一编号,这是字符编码的基础。字符编码就是依据字符集中的编号,采用某种规则把这些编号转换成计算机可以处理和存储的二进制数。

如何查看文本的二进制编码

说了那么多,大家可能都想知道要如何才能查看文本文件中字符的二进制编码吧?毕竟眼见为实嘛。

在 Linux 系统下,我们可以通过 xxd 命令打印文本文件的二进制编码。

  1. 查看文本文件的 UTF-8 编码

# 创建一个 UTF-8 编码的文件,输入汉字 “严” 后保存并退出。

vim  "+e ++enc=utf-8"  test_utf8.txt

# 使用 xxd 输出文件的十六进制表示

xxd  test_utf8.txt

# 输出内容如下,其中 e4b8a5 就是汉字 “严” 的 UTF-8 编码,后面的 0a 是 Linux 系统下默认的换行符 LF。

00000000:  e4b8  a50a ....
  1. 查看文本文件的 GB2312 编码

# 创建一个 GB2312 编码的文件,输入汉字 “严” 后保存并退出。

vim  "+e ++enc=gb2312"  test_gb2312.txt

# 使用 xxd 输出文件的十六进制表示

xxd  test_gb2312.txt

# 输出内容如下,其中 d1cf 就是汉字 “严” 的 GB2312 编码,后面的 0a 是 Linux 系统下默认的换行符 LF。

00000000:  d1cf  0a ...

我们可以通过在线网站来查询下汉字“严”的 UTF-8 和 GB2312 编码:

  • 汉字“严”的 UTF-8 编码:

汉字“严”的 UTF-8 编码

  • 汉字“严”的 GB2312 编码:

汉字“严”的 GB2312 编码

Unicode 编码查询可以通过 BabelMap Online 网站在线查询。

在文本框中输入想要查询的文字,点击 Hex refs 按钮,即可将文字转换成对应的 Unicode 编码。同时,也可以通过 Go To U+ 按钮,输入 Unicode 编码,来查询对应的字符。

Unicode 查询

参考

写在最后

如果本文对您有所帮助或者有所启发,请帮忙扫描下方二维码或微信搜索 「自在牛马」 关注一下我的公众号,您的支持是我最大的写作动力。感谢~

拒绝白嫖,转载请注明出处。
自在牛马

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!