背景
上周在评审测试用例时,有一个营销话术的接口字段,业务上要求不能超过200字,会上有人问,后端数据表中的这个字段,最多能存储多少个中文字符,有没有对存储字数做限制。我插入了一句,那要看数据表中这个字段定义的是什么数据类型(CHAR、VARCHAR,TEXT等),一个中文占两个字节,用这种数据类型的字节数除以2,就是能存储的中文字符数。然后另一个同事说,UTF-8编码,一个中文占用3个字节。这句话颠覆了我之前的认知,在我的印象里,一个中文占用两个字节,怎么会是三个。我决定查一查,看看谁对谁错。
一石激起千层浪
本以为搜索一下就能获得答案,可是发现这个知识点,有些渊源,一言难尽。说得太概括,人难免还是会有疑问。需要追根溯源,才能讲清楚。在查找答案的过程中,发现网上的文章良莠不齐,对于同一个知识点,不同的文章说法相互矛盾,让人思维有些凌乱,对于这种情况,我选择取这些文章的交集,摒弃矛盾与冲突。
缘起ASCII码
字符编码的起源,是为了解决在计算机中存储与表达特定字符的问题。比如说英文字母A, 如何表达才能让计算机能够识别。众所周知,在计算机底层只识别0和1。当计算机要存储/展示字符时,需要一个规则,在字符和 0/1 序列之间建立映射关系,这就是字符编码规则。
1945年世界第一台计算机诞生于美国,所以美国人第一个遇到字符编码问题。自然而然第一个编码规则也是美国人制定的。美国使用的是英语,英语字符数量比较少,26个英文字母+数字+标点符号。一个字节是8位,如果每一个状态对应一个特定字符,每个二进制位有0
和1
两种取值,可以组合出256个字符,足够英语语境使用了。最早的字符编码标准就这样诞生了。美国人起草了计算机的第一份字符集和编码标准,叫 ASCII(American Standard Code for Information Interchange--美国信息交换标准代码),一共规定了 128 个字符及对应的二进制转换关系,128 个字符包括了可显示的26个字母(大小写)、10个数字、标点符号以及特殊的控制符,也就是英语与西欧语言中常见的字符。1967年定案,最初是美国国家标准,后来被国际标准化组织ISO(International Organization for Standardization)定为国际标准,称为ISO 646标准,适用于所有拉丁文字字母。
各国衍生自己的编码
计算机在世界普及之后,人们发现,在英语国家,128个字符编码够用了,但是对于非英语国家,无法在 ASCII 字符集中找到本国的基本字符。如在法语中,字母上方有注音符号,无法用 ASCII 码表示。于是有人建议,ASCII 字符只是使用了一个字节的前128个,后面的128个完全可以利用起来,于是一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的 é
的编码为130(二进制 10000010
)。这样一来,这些欧洲国家使用的编码体系,可以最多表示256个符号。
但是又出现了新的问题。不同国家有不同的字母(参见下图,英语,法语属于拉丁字母体系,俄语属于斯拉夫字母体系,以色列用的是希伯来字母),它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码中代表了 é
,在希伯来语编码中却代表了字母 Gimel
(ג
),在俄语编码中又代表另一个符号。128-255之间不同的字符集导致人们无法跨机器传播交流各种信息。
至于亚洲国家的文字,使用的符号就更多了,最典型就是中文,汉字多达10万左右。常用汉字有6000个左右,用一个字节是无法表示的,所以也发展出了自己的一套编码规范:
- 1980 年,中国搞了自己的编码方案GB/T 2312,一般简称 GB2312。
- 1993 年,国际标准化组织 ISO 制定了编码标准 ISO/IEC 10646-1:1993,国内予以承认,并编号为 GB 13000.1-1993。
- 1995 年,国内基于 GB2312 扩展了一套编码方案 GBK(汉字内码扩展规范),并收录了 GB13000.1 和 Big5(由台湾资讯工业策进会在1984 年制定)中的汉字,微软在 Windows 95、Windows NT 3.51 中进行了实现,称为 Code Page 936。
- 2000 年,制定了国标 GB 18030-2000,目前已作废。
- 2005 年,制定了国标 GB 18030-2005,为现行的 GB18030 标准。
- 2022 年,制定了国标 GB 18030-2022,2023年8月1日生效。
前面只介绍了西方国家和中文的编码,放眼全世界,有许多种语言文字,如果各自都搞一套就乱成一锅粥了。如果有一种编码,将世界上所有的符号都纳入其中。给每一个符号赋予一个独一无二的编码,那么相同的二进制值在世界不同国家编码中代表不同的字符乱局就会消失。
Unicode码统一乱局
Unicode码将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换。第一版 发布于1991年 ,目前已发展到第 15 版。Unicode 字符集的编码范围是 0x0000 - 0x10FFFF , 可以容纳一百多万个字符, 每个字符都有一个独一无二的编码,也即每个字符都有一个二进制数值和它对应,这里的二进制数值也叫 码点 , 比如:汉字 "汉" 的 码点是 0x6C49, 大写字母 A 的码点是 0x41, 具体字符对应的 Unicode 编码可以查询 Unicode字符编码表。
Unicode推行过程中遇到的问题
Unicode为每个字符规定了唯一的二进制代码,却没有规定这个二进制代码应该如何存储。例如,汉字的"汉" Unicode 编码是十六进制数0x6C49,表示这个符号需要2个字节。依此类推,表示其它在Unicode编码中排序更靠后的符号,需要3个或4个字节。
这就出现两个问题:
- 怎样区别 Unicode 和 ASCII 码?计算机无法知道三个字节是表示一个字符的Unicode码,还是分别表示三个字符的ASCII码。
- 英文字母只用一个字节表示就够了,如果按照 Unicode 编码,每个符号用三个或四个字节表示,英文文本文件的体积因此大出二三倍,这对存储来说是很大的浪费。
它们造成的结果是:
- Unicode 在很长一段时间内无法推广,由于 ASCII 字符经过 UTF-16 编码后得到的两个字节,高字节始终是 0×00,很多 C 语言的函数都将此字节视为字符串末尾从而导致无法正确解析文本。因此 UTF-16 刚推出的时候遭到很多西方国家的抵触,大大影响了Unicode 的推行。
- 为了平衡Unicode浪费存储空间和表达更多字符的问题,出现了多种不同的Unicode编码存储格式。
UTF-8 问世
伴随着互联网的普及,强烈需要一种统一的 Unicode 的编码方案, 尤其是跨国商务办公场景。UTF-8 是目前互联网上使用最广的一种 Unicode 的编码实现方式。Unicode的其它实现方式还有 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),在浏览器上很少用,在本地文件中用的较多。
UTF-8 最大的特点,就是它是一种"变长"编码方式。它可以使用1到4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8 编码规则是:
-
对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英文字母,UTF-8 和 ASCII 编码是相同的;
-
对于 n 字节的符号(n>1),第一个字节的前 n 位都设为1,第 n+1 位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码; 下表总结了编码规则,字母 x 表示可用编码的位:
UTF-8编码方式
Unicode符号范围 | 二进制表示 |
---|---|
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 |
基于UTF-8编码规则,解读 UTF-8 编码就非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
还是以汉字的汉为例,演示如何实现 UTF-8 编码:
汉的Unicode 码是U+6C49(二进制表示为 110 1100 0100 1001),根据上表,可以发现 6C49 处在第三行的范围内(0000 0800 - 0000 FFFF),因此汉的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从汉的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,汉的UTF-8编码是 11100110 10110001 10001001,转换成十六进制就是 0xE6B789。
乱码问题分析
所谓"乱码"是指应用程序显示出来的字符文本无法用任何语言去解读,通常会包含大量 ? 或 �,造成乱码的根本原因就是因为使用了错误的字符编码去解码字节流,想要解决乱码问题,就先要搞清楚应用程序当前使用的字符编码是什么。
比如最常见的网页乱码问题。需要从以下三个方面查找原因:
- 网页文件本身存储时使用的字符编码和网页声明的字符编码是否一致
html
<meta charset="utf-8" />
- 服务器返回的响应头 Content-Type 有没有指明字符编码
- 网页内是否使用 META HTTP-EQUIV 标签指定了字符编码
ini
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
MySQL中一个中文占用几个字节
varchar(n)能存储几个汉字?
varchar(n)表示n个字符,一个汉字也被视为一个字符,无论汉字和英文,MySQL都能存入n个字符,区别是实际占用字节长度不同
一个中文汉字占多少字节与编码有关
- UTF8:一个中文汉字=3个字节 (常用汉字是汉字总数中占比较小,常用汉字每个占用3个字节,多数不太常用的汉字每个占用4个字节)。
- GBK:一个中文汉字=2个字节
Unicode 简体中文字符集范围
Unicode 简体中文字符集的范围是U+4E00 到U+9FFF,共包括20992个字符。 其中:
- U+4E00 到U+62FF 是常用汉字区
- U+6300 到U+77FF 是次常用汉字区
- U+7800 到U+8CFF 是非常用汉字区
- U+8D00 到U+9FFF 是未分类汉字区
除了汉字,这个字符范围还包括了汉语拼音、注音符号、部分汉语方言文字和一些符号等。所以一个简体中文汉字的Unicode编码值是用2位16进制数表示,占用2个字节。
最后
我和同事到底谁对谁错,查完资料之后,客观的说,同事的说法更接近正确,我没有搞清楚Unicode字符集和Unicode编码的关系。字符编码是个看似容易,实则容易搞混淆的知识点。对于这类知识点,就得多花点时间,把容易搞混的概率梳理清晰,不然不经意间与别人争论起来,会贻笑大方。