锟斤拷
当互联网的浪潮在国内席卷的时候,在电脑前冲浪的新新人类们想必都见过这种乱码,它们乱且统一,规矩且诡异。不管是在学校的机房,门户网站的犄角旮旯,还是新建的记事本都曾发现过它们的身影,年轻的我们都曾疑惑过:乱码为什么有的长这样,有的长那样,为什么是锟斤拷, 是烫烫烫,是屯屯屯, 它们到底怎么来的?
时隔多年,我们回头再来打破砂锅问到底,把乱码的“乱”和乱码的“码”,娓娓道来。
合规性风险
计算机在普及之前,虽然经历过各种边界值测试,但是依然出现过很多大规模合规性风险问题,比如千年虫事件、GPS周数问题、UNIX时间戳溢出问题。这些问题冒头初期一度引发民间对计算机的信任危机。
拿千年虫事件来说,从发现到影响都被过度渲染,夸大其词,还一度被科幻媒体戴上了末日灾难的帽子,这在信息闭塞的年代造成了不同程度的恐慌。但是实际上大多数国家的公司早已经提前做好了准备,成功处理了因年份变更带来的各种数据异常。这些问题本质上暴露了计算机在发展过程中存在的计数、计时、存储力的储备瓶颈和边界隐患,消除这种隐患本身就是一个不断发现和解决问题的过程。
和地球围绕太阳公转的问题一样,为了保证计时的准确性,在日历上,人们规定每4年为闰年,打平因24小时制而带来的0.2422天偏差。而计算机时间的程序也加入了闰年逻辑,保证每年的年月日计算精准,人类在已发现领域规则上的应用往往都会立即更新到计算机,给计算机不断打上补丁,让计算答案不会“乱”。
计算机可识别语言的发展
计算机可识别语言的发展亦是如此,早期不同的计算机厂商采用的字符编码方式不一致,而且各自为营,互相通信非常困难。底层开发者苦不堪言,受够了这种打南边来了个喇叭,打北边又来了个鳎蚂的乱象,计算机语言的发展初期急需一纸“官令”来统一和标准化。
IEEE的出手便如一束光照亮大地,他们发明并统一了后来行业基石的字符集编码:ASCII码。在计算机行业,它的出现如同秦始皇统一文字和货币,标志着人类在计算机文明已经携手迈进了一个新台阶。
ASCII码
ASCII码(American Standard Code for Information Interchange,美国信息交换标准代码)是在20世纪60年代初由IEEE(美国电气和电子工程师协会,American Institute of Electrical and Electronics Engineers,简称IEEE)开发的一种字符编码标准。ASCII码采用7位二进制数编码了128个字符,其中包括英文字母、数字、标点符号和一些控制字符等,为计算机系统间的信息交换提供了一个标准的字符编码方式。
ASCII码从第一版标准发布于1963年 ,其中1967年经历了一次主要修订[5][6],最后一次更新则是在1986年,至今为止共定义了128个字符。这128个字符与键盘的按钮和布局相辅相成,从早期的机械式打孔发展到按键的统一,人机交互的翻译便是ASCII码。在键盘上按下字母“a”键,计算机会将其转化为 ASCII 码值为 97 的二进制数码,然后存储在内存中,在需要显示或打印的时候再转化成字符。
ASCII码的设计简单、空间也很节省,再加上IEEE的推动,很快便在美国这样的土壤下生根发芽、盘踞壮大,为当时的计算机公司扫清了交互的障碍。但是在美国之外的世界,它的局限性暴露无疑——英语的语言构成只有26个字母,算上大小写也只有52个,如此贫瘠的展示组合怎么也无法继续壮大到非英语的国土上。并且有相当一部分国家由于科技发展的落后,人民生活水平的质量低下,并没有为计算机语言字典里能有自己的语言一隅而去争取。于是在1991年,由几个实力强大的计算机公司(苹果、IBM、微软)自发组成了Unicode 联盟(Unicode Consortium),开发和维护了一套新的字符集标准:Unicode。
Unicode
Unicode 是一种字符集标准,它采用全球范围内的统一编码方式,为所有的语言和符号提供了唯一的编码。Unicode 囊括了全球几乎所有的语言和符号,并为它们赋予了唯一的编码。Unicode 标准采用 16 位或 32 位的编码方式表示字符,能够表示超过 110000 个字符,远远超过 ASCII 码的128个字符。Unicode也是兼容ASCII码的,其中前128个字符就是ASCII码的128个字符。
Unicode至今仍在不断增修,每个新版本都加入更多新的字符。具体字符对照表可见Unicode 15.0 Character Code Charts
对于 Unicode 编码,每个字符都可以用一个唯一的码点(Code Point,也叫码位)表示,汉字的码点可以用 Unicode 16进制表示,汉字的编码范围位于 0x4E00~0x9FBF 之间,共收录了近 21000 个汉字,它们被归类到CJK(中日韩)开头的东亚文字里,其中包括了简体字、繁体字、注音符号、部分方言文字以及一些历史上使用过的汉字。这些汉字按照笔画、偏旁部首、音序等方式进行编码,以便于计算机程序对它们进行处理和管理。
例如: “天” 字的 Unicode 代码点为 U+5929,也可以写成 0x5929,“哭”字的码点为U+54ED。
部分汉字在Unicode中的编码
Unicode就像全球的活字印刷模版,规定了一套标准的字符集编码,使得计算机在可识别语言文字上真正做到了万国统一,在操作系统、互联网协议、数据库管理、计算机编程等方面提供了基本通信语言。著名科幻小说《三体》里说过:“主想要你们阻碍发展,就先给你们不同的语言”。Unicode就是打破了计算机底层识别领域沟通阻碍的真正的可识别语言字典,它为计算机发展立下汗马功劳,称得上功在当代,利在千秋。
Unicode的存储
虽然Unicode统一了几乎所有的世界文字的计算机编码,但是在文件存储上却并非和它有关——它的本身只是一套编码方案,是一套文字的标准写法,在计算机存储时却并不适合直接转换为二进制存储。
在计算机里,一个字节(Byte)是存储和处理数据的基本单位之一,通常由8个二进制位(Bit)组成(也就是8比特),共可以表示256个不同的值(2的8次方)。
而每一个二进制位只有0和1两个状态,也就是只有两种取值用于表达信息。8个比特就可以组合成一个字节,表示256种不同的状态组合,因此一个字节可以表示一个ASCII字符、一个整数或者一个颜色值等。
Unicode编码的码点数量非常庞大,截至Unicode 13.0版本,它共定义了143859个码点,其中包括了各个语言和符号的字符、标点符号、表情符号和特殊符号等。这意味着,如果用每个编码点都用一个字节进行存储,需要至少使用143859个字节(约140KB),这是超出了绝大多数现代计算机的存储能力的。
为了解决这个问题,Unicode便采用了很多种编码方案。比如UTF-8、UTF-16、UTF-32等根据不同字节来表示的常用方案。这些编码方案对于不同的应用场景,可以提供不同的存储效率和处理速度,为计算机字符编码的格式提供了非常通用高效的解决方案。
UTF-8
UTF即Unicode Transformation Format,一种Unicode的通用转换格式
UTF-8 是一种采用可变长度编码的 Unicode 字符编码方式,它将 Unicode 中的每个字符编码成一系列 8 位字节的序列。UTF-8 最少使用一个字节(8 位),最多使用四个字节(32 位),根据不同的 Unicode 字符,使用不同的字节数来表示。
目前Unicode 编码共有三种具体实现:除了UTF-8外,还有UTF-16、UTF-32。 其中UTF-8使用最为广泛,主要由于它的以下几个优点:
- 兼容性:对于 ASCII 字符,UTF-8 使用一个字节来表示,和 ASCII 码完全兼容。
- 效率和空间利用率:对于汉字、日文、韩文等非 ASCII 字符,UTF-8 使用两个至四个字节来表示,使用率较低的字符使用更少字节数编码,使用率较高的字符使用更多字节数编码,保证编码效率和数据紧凑。
- 可逆: UTF-8 支持可逆转换,不同字节数的 UTF-8 字符串可以通过转换得到相同的 Unicode 字符串。
UTF-8的存储
现在,我们用一个汉字来展示它的编码原理。
在转换UTF-8编码前,需要查阅Unicode编码表,查看该字符的码点,确定需要使用几个字节:
- 单字节:对于编码值在 U+0000 到 U+007F 范围内的字符,UTF-8 使用单字节编码,即使用该字符的编码值作为该字符的 UTF-8 编码。
- 双字节: 对于编码值在 U+0080 到 U+07FF 范围内的字符,UTF-8 使用双字节编码,即用两个字节来编码该字符,第一个字节使用 0b110xxxxx 编码,第二个字节使用 0b10xxxxxx 编码。
- 三字节:对于编码值在 U+0800 到 U+FFFF 范围内的字符,UTF-8 使用三字节编码,即用三个字节来编码该字符,第一个字节使用 0b1110xxxx 编码,第二个字节使用 0b10xxxxxx 编码,第三个字节使用 0b10xxxxxx 编码。
- 四字节:对于U+10000至U+10FFFF这部分的字符,UTF-8使用四字节编码,但是其使用率偏低,我们在下面生僻字部分单独说明这块。
举个例子,对于“平”这个字,它的 Unicode 编码是 U+5E73。根据 UTF-8 的编码规则,它需要使用 3 个字节来表示(U+0800~U+FFFF)。具体的编码方式如下:
- 首先,将“平”字的 Unicode U+5E73编码转换为二进制:01 01 11 10 01 11 00 11。
- 接着,根据字符长度确定字节的开头格式。由于“平”字需要使用 3 个字节来表示,因此它的编码将以“1110xxxx 10xxxxxx 10xxxxxx”的格式开头。其中“x”表示实际的 Unicode 二进制编码值。
- 将 Unicode 编码的二进制值按照从左到右的顺序,依次填入 UTF-8 编码中。具体步骤如下:
- 将 Unicode 编码的前 4 个二进制位填入第一个字节的“xxxx”部分,得到 1110 0101,转换为十六进制得到 0xE5。
- 将 Unicode 编码的第 5 到第 10 个二进制位填入第二个字节的“xxxxxx”部分,得到 1011 1001,转换为十六进制得到 0xB9。
- 将 Unicode 编码的第 11 到第 16 个二进制位填入第三个字节的“xxxxxx”部分,得到 1011 0011,转换为十六进制得到 0xB3。
示例图
因此,“平”字的 UTF-8 编码为 0xE5 0xB9 0xB3, 即E5B9B3。
UTF-8的编码方式将Unicode尽可能多地使用1~2字节表示常用的字符,提高了存储和传输效率,在很多使用场景下,被默认为首选方案。但是字节依然是有先来后到的,其中优先纳入的文字就会占用1字节来展示,后来纳入的,可能就得3~4的字节,对于不同的受众来说,常用程度并不相同。拿汉字来说,在UTF-8中一般采用3字节进行编码,对中国宝宝的体质来说,就显得并没有那么极致紧凑了。
早在UTF-8出现之前,中国的前辈们就已经在思考:是不是应该有这样一种编码,只包含中文和ASCII的编码,只需要2个字节就够了?
GBK
对于大部分网站而言,它的客户群往往只需要一种语言就够了,国内的网站只要支持中文简、繁、英文即可适应绝大多数场景,而这种定位也就为早期的编码方案定制化确定了标准。
基于这样的考虑,中国国家标准总局于1981年制定并实施了 GB 2312-80 编码,即中华人民共和国国家标准简体中文字符集。后来微软利用GB 2312-80未使用的编码空间,不断扩展,收录了GB 13000.1-93全部字符制定了GBK编码,这便是后来中国人最常用的中文编码方案(早于UTF-8的)。
GBK 是一种中文编码标准,它是 GB2312 的扩展,支持了更多的中文字符。它使用双字节编码,1995年发布的1.0版本里规定了每个字节的范围都是 0x81 ~ 0xFE,第一个字节和第二个字节的范围各有 126 个,因此 GBK 编码可以表示 126 * 126 = 15876 个字符。在后续的修订版里新增了编码位置,每个字符在内存中都被表示成两个字节(16 位),因此可以存储 2的16次方=65536 个不同的字符。
满足汉字编码需求,因此它主要关注的是中文字符集,迄今未止,GBK收录有21003个汉字,883个符号,共21886个字符。
GBK 1.0版本
GBK编码中的字符编码是使用16进制数来表示的,一个汉字占用两个字节,其中一个表示高位,一个表示低位
例如,汉字“你”在GBK编码中的编码为0xC4E3(十六进制),其中高字节十六进制数是0xC4,低字节十六进制数是0xE3。
GBK编码曾被广泛应用于中文信息处理、网页设计、操作系统、数据库和其他各种计算机软硬件系统中,在中国,GBK曾经是非常流行的一种汉字字符集编码方式,特别是在早期的中文软件和操作系统中,它的足迹遍布大街小巷。但随着时代的变迁和技术的进步,GBK 编码的应用范围受到限制。由于它只能表示有限数量的字符,而且不支持国际化字符集,因此它已经逐渐被UTF-8 编码所取代。
现在看来,GBK在中文存储上节省了相当可观的空间,在早期相对贫瘠的磁盘存储能力背景下发挥了它应有的作用,甚至一度在20年代初成为国内一种主流编码,但是风云千樯,随着计算机软硬件发展迅猛,中文与其他文字的混合需求迫在眉睫,再加上与国际接轨的大背景,人民和国家交互更加多元化,GBK逐渐变为科技周刊纸媒一样的时代眼泪。
而锟斤拷、烫烫烫与屯屯屯便是那眼泪里一块小小的缩影。
锟斤拷、烫烫烫与屯屯屯
在计算机从学校机房才能看到发展到网吧遍地开花,再到个人电脑普及的背景下,GBK也如同一朵鲜艳的花开在了每台电脑的软硬件里,开在了那个时代的青少年的网络里。
锟斤拷,便是那时一声声“乱码”的代表之一,相较于特殊符号的组合乱码,锟斤拷是中文独属的“浪漫”,它的辨识度高到离谱,在某些场景下,甚至能和宫廷御液酒一样能区分国籍。
时至今日,我们再来重现一下它的诞生:
- 打开文本编辑器,随便输入几个文字,先使用GBK格式保存
- 再使用UTF-8字符集打开,并且保存,你会发现此时文字已经变成4个问号
- 不急,再次用GBK编码打开这个文件
你看,得到了两个锟斤拷。
拿出第一次发现它的好奇心,再问一遍:为什么不是拷斤锟或是鲲斤拷,偏偏就是锟斤拷?
必须先从图中的� � � �说起 。
�
在计算机里,因字符集问题导致文字无法正常显示时,也需要有一个字符来表示它,在Unicode中,这个字符就是 � ****,它也是Unicode中定义的一个特殊字符,就是“0xFFFD REPLACEMENT CHARACTER” ,所有无法表示的字符都会通过这个字符来表示, 相当于兜底字符。
Unicode官方有关于这个符号的介绍,里面解释了这个字符的使用场景和意义。该字符的16进制0xFFFD在10进制表示是65533,在UTF-8下它的16进制形式便是“0xEF 0xBF 0xBD”(三个字节)。
在上述的例子里有两个连续的字符都无法显示,那就是“� �” ,在UTF-8编码下,16进制表示为:
0xEF 0xBF 0xBD 0xEF 0xBF 0xBD
以上这段编码,放到GBK中进行解码的话,因为GBK中一个汉字两个字节,那么结果就是这样解码:
0xEF 0xBF, 0xBD 0xEF, 0xBF 0xBD
即
0xEFBF0xBDEF0xBFBD
展示出来,就是:锟(0xEFBF)斤(0xBDEF)拷(0xBFBD)
因为GBK 编码和 UTF-8 编码所占用的字节范围不同,导致在转换编码过程中出现了数据丢失或者数据错误的情况——具体来说,GBK 编码是一种固定长度的编码方式,而 UTF-8 是一种变长的编码方式,根据字符的不同编码范围,会占用不同长度的字节。一个 GBK 编码的文件,其中的汉字数据被错误解释为 UTF-8 编码的数据,那么在转换编码过程中,每个 GBK 编码的汉字都会被错误地当作 Unicode 编码下的多个字节来处理,例如被解析为三个字节表示一个汉字。由于这些字节不符合 UTF-8 编码规范,因此在保存时就会出现乱码或者显示为“� �”。而“� �”用GBK格式来解码,它就会转换为锟斤拷。
所以就锟斤拷是字符集编码的 �� 转换转出来的。
无独有偶,还有两组比较经典的乱码:烫烫烫和屯屯屯。
烫烫烫与屯屯屯
与锟斤拷不同,这俩倒不是因为编码转换的问题。在 Windows 平台中,为了帮助诊断和调试程序中的内存错误,未初始化的栈空间通常会被用16进制的 0xCC 进行填充,未初始化的堆空间使用0xCD填充。这是因为通常情况下,堆栈空间分配之后并不会被清空,其中可能会残留上一次调用中的数据,如果程序在使用这些未初始化的空间时发生了错误,那么可能会导致程序崩溃或者出现其他异常状况。因此,在进行调试时,将未初始化的空间用 0xCC/0xCD 进行填充,能够更容易地识别这些未初始化的数据。
即在VC编译器里,0xCC/0xCD 是一条中断指令,它会导致程序暂停执行,从而使调试器能够暂停程序的执行并分析栈空间中的数据,告诉你这里到头了。
而0xCCCC和0xCDCD在中文GBK编码中分别对应“烫”字和“屯”字,如果一段字符没有结束符(学过C的会知道,C语言里“\0”是结束符,它的ASCII码为0,也叫Null字符),那么从内存空间读出来的0xCC/0xCD就会被转换出来,变成烫烫烫与屯屯屯。
这种不同场景下出现的乱码在如今已经随着每个公司标准的统一制定越来越少见了,但是锟斤拷这种具有代表性的文字却成为了一种独特的计算机趣梗。
生僻字
前文提到过,在UTF-8里,大部分使用3个字节来表示,它们多为使用频率较高的常用汉字,很多数据库默认就是3字节的UTF8格式,根本没有考虑那些不常用的汉字或者后录入的符号,比如元素周期表里靠后的元素名、Emoji表情,这里要考虑的兼容性就为开发者们带来了痛苦面具。
MySQL就是这样。
MySQL的字符集UTF8MB4
在MySQL数据库中,utf8mb4是一种支持MySQL使用4字节UTF-8字符集的编码方式,它使用的是UTF-8的最大子集,可以表示所有的Unicode字符,包括表情符号等特殊字符。使用utf8mb4编码可以避免在存储和处理这些字符时出现乱码等问题。
需要注意的是,MySQL的默认编码方式是utf8(只支持最多3字节UTF-8字符集),如果需要使用utf8mb4编码,需要在创建数据库和表时指定编码方式为utf8mb4。此外,也需要在连接MySQL数据库时指定编码方式为utf8mb4,否则会出现乱码等问题。
utf8mb4是utf8的超集,mb4就是most bytes 4的意思,专门用来兼容四字节的Unicode,MySQL在5.5.3之后增加了utf8mb4的编码。
如果一个汉字的Unicode代码点在U+10000至U+10FFFF范围内,那么使用UTF-8编码时需要使用4个字节来表示。
以“𠄘”这个字为例,这个字的Unicode码点是U+201D8。它是一个汉字部首衍生字,称为“龖”(dàng),音同“当”。在Unicode标准的汉字部首扩展区中,该字符被归类在部首“龍”(龙)的下面,是个相当生僻的汉字。该字符的UTF-8编码是“\xf0\xa0\x84\x98”,需要4个字节来表示。
在需要中文支持的场景里,不管是手写识别的问题还是地址本身就是生僻字,我们都必须要使用UTF8MB4字符集来最大化兼容。
MySQL里的生僻字
去年生产遇到过一个问题,需要在MySQL里插入一个生僻字或是Emoji(Emoji 表情也是一种特殊的 Unicode 编码),但是应用却报了以下的错误
Cause: java.sql.SQLException: Incorrect string value: '\xF0\xA6\xAD\xB5' for column 'name' at row 1
在排查过程中发现,有相同遭遇的小伙伴还有不少,并且都曾利用一样的排查手段,找过DBA确认过字符集,但是依然没有及时解决,为此DBA还拉了专门的问题群。
为了减少此类问题发生,总结了一点排查的步骤,节约开发成本。
插入生僻字报错排查步骤
- 确认库、表、列字符集
因为MySQL的默认的utf8字符集,只支持存储3个字节,一旦为超过3字节的文字(通常为生僻字)必然会报错。
库、表、列的字符集查询:
# 库字符集查询
show variables like 'character_set_database';
# 表字符集查询
show create table 表名;
# 列字符集查询
show full columns from 表名 where field='列名';
当你通过以上方式确认库表列包括连接charset都是utf8mb4,但是问题还是没有解决,那还是需要从应用入手来排查问题。
- 确认应用还是数据库问题
在面对这类问题,你可以先尝试在数据库中手动insert一条含有生僻字的数据,查看mysql是否可以成功插入,如果可以,那必然是程序里操作存储时字符集出了问题——几位小伙伴就是这样的情况:明明手动插入没有问题啊,为什么代码插就不行?
不妨回想一下,应用程序连接MySQL的时需要啥?
- 确认驱动版本
是名为MySQL-connector的驱动程序。它为了支持JDBC,使Java可以使用标准的Api和标准的SQL操作数据库做了非常多的兼容性、事务、连接池相关的工作,并且很重要的是,他本身的更新迭代里也有包含了更多字符集的支持。
从官网找到其重要版本变更的说明(Java为例)
其中有展示,utf8mb4字符集的支持情况和mysql-connector-java的版本有关,当你使用了5.1.12版本以下的mysql-connector-java,就必须考虑升级了,而且即便是5.1.13 ~ 5.1.46版本,还依然需要设置对应的MySQL服务端配置才能使用。
所以检查mysql-connector-java版本号,将其修改为5.1.47及以上或者8.0.13及以上非常重要。而且值得注意的是,mysql-connector-java 5.1.47是在2018年8月发布的,8.0.13也是在2018年10月22日发布的,对于创建于2019年之前的应用,升级是必要的,往往也是最容易被忽视的。
这样的一道工序,在发布过程中,不值得设置一个Jar包卡点吗?
按照以上步骤排查完毕后,插入问题基本能解决了。
但是关于生僻字使用问题,你可能不一定能发现。
当你插入一条生僻字数据后,想要根据该字搜索对应的数据时,你会发现结果不是你想要的那样。
比如:
# 尝试搜索表里 word 字段为 𤋮 的数据
select word from des_sensitive_words where `word` = '𤋮'
结果却是
SQL条件失效了。想要的是“𤋮”,但是结果却展示了全部的生僻字,莫非查询也有字符集的设置?
没错,查询语句也是有字符集的,也是字符集转换一样的原因:查询语句的字符集和MySQL的字符集不一致,导致MySQL在执行语句时无法正确匹配。
我尝试在网络寻找修改不一致的方法,很遗憾并没有找到相对完备的答案。但是依靠先前对锟斤拷的经验,不妨做一个猜想:因为字符集不一致,查询时把“𤋮”转换为了类似于“�”这样的兜底值来查询,其余生僻字在存储时,虽然以utf8mb4存储,但是在检索时依然会转换为当前查询的字符集,在转换为当前查询的字符集后,它们都是统一的“�” ,这就导致它们也以这样的值被检索到。
其实想要知道当前语句的具体字符集,可以在MySQL执行如下命令
SHOW VARIABLES LIKE 'character_set_%';
在所展示的结果里,可以看到几个可能影响查询结果的字符集设置
character_set_client:客户端发送给 MySQL 服务器的字符集。
character_set_connection:连接层上客户端和服务器之间的字符集。该变量的值由 character_set_client 和 character_set_server 两者中的较小值决定。
character_set_results:MySQL 服务器向客户端返回数据的字符集。
character_set_database:当前连接所使用的默认数据库的字符集。
character_set_server:MySQL 服务器的默认字符集,决定了所有存储在数据库中的文本数据的编码方式。
如果character_set_server=utf8,character_set_client=utf8mb4, 那么很可能
的原因就是客户端和服务端的设置字符集并没有一致。
设置很多,查起来很麻烦,还要更改设置。我只想快速解决开发查询生僻字的需求——有没有MySQL自带的通用解决办法呢?
MySQL生僻字查询
MySQL存在一个关键字:BINARY,它的使用方式如下
select word from table_name where BINARY `word` = '𤋮'
在 MySQL 中,如果不使用 BINARY 运算符,就会按照字符集进行比较,一旦字符集不匹配,就会导致查询结果不准确,所以在一些特定场景下(比如剔除地址里的生僻字)会需要使用这种绝对值的查询方式。
BINARY关键字代表二进制运算符,它会把操作视为二进制的值来比较,对字符串以按位比较的方式来确认是否一致(区分大小写),这个方式就等于在查他们的Unicode,避免了字符集转换。
举个例子,“𤋮”这个字,它的 Unicode 码点为 U+2486E。在 UTF-8 编码中,“𤋮”的编码需要使用四个字节来表示,其编码为:\xf0\xa4\x8b\xae,我们按照MySQL提供的二进制码点转十六进制值方案:
-- 可以用此sql查询十六进制值
SELECT HEX(CONVERT('𤋮' USING utf8mb4)) as binary_hex;
在查询生僻字问题时,可以利用如上SQL将上述字段转换为F0A48BAE的16进制串来比较(二进制情况下由于字符集问题展示可能是 ???? )。
Java里的生僻字
和物流相关的部门经常会遇到收发地址包含各种生僻字的问题,快递承运商对生僻字的兼容能力参差不齐,顺丰的GIS可自动识别,京东就得下单失败了。于是乎,类似于 𤋮𠄘𦤎 这样的生僻字,就需要开发人员下单前提前检测做出处理,避免因需要修改地址等原因回流,浪费人力。
如何在Java代码里区分生僻字
Java 里使用UTF-16的编码方案,而UTF-16 编码是使用两个 16 位的字节表示一个字符。对于生僻字,如果其 Unicode 码值超出了 BMP(Basic Multilingual Plane)范围,也就是大于等于 0x10000,就需要用到代理对(Surrogate Pair)来表示这个字符。
代理对本质上是由两个 16 位的编码值组成,其中第一个编码值(高代理项High Surrogate)在范围 0xD800~0xDBFF,第二个编码值(低代理项 Low Surrogate)在范围 0xDC00-0xDFFF。两个编码值的计算方式如下:
- 将生僻字的 Unicode 码值减去 0x10000,得到一个 20 位的二进制数。
- 将得到的 20 位二进制数拆成两个部分,高 10 位为高代理项,低 10 位为低代理项。
- 对高代理项和低代理项各加上偏移值,得到最终的两个 16 位的编码值。
下面是一个示例,假设一个字Unicode 码点为 U+24B62,二进制码点为 0b10010 010111 0110010(20 位):
- 减去 0x10000,得到 0x4b62(这个值在二进制下是 10010 101101 01100010)。
- 拆分成两个部分,高 10 位为 0x12B(它对应的二进制为 0b1 00101 0110),低 10 位为 0x62(它对应的二进制为 0b01100010)。
- 对高代理项和低代理项各加上偏移值,得到最终的两个编码值为 0xD852 和 0xDF62。
对大部分人来说根本不想关心Java如何处理生僻字,只想知道判断它是或不是生僻字的方法,比如可以使用 Character 类提供的一些方法来判断一个字符是否为生僻字:
- isSupplementaryCodePoint(int codePoint):判断一个 Unicode 码点是否在 BMP 之外,也就是是否为生僻字,返回值为布尔型。
// 1.查询当前字符的码点,直接复制会变为对应的UTF16编码 \ud840\udd18
int codePoint = "𤋮".codePointAt(0);
// 2.判断生僻字的码点范围
if (Character.isSupplementaryCodePoint(codePoint)) {
System.out.println("是生僻字");
}
- isHighSurrogate(char ch) 和 isLowSurrogate(char ch):分别检查一个字符是否为高代理项和低代理项,返回值为布尔型。
// 判断该字符是否为高代理项或低代理项来判断生僻字
public static boolean isSquareChar(char c) {
return Character.isHighSurrogate(c) || Character.isLowSurrogate(c);
}
// 判断高低位代理项
char highSurrogate = '\uD852';
if (Character.isHighSurrogate(highSurrogate)) {
System.out.println("高位");
}
char lowSurrogate = '\uDF62';
if (Character.isLowSurrogate(lowSurrogate)) {
System.out.println("低位");
}
- charCount(int codePoint):返回给定 Unicode 码点所需的代理对数量,如果码点不是生僻字,则返回 1。
// 1.查询当前字符的码点,直接复制会变为对应的UTF16编码 \ud840\udd18
int codePoint = "𤋮".codePointAt(0);
// 2.判断对应返回的代理对数量,大于1则是生僻字
int surrogateCount = Character.charCount(codePoint);
以上这些方法都是现阶段的标准处理方式,看到了不妨就当作一种工具类收藏一下,某些情况你可能会用得到。
最后
由于中文里生僻字的数量众多,而且每个字的大小、形状、结构和含义都不相同,这给计算机字符集编码的收录、编排、溯源带来了极大的挑战。但是另一方面,保护我们的文化遗产这也是必不可少的一步,不可因少而废。
我们的文字表达历史悠久、文化底蕴深厚,字符集编码是我们用计算机这种新兴工具来传承和发展这一文化的必要途径。乱码也并不是计算机字符集编码本身的问题,而是因为编码不匹配、信息丢失和传递中断等各种原因导致的。因此了解计算机字符集编码是如何工作,以及如何选择和使用正确的字符集编码依旧非常重要,毕竟不是谁都能说得清这错综复杂的编码历史试了多少错。