拷斤锟背后的字符编码问题
一个有趣的现象
笔者就职于某一游戏公司。总所周知内网的测试服务器总是有形形色色的人,用形形色色的软件修改配置,此非人力规章所能约束。有的人使用vscode
远程打开,有的人呢则远程上去用notepad++
打开,于是就喜闻乐见得经常看见GBK
的配置文件里中文注释出现大量的拷斤锟
。不由让人想要探索一番现象背后的本质。
字符集与字符编码本质
众所周知,计算机只能以二进制的形式存储、传输任何数据。因此无论是中文、英文抑或是标点符号,只要你希望它以字符的方式被显示出来,就必须对它进行编码。换言之必须给他们一个编号。例如以 1
表示 大
,以 2
表示 小
,则 12212
则被标识为 大小小大小
。
有聪明的读者可能分析一个问题:随着序号的增长,如果出现一个为12
的标记表示 猪
,那 12212
的解析就会出现歧义,可以为 大小小大小
,也可以是 猪小大小
,因此当我们需要存储时,需要将 1
拓展至 01
, 2
拓展到 02
,这样就不会出现歧义。
此时有学习过信息论的朋友,敏锐地发现:以上文所说的这种方式存储,信息熵其实未到极限。我们可以采用哈夫曼编码的方式,进一步缩短平均码长。具体来说,可以将所有的词以出现的频率拼接在一棵树上,然后以树的左枝为0
,右枝为1
这种方式得出一个词的编码。
这种编码的方式很理想,但是现实中我们往往面临两个问题:
- 计算机内访存往往以
8bit
的方式进行,将字符压缩进8bit
以内意义不大。而将字符长度处理为固定的,则对于使用者来说意义巨大。 - 编码一旦被指定就要考虑向后兼容,难以根据词频时时更改,而且不同的领域、使用场景,词语的频率天差地别。例如在绝大多数的情况下,哈夫曼编码这个词都位于不存在的世界的另一端。但在信息论的学习者中,这个词语还是个常用词。就很难取舍设计问题。当然设计多套字符集也是非常糟糕的。有经常使用中日韩台的软件的朋友应该深有感触。
下面我们考虑一种理想的设计:
我们给字符进行编号时,不考虑字符的使用频率、优先级,仅仅给他们进行编号。当我们存储的时候则将编号进行优化,来表示字符。前者被我们称为字符集,后者则被我们称为字符编码。当系统收到以一定的字符编码编码的数据时,会以对应的规则,将它解析为对应的字符集序号,再根据这个序号,来读取对应的字体以显示(当然,程序员会根据对应的情况进行对应的优化,创造出更省力的映射关系)。
设计字符编码是一个典型的信息与编码和计算机工程。不仅仅要考虑如何最优的进行编码也要考虑计算机是以8位为基础单位存储信息的。因此在8bit
以内的优化都不用考虑,以 8bit
为一个小单位,仅需考虑多个单元之间的优化。
考虑到一个前提,“使用某种语言某个字符的人,往往会使用这种语言的别的符号“。因此设计时,最好将统一语言的不同字符相邻编号,然后将相邻的编号相邻存储。
ASCII、UNICODE、UTF-8、GB2312、GBK
现实中字符编码与存储、通信设计也基本满足上述原则,但是也有各自的局限性和妥协。
在早期,计算机仅在美国小范围使用,因此在设计上,仅考虑52个英文大小写+数字+常见的符号+一些命令行控制符。共有128个(7bit
),最前面填上一个0
,凑够8bit
。
随着计算机的流行,这种方式逐渐暴露出了一些问题。例如难以表达一些非拉丁的字符,例如全世界的大多数文字。因此各个国家都开始自己研发一些能够兼容ASCII,但是互相冲突的编码方式。例如BIG5、GB2312。因为ASCII的首位比特为0,因此这些编码方式普遍采用16bit
存储,将首位比特恒定为1。这样同时兼通ASCII并拥有2^15
大的空间。但是也引入了一些缺点。
- 缺乏快速定位首位的能力,在一定情况下会产生歧义,不便于软件取数
- 彼此互相冲突,有时要求对应的硬件、软件
- 一些落后的国家设计出的编码方式和字符集并不合理
为了通用地表示所有的语言,UNICODE
横空出世,它分为16bit
版与32bit
扩展版,对世界上所有的语言的所有字符进行编码,同时预留出一些位置给自定义空间()。因为这是一种字符集,而非存储方式,因此也非常方便各个国家节选出子集进行修改。conlang
爱好者福音
针对于UNICODE
,如果采用定宽方式存储则会造成大量的信息浪费。因为绝大多数存储的信息都是8bit
或者16bit
足以表示的。因此后面推出了UTF-8
、UTF-16
,这两种编码的意思是最低为8bit
、16bit
的UNICODE
表示法。它支持bom
机制,可以在字符串最前面写入一些元信息来让软件正确的理解字符串的编码方式。它支持一定的容错机制,也类似于哈夫曼编码在编码上进行无歧义表达。
但是这种方式也引入了一些问题
UNICODE
的编码前后顺序可能未必合理,例如一些小众语言,它们的编号都极为靠后,可能都需要24bit
乃至更长位宽的数据来表示一个字符,这对于这些语言的常用国家未必公平。但是也不能因此对UTF-8
过分苛责。因为如果引入通用表达不同字符集(例如以不同的优先度表示UNICODE
的不同码段,而非按顺序,这样小众的语言也可以在16bit
内表示,而在这些小众语言中极为罕见的字符,例如在乌克兰的中文符号,则采用24、32bit
表述)的能力,会导致UTF-8
的机制极为复杂,也影响了用户设计对应的软硬件。同时从全局的角度上来看,这种”更优“的UTF
编码其实降低的信息熵极为有限,因为这些小众语言的使用者极为少、大多数主流语言都被16bit
囊括,而由于计算机系统的设计,在16bit
的前半段金额后半段,对于表述的信息熵没有任何影响。同时,引入码段优先度选择功能,可能需要占用巨量的空间做冗余。而对于有些使用者提出的所谓语言霸权的问题,更是无稽之谈,因为在设计的时候,UNICODE
本身会均衡地放置单个语言的各种字符。例如中文中的大大多数常用字都在16bit
段,但是异体字、甲骨文等则分布在别的段。- 为了一些容错、无歧义的机制,它的信息熵也未达到表达极限。
- 很多人对于
UNICODE
的制定标准是有意义的,毕竟众口难调。
因此有些国家制定的新标准会采用一些设计:
- 采用类
UTF
设计,但是缩减字符集,来让本国的语言能更短表达位宽 - 减少一些
UTF-8
的多功能冗余,增加表达能力 - 制定一种映射关系,将
UNICODE
中的不同本国语言段映射到某一种字符码上,再将这种字符码以UTF
方式表述,这种方式改动较小,只需引入一个新的解析层
众所周知的GBK
,它是GB2312
的扩展版。增加了一些少见的字符。它对所有的汉字进行编码,并直接将这些编码进行输出。需要注意的是GBK
是一种理论方案,它的实际实现为GB18030
。
此外虽然那些被GBK
拓展进的词都为UNICODE
中有但是GB2312
中没有的,但是它完全没有对UNICODE
兼容,以一种扭曲的映射关系进行简单映射。考虑到当时所有的UNICODE
中文都能在16bit
表示、GBK
的提出也事实上远晚于UTF-8
,因为时间较早,历史包袱也不大。字符集事实上也参考了UNICODE
的词库。完全可以以映射的方式,最大程度上兼容UNICODE
基础设备与GB2312
,也可以利用UTF-8
的一些容错机制,做一些错误兼容,让别的系统识别为别的编码而非乱码。也没有吸收UTF-8
的一些先进设计,只是粗暴的对GB2312
进行拓展,大量冗余都被浪费了。缺乏有效的元信息,导致编辑器除非默认识别GBK
,不然难以简单的方式识别这些字符串是GBK
的。在一些编程接口上(例如python
),如果不做额外处理,不仅仅会导致乱码,有时也会崩溃。
因此,GBK
以笔者之见是一个在技术上比较失败的方案,当时也有很多政治上的考虑类似于汉芯,此处就不展开了。也许可以省一点空间,但是转换的坑太多了。
回归正题
出于一些技术上的考虑,UTF-8
在如今,已经被绝大多数标志、环境指定为强制使用。也给一些常用别的编码方式带来了一些问题,例如开头的拷斤锟
。
当一个文件以GBK
存储,此时使用UTF-8
编码打开,因为UTF-8
完全不兼容于GBK
,因此会在转换层显示为UNICODE
特殊字符 �(0xFFFD
) ,一般情况下,这会被显示为一个特殊的问号。此时如果对编辑器进行存储操作,会将转换层的 0xFFFD
落盘。而 FFFD
的UTF-8
表示恰好是0xEF 0xBF 0xBD
,其在GBK
上恰好为锟
(0xEFBF
),斤
(0xBDEF
),拷
(0xBFBD
)。下一次我们再用GBK
编码打开时就显示为拷斤锟拷斤锟拷斤锟拷斤锟
。
笔者的一些看法
总的来说还是希望开发者、开源组织更多的放弃门户之见,拥抱国际标准,也希望国际上的主流开发环境能对小众的编码更为友好。例如不要将0xFFFD
直接落盘顶掉原信息,也就是不将不认识的字符串,直接识别为没有信息熵的纯乱码。
当时更加希望大家看见乱码型问号不要随便保存啦。