西西河

主题:218-Dylan Beattie:论纯文本 -- 万年看客

共:💬6 🌺27 新:
全看分页树展 · 主题
家园 218-Dylan Beattie:论纯文本

https://www.youtube.com/watch?v=gd5uJ7Nlvvo&t=1425s

……纯文本,难道纯文本还值得讨论吗?是的。因为我们是程序员,我们的源代码就是纯文本。YAML文件也是纯文本, JSON文件也是纯文本,HTML文件,配置文件,XML文件,全都是纯文本。我们为客户构建的软件采用二进制格式,但是我们自己工作的时候只会将纯文本文件扔来抛去。你问一名程序员:“你喜欢纯文本吗?”他说:“是啊。”这个问题还有啥好说的?但是让计算机这么酷、这么有用、这么有趣且令人兴奋的原因在于我们可以将一种思想——一种概念,一个词语,一段视频,一段音乐等等——我们可以将这一切编码并且导入技术当中,然后技术就可以对其放手施为。或许技术可以将这段信息发送到澳大利亚;或许技术可以将其录制在光盘上,方便我们日后读取;或许技术可以将信息按照字母排序,或许技术可以寻找信息当中的模式与差异。同时在技术的另一端,其他人可以将这些信息读取出来,可以分享我们一开始输入的初始值。

人类最早发明书写是在五千年前的美索不达米亚地区,也就是现在的伊拉克。我们在伊拉克发现了写有文字的泥板,可以追溯到五千年前。我们大约也是在同一时期发现了电,存世的阿拉伯语文本告诉我们,古人意识到河里电鳗发出的电和天上的闪电或许颇有相通之处。但是直到一百五十年前情况才变得有趣起来,有人想到:“要不然我们将电与文字结合在一起看会怎怎么样?”于是他们就发明了全世界第一台电气化文本编码系统,也就是库克—惠斯通电报机。这套系统在十九世纪三十年代额英国首度得到应用。回头看看关于这套系统有很多莫名其妙地令人感到非常熟悉的特质。首先,这套系统由两个人共同发明,一位是威廉.库克(William Cooke),他是个生意人,想把这套系统卖给出价最高的买家。他坚持要求系统应当简单易用,不需要操作手册。另一位查尔斯.惠斯通(Charles Wheastone)是个科学家——如果你们有人用惠斯通电桥的话,这就是他的发明——惠斯通认为系统难用一点也无所谓,可以让用户阅读操作手册,但是他们应当将这套体系无偿捐赠出去,从而改善这世界。换句话说,早在1831年我们就面对着盈利对开源、可用性对用户手册之间的争执。最后双方妥协的结果就是这么一个东西:要想发送信息,你必须阅读手册,但是要想解读信息则不必。可以看到,仪表盘上有五根指针,换句话说这是一个五线电报系统。这套系统最早安装在伦敦西部,帕丁顿火车站装了一台,西德莱顿——也就是今天的希斯罗机场附近——装了一台,在两个站点之间拉起了一条20到22公里长的电线。为了发送信息,首先我们要闭合回路,换句话说也就是制造一道一来一回足有40公里长的电路。沿着电路发送电流就可以移动指针,两根指针方向相交之处的字母就是对方发送的字母。这套系统不能发送全套字母表,因为表盘上总共只有20个字母,因此在拼写的时候需要动一点心思。所以如果现在你因为ASCII、Unicode或者其他我们将要谈到的文本格式而挠头,请别忘了我们原本可能使用这么一套玩意,原本可能不得不使用一套五符号三进制编码体系,每一个字母都有五个符号来代表,这些符号又分成正、负与中立状态。所以我觉得我们多少躲过了一劫。

这套系统投入使用几周之后有一条电线失灵了。有人说:“要不然我们把电线修好吧。”其他人说:“不,倒也不必。四线体系也一样能用。”然后电线又坏了一根,他们又想:“要不然三线体系也可以试一试。”与此同时在大西洋的另一边有一位美国人名叫萨缪尔.摩斯,此人进行了我们所谓的蛙跳式创新,也就是忽视下一代技术,直接跳到更下一代技术。摩斯发明了单线电报体系,还发明了与这套体系相配套的编码系统。现在大家在画面上看到的并不是摩斯本人发明的摩斯码,他发明的是最初版本的摩斯玛。这套系统传播到欧洲之后,有一位德国工程师弗雷德里克.哥克(Friedrich Gerke)对其加以标准化,这才成了我们今天都熟悉的国际摩斯码。摩斯玛乍一看上去像是一套二进制编码体系,只有点与划组成,但实际上还要更复杂一些。除了点与划之外,摩斯码还有短、中、长暂停,所以实际上是一套基于时间的编码体系。1865年在巴黎召开了国际电信大会,将摩斯码列为了标准。设立了电报体系的欧洲各国一致同意“我们就要用这套标准化系统”。各位都是科技行业从业者,所以应该很清楚,如果没有人使用你的系统,那么你可以随心所欲地加以修改;一旦有了用户,你再想修改就没那么容易了,因为用户会不高兴。国际摩斯码体系得到了世界各地的广泛应用,从欧洲到亚洲到中东再到澳大利亚,想要改动这套体系非同小可,因此这套体系整整延续了差不多一百年。

直到二十世纪六十年代全世界才想到“或许我们应该换一套更好的体系”,因为这时候我们发明了电子计算机。我们为计算机设计的文本系统是ASCII,即美国信息交换标准代码。ASCII是一套非常成功的体系。今天我们在讨论纯文本文件时涉及的所有对象几乎都基于ASCII。但是我们要记得,ASCII刚刚问世的时候计算机还没有显示屏,只能使用电传打印机在纸带上打印输出结果。数据也不能存储在光盘上、磁带上或者固态硬盘上面,这些技术还要等好几年才能发明出来,当时的数据存储在打孔卡上面。我们今天看一看ASCII的设计可能会觉得非常不合理。首先来看看空字符NUL。在座有没有C++的开发人员?那你们肯定见过NUL。这个符号留存到了今天,用来表示一个字符串的结束。如果你一不小心越过了NUL,就可以黑入五角大楼搞天搞地。然后我们还有标题开始、标题结束、正文开始与正文结束,这一切都是组成字符的基本构成。这些其实都是针对机械电传打印机设计的控制符,它们的设计初衷是为了控制你的办公桌上这台实体机械的各种移动部件。假设你的电传打印机想要发送标题开始,你首先要按下控制键Ctrl,然后按下A,因为A是字母表的第一个字母。Ctrl加B则是正文开始。如果你想结束一段文本则要按下Ctrl加C。这套体系一直延续到了今天。就在昨天我还不得不按下Ctrl加C,因为我不小心写了一段无限循环的代码。因此电传打印机时代的遗迹直到今天还时常冒头。其他代码还有传输结束、请求与受到通知等等。如果你有一台Windows电脑,输入echo加空格加Ctrl加G,然后再按下回车键,计算机就会发出一阵铃声。因为这段代码的意思是“我想敲响我的电传打印机上的铃铛”。如果你的退格键坏了,可以按Ctrl加H,绝大多数命令行的控制码都一样。然后还有水平制表,换行,垂直制表,换页与回车。垂直制表非常有趣,因为这个代码意味着让电传打印机垂直打印,直到触碰下一个金属固定键位为止。所以如果有人还在争论水平制表与空格键的优劣,你不妨告诉他们你在编码时用垂直制表来缩进,保证让他们摸不着头脑。

我们之所以有这些代码,是因为机械电传打印机曾经有一个名为字车(carriage)的部件,或者说打印头。如果想要打印一行文字,就要将字车从纸张一边移动到另一边。然后你将字车归位——也就是回车——再将纸张向上移动一行,让字车再去打印下一行。实际上电传打印机的原理略微简单了一点。好比说你想打印黑体字,那你在打印完一行字之后将字车归位,但是却不移动纸张,而是将同样的字符在同样的位置再打印一遍。由于两遍打印的字体并不会完全重合,看上去就像粗体字差不多。所以说单纯的回车指令自有用处,而单纯的换行指令则没这么有用。只有在极少数情况下你才会只想换行而不想让字车归位。这一点留存到了今天。今天假如你的设备的操作系统是Unix的后代——MacOS,Linux,安卓——它们的换行指令是\n。因为这些系统是从Unix进化来的,而Unix又是从Multics系统进化而来。Multics是第一套拥有设备驱动的计算机系统,操作系统与桌面上的物理硬件之间首次出现了一个小小的软件。因此他们可以要求“如果看到新的一行,那就将字车归位,回到下一行的起始位置”。 Windows11从Windows10进化而来,而后者又从Windows7、5、XP、vista、3.1等等依次进化而来,这些系统又从MS-DOS进化而来,DOS又从CPM进化而来,而CPM的设计宗旨就是在极其廉价、没有设备驱动的微型计算机上运行。这些微软Windows系统的古早祖先必须在换行后面加一条回车,否则字车就会从托架上掉下来。

下一部分ASCII代码的设计非常高明。ASCII设计师们必须做出的决策之一是我们究竟需要哪些标点符号。如果有人在印刷排版行业工作过,就知道我们有负号,有连字符,有短破折号,有长破折号,如果你要印刷书籍或者科学论文,每一种横线都是单独一种字符。ASCII设计师们心想:“我们可没有足够空间来存储所有这些符号,而且它们看上去长得也差不多,我们就用负号充当连字符和短破折号好了。”但是在过去十几年,我们看到了数字印刷学的兴起,因为现在我们有了视网膜成像技术,人们开始在Kindle与iPad上读书。因此例如弯引号与破折号之类的标点变体也开始卷土重来。ASCII当中有一部分代码表示十进制数字。在计算机上我们最经常完成的工作就是将数字转换成字符串再转换回来。在ASCII系统当中我们要忽视字节的前四位比特0011,只读取后四位比特,这就是这段字符代表的数字。0000就是0,0001就是1,0010就是2,等等。即便在四比特处理器上这一转换过程也非常迅速。

你或许想过,为什么ASCII当中大写A的编号是65,而小写a的编号是97?为什么要挑选这两个数字来代表拉丁字母表的起始字母?因为这两个数字的二进制表达形式只差一个比特——65是1000001,97是1100001,所有大小写字母的区别都在于第6位比特。假如你想进行不区分大小写的字符串比较,只需要忽视第6位比特,然后对照剩下的比特的数字值是否一致。就这样我们收拾了许多标点,然后终于来到了第127号字符,也就是删除,代码是1111111。为什么要用这个数字指代删除?想象一下,你在打孔卡上存储数据,打了一排孔用来代表你的银行账户,然后你又想到“我要抹除我的银行账户”。你可以直接将打孔卡烧掉,也可以将卡上孔洞之间原本的空白处全都打上孔,使之无法读取——这是我之前的想法,然后我看到Youtube上有一条评论说:“delete从来都不是为打孔卡准备的。”我调查了一下,的确如此。如果用这种方式来处理打孔卡,只会得到所谓的蕾丝卡,在读卡器当中很容易碎裂并且卡住你的主机。Youtube是正确的,我说错了,Delete不是为打孔卡准备的,而是为打孔纸带准备的。如果大家看看画面上这台电传打字机的左手边,就能看到纸带打孔设备。

美国人当然洋洋自得:“得了。127个字符。别的什么都不需要。我们解决了文本编码问题。”结果世界各地的反应是:“啥?你这玩意儿我们可用不了,这里面就连我们的字母表都没有。”对于许多文化来说ASCII都根本没法用。ASCII无法表达中文,日文假名,日文汉字,韩文,泰文,越南语,阿拉伯语,西里尔字母,等等。世界上存在着很多延续上千年的文化,在他们看来ASCII就是小孩子闹着玩的玩意。对于另外很多文化来说,ASCII差一点就能用,还不算尽善尽美,但是问题也不大。我个人开始与纯文本打交道是1988年的事。我出生在津巴布韦,当地的通用语是英语,当地的通用货币是美元,所以我最早用我父亲的Amstrad 128电脑学编程的时候,ASCII非常有用,我熟悉的所有字符都存在。后来我搬到了英国,有一天在计算机上做作业,用的是一台286个人电脑,搭配的是爱普生点阵式打印机。我想打印英镑符号£,但是打印不出来,因为1965年美国人并不关心这个符号,所以这个符号并不是ASCII的一部分。ASCII问世以后,全世界许多用户与公司都发现ASCII只有7比特,而一个完整字节是8比特。当时还没有人主张要设立国际规范,因为没有国际规范,因此许多公司与个人都发现“我们可以利用剩下这个比特解决我们的问题”。这就出现了我们所谓的代码页。代码页是一套规则,用来规定表示ASCII的前4位比特在眼下这台计算机的这段程序或者这个文件当中究竟意味着什么。我之所以没法打印家庭作业,是因为我的打印机尽管距离我的电脑只有一米之隔,还有缆线相连,但是却使用另一套不同的代码页,与我用来写家庭作业的计算机说不上话。

最常见的一种代码页是画面上这张代码页437,这是IBM个人电脑的默认代码页。这张代码页的上半部分保留了普通的ASCII代码,然后在下半部分加入了北欧与西欧地区日常需要的几乎全部字符以及一整套制表符。然后他们又加入了希腊语字母表的一半内容,因为这些都是日常数学运算需要的符号。这些字母并不足以用来书写希腊语,但是确实足以进行最常见的数学公式运算。他们本来可以输入全套希腊语字母,但是他们说:“我们只输入物理课本上常见的字母,剩下的你们就自己想办法吧。”但是他们又指出:“我们现在已经不再用电传打字机了,或许最上面这一行已经用不着了,我们可以用这一行来编码笑脸或者音乐符号之类的东西。”你要是见过IBM的个人电脑严重死机的话——以前还挺常见的——那你就会看到屏幕上布满了笑脸与扑克牌,这是因为操作系统正在往外抛洒控制代码,告诉电传打印机:“快停下!不管你在做什么都快停下!”反映在屏幕上就是笑脸。

美国国家标准研究院也创建过自己的代码页用来书写西里尔字母,也就是俄语、乌克兰语、保加利亚语所用的字母。但是由于代码系统的一点历史问题,这套系统应用起来并不十分顺手。比方说要显示俄语的привет,根据代码页,这串字母转换成数字是191,224,216,210,213,226。但是很多系统会忽视第8位比特,因为ASCII只有7位。Word用第8位比特来检查你是否对单词进行了拼写检查,从而避免对整个文档进行拼写检查,他们用第8个比特来标记已进行过拼写检查。有些软件利用第8个比特来存档,有些电子邮件用第8位比特来进行奇偶校验。所以第8个比特会在传送当中丢失掉。假设你将这个词的每一个字母的第8位比特去掉,得到的就是63,96,88,82,85,98,再用ASCII转换回来就变成了?`XRUb。俄国人觉得这样肯定不行,我们得发明一套自己的系统。于是他们就创建了Код Обмена Информацией 8 бит,简称KOI8,或者说IBM878号代码页。他们说:“别管西里尔字母表了,我们要把与西里尔字母发音类似的英文字母利用起来,将这些字母填补在对应的字位,这样就算在转换过程当中出了问题,至少转换之后结果的发音和转换之前比较相似——于是привет就变成了pRIWET。

这方面有很多有趣的小故事。去年10月份我在英国进行这场演讲,演讲之后有人问我“你听没听说过哈利波特的故事?”原来早在2002年的时候,有一位法国女士结交了一位俄罗斯笔友,两人都是哈利波特粉丝,频繁邮件往来。俄国女士的名字叫苏维特拉娜,我们姑且将那位法国女士称作克劳黛特。克劳黛特询问斯维特拉娜:“你要不要我将最新一本哈利波特寄给你?因为新书在法国已经出版了。”苏维特拉娜回信说:“那太好了,我们俄国这边还没出版,请将新书寄到这个地址。地址是用西里尔字母写成,请小心誊抄。”然后她就写下了莫斯科街道上的一个地址。当然,克劳黛特接受这份邮件用的是win98系统,运行的是法语代码,结果显示了一堆乱码。然后她心想“好吧,看来他们在俄国就是这么写地址的。”这两套地址的每一个字母都对应相同的数字,只是采用这些数字的代码页并不相同。于是克劳黛特就将这段乱码一丝不苟的抄写到信封上,然后寄了出去。再然后俄国邮政系统的某人看到这段乱码,非常尽职尽责地又根据读音为每一个乱码字母标注了相应的西里尔字母,结果这本书还真就送到了。

请大家把手机拿出来,如果你们手机上装了Spotify、苹果音乐或者Google Play,请搜索一下屏幕上这个词:kohuept。你们找到了什么?比利.乔的1987年列宁格勒现场演唱会录音。比利.乔是第一个在苏联举办现场演唱会的西方艺术家,他在列宁格勒的现场演唱会录音也在苏联发行了,这是非常少见的情况。这张专辑的名称叫做《音乐会》(concert),在唱片封面上他们用俄语印上了concert这个词,也就是концерт。然后这张专辑又在美国发行,有人想“我们应当将这个名字输入我们的数据库”,于是就有了kohuept这个译名。再然后在线音乐问世了,Spotify和苹果音乐开始向各大唱片公司购买版权:“你们是不是有一个庞大的音乐数据库?”唱片公司说:“是的,这个数据库还是我们在1987年用Wordstar整理出来的。”结果这张专辑从此变成了Kohuept,这个奇怪的代码与翻译错误就这样保存了下来。

代码页提供了暂时的解决方案,但是并不理想。它们的设计思路很聪明。但是使用代码页不可能写出一封既包含希伯来语又包含阿拉伯语的邮件,不可能在同一份文本当中包含不同类型的字母,你往往会写出你无法打印的内容,你的打印机也可能打印出你无法上传的内容。随着互联网与电子邮件的发明,我们逐渐意识到代码页并不能解决文本问题。 Windows有15到20种不同的代码页,微软也有一套Windows不用的代码页,IBM还有一套不同于ASCII的代码系统,直到现在还偶尔得到使用,苹果也有自己的编码。甚至就连PostScript和DOS也有自己的编码页,总之就是一团糟。显然我们需要一套统一化的编码系统,于是就出现了统一码Unicode。编订工作始于1988年,统一码联盟在1991年成立。他们的任务宣言写的非常精彩:“以单一且一贯的方式,在一切计算机与设备上体现一切人类语言的所有字母与符号。”这项宣言可以分成三个部分。首先,如何包括“一切计算机与设备”?这就要求你提供的方案要比其他现有方案全都更好,而且应用起来必须非常容易。正是因为有了统一码,丹麦的计算机用户才能与挪威的计算机用户交流,英国的计算机用户才能与俄国的计算机用户说上话。这是一个非常高明的解决方案,而且他们执行得非常漂亮。

接下来是“以单一且一贯的方式”来表现每一种语言当中的每一个字母与符号,就连古埃及的象形文字与苏美尔语字母也必须包括进来,因为我们不想将研究历史语言与死语言的学术人士排除在外。那么究竟要怎样才能以单一且一贯的方式来体现每一个字母与符号?首先要定义怎样才算是一个字母。请看хор与xop,这两个组合是同样的字母吗?它们的发音一样吗?实际上前者出自俄语单词хороший(好的),后者出自英语单词exoplanet(外星球)。显然我们必须约定一个字母究竟意味着什么?我是说英语的英国人,我一说话就是“早上好,这里是伦敦,这里是女王,想不想喝茶?”这就是始终在我脑海里面播放的噪音。所以当我访问例如丹麦之类地方的时候。后来我去了丹麦,看到Smørepålæg这个单词。我心想“这个词究竟怎么发音?我也不认识,肯定都是些细枝末节,而且还有人一不小心把a和e挤在一起了,我先把这两个字母分开再说,就写成smorepalaeg吧。”所以说究竟怎么才算是相同的字母?这里的挪威语字母究竟是不是英文字母外加一点多余的装饰,还是说它们其实完全不一样?

我向大家介绍两位朋友,一位是法国的考古学家François Bordes,另一位是洛杉矶摇滚乐团的主唱Mötley Crüe。下面这句话显然是英语——François the archæologist went to the Mötley Crüe concert——但是这句话当中包含了好几个英语当中没有的字符。首先是带有下加变音符的c;其次是ae拼成的æ,这是丹麦语的字母,但是在英文当中不算字母;最后还有顶着重金属摇滚元音变音符的o与u。我原以为æ在英语当中不算字母,只能算连字,来自当年排版印刷时代遗留下来的传统,当年的印刷工人最早将两个铅字拼在了一起。然后Youtube上有人指出æ不算连字,我查了一下确实如此。我现在正在用一款带有连字的字体,其中ff,ti,ij都以连字形式展现。连字与非连字都是相同的字母,只不过印刷方式不一样而已。

再来见一见我的另一位朋友Magnus Mårtensson,他是微软公司瑞典分公司的一位高管,一年前他持有一本瑞士护照来到美国,护照上标注他的名字是Mårtensson。瑞典的拼写法要求如果名字里带å,那么护照底部必须用ASCII将你的名字再拼写一遍,将å替换成aa,只有这样才能让生产于二十世纪六十年代的老式美国计算机读取出来。然后他买了一张机票,这张机票并不是由瑞士方面提供的,于是他们就把小圆圈去掉了。结果他机票上的名字是Martensson,护照上的名字是Maartensson,而美国海关又素来以热情好客闻名,这样一来他难免要多回答几个问题才能通关。万幸的是Magnus是一名衣冠楚楚的欧洲白人,前来微软召开会议。如果换一个人的话,这个问题再加上美国边境控制机构的一贯作风,难免会导致非常严重的后果。而导致这一切的起因仅仅是因为人们对于怎样才算一个字母以及这个字母的代码是什么的看法不一致。

如果你觉得单独一个字母已经够难办了,那我们再来看看字母排序的问题。画面上是几张欧洲城市的清单:Berlin,Aachen,Zürich,Aarhus,Örebro。你们觉得画面上这几张清单当中哪一张是按照字母排序的?我个人显然支持第一张——Aachen,Aarhus,Berlin,Örebro,Zürich——因为A排在B前面,B排在O前面,O排在Z前面。但是我看现场观众也有人举手支持二号清单——Aachen,Aarhus,Berlin,Zürich,Örebro——三号清单——Aachen,Berlin,Zürich,Aarhus,Örebro——以及四号清单———Aachen,Berlin,Örebro,Zürich,Aarhus。大多数观众或许会奇怪后三张清单怎么就按照字母表排序了,那么我们不妨将所有的Ö都替换成Ø再来看看。我们往往觉得字母排序并不会引起多大争议,但是由于不同的文化会使用不同的字母表,所以这一点也同样难免争议。假如我们将这些地名输入微软 SQL服务器数据库来进行排序,结果微软给出的是三号排序。那么我们再来搜索一下:SELECT * FROM @cities WHERE Name = ‘orebro’,结果却啥都没有。或许这套搜索系统会区分大小写,那么我们搜索一下’Orebro’又怎么样?还是啥都没有。那么我们设定’Orebro’=’Örebro’行不行?还是不行。因为这套sql的默认设定认为O不能等于Ö,除非我要求系统使用不区分大小写、不区分轻重音的Latin1校对规则。这样系统才突然回过神来:“啊没错,orebro在这儿呢。”

再来看看地名排序。微软 SQL的默认排序就是一号清单。这样的排序对于我这样的英国英语使用者来说很合理,但是显然你们当中有很多人都觉得这样排序不合理。我们可以规定按照Latin1校对规则,结果不变,因为这本来就是默认设置。但是如果我们按照芬兰语与瑞典语的规则来进行校对,那就排出了二号清单的顺序,因为Ö排在Z后面,在芬兰瑞典语字母表里排到二十六七位。如果我们改用丹麦语或者挪威语的规则,还有更奇怪的排序——Berlin,Zürich,Örebro,Aachen,Aarhus。你肯定会想“这是个什么顺序?”却原来Aarhus的丹麦语拼法是 Århus。在1948年发生了所谓的丹麦拼写改革,当时二战刚刚结束,此前丹麦语的拼写方法与德语很相似,也采用同样的一套字母变体。出于不必细说的原因,二战之后很多国家都决定“我们不想看上去再和德国一样了,我们希望更像瑞典。”于是Æ、Ø与Å这三个字母在1948年加入了丹麦字母表。按照当时的做法,如果原词的拼写是Aa,那么今后就要改写成为Å。再然后到了2011年,Århus居民抱怨道:“这种做法当真影响到了我们在谷歌搜索页面的排名,对我们当地的旅游业造成了不好的影响,我们能改回来吗?”于是Århus又改回了Aarhus。但是这座城市依然排在整个字母表的最底部,因为它位于斯坎迪纳维亚半岛,所以理应放在最底部。而Aachen位于德国,所以可以留在排序顶部。

我曾经有一次看到某人运行挪威语版本的Windows,他有一个文本文件的文件名是aardvark,结果排在文件排序的最底部:我很奇怪:“为什么aardvark文件要排在最下面?”他说:“这是挪威语的字母排序。”我说:“但是aardvark是英语单词啊?”问题就出在这里,这个单词本身并不包含足够的信息让SQL server或者Windows来决定应该采用怎样的本地拼写惯例。所以假如你要构建一个在字母表排序方面遵照当地语言惯例的系统,除非黑进系统,在原本的排序栏后面另起一栏,用来解决问题。几周之前我在一个网站的下拉式清单里寻找英国,结果找不着。后来我才发现这个清单最早是按照法语字母排序,后来又被翻译成了英语。如果你有需要的话,必须自行添加足够的线索,从而让计算机能够给出人们希望看到的结果。

当你遇到ASCII控制码或者字母表排序惯例的时候,一开始看上去似乎完全不合理。要是你在你自己的计算机上干活,结果遇到这些奇怪的结果,你肯定会想“这是在干嘛?”但是你研究一下历史就会发现,自从我们发明了计算机技术以来,聪明人就一直在试图解决现实世界的问题。导致这些问题的因素包括人、文化、惯例、政治与社会学。你尽管可以说:“我是理工宅,完全不关心这些东西,我只想写代码而已。”但是如果你不了解相关文化背景,你就写不了代码,因为我们日常使用的技术恰恰正是为了解决由这些因素产生的问题而研发出来的。不存在基于政治不可知论的技术。就算只要理解字母表排序这样的小事,首先也要知道二战及其带来的后果。

回头看看ç,统一码不会告诉任何系统应当怎样表现这个字母,只会设法按照我们历来的做法以一贯的编码方式来表现这个字母。如果你是法国人的话,那么ç就不过是你键盘上的一个键而已,我们可以给它一个码位——U+00C7。如果你是英国人的话,首先要打一个C——U+0043,然后再画一条小尾巴——U+0327。统一码允许我们往现成的字母上面添加附件,这样得出的结果就是所谓的组合字符。组合字符的最大好处就是可以随意拼接。在某些设备上拼接过度会让设备死机,比方说安卓手机。在另一些设备上你会得到通常称作Zalgo文本的乱码。如果你想用正则表达来解析HTML就会得到Zalgo文本。

再回头看看我们的老朋友Mötley Crüe。这里的上两点是所谓的重金属元音变音符,美国的重金属摇滚乐手喜欢在自己名字的元音字母上面加两点,因为他们觉得看上去很酷……Mötley Crüe里的ö与ü可以是单独字符,也可以是组合字符,这两种表示方式都正确,但是它们一样吗? 统一码表示我们不会替你做决定,我们只会将工具提供给你,你自己要决定采用哪种表现方式。 统一码提供了四种规范化字符串的方式。规则一名叫等价分解(Canonical Decomposition),简称NFD,也就是将一条字符串分割成为数量最大的码位。假设你要写一个有小尾巴的C,那你就先将其分解成C与一个附加部分。规则二名叫等价合成(Canonical Composition),简称NFC,这种方法是将一个C与一个附加部分拼接起来,占据单独一个码位。规则三名叫兼容分解(Kompatibility Decomposition),简称NFKD——这里之所以用K而不是C来代表兼容,是因为C在之前已经用来表示等价了。这意味着两个字符的码位未必一致,但是意义或许一样。规则四是兼容合成(Kompatibility Composition),简称NFKC。

我们可以在.NET上面截取一段代码:

class Program { /l This code at https :// bit.ly /3kgNAlE

static NormalizationForm[ ] forms = new[]{

NormalizationForm.FormC,NormalizationForm.FormD,NormalizationForm.Formkc,NormalizationForm.FormKD

};

static void Compare(string s1, string s2) {

Console.writeLine($" s1=s2 : {s1 =s2}");foreach (var form in forms) {

var result = s1.Normalize( form)= s2.Normalize( form);Console.writeLine($""{form} : {result}" );

static void Main(string[] args) { …

这就是在C#当中进行正规化的方式。现在我们要将上述四种规则依次过一遍,并且根据相应的正规化方法进行字符串比较,看看最后能够得到怎样的结果。比方说Mötley Crüe里的ö,既可以是单一字符,也可以是o加上组合用分音符U+0308。两种方式构成的字符串在二进制层面并不等同。如果我们问计算机这两者是不是一回事,计算机肯定会回答不是。但是所有四种规范化方式都认为它们是一回事。统一码也支持花体字母,例如ⓟⓛⓐⓘⓝ ⓣⓔⓧⓣ与plain text这两种拼法是同样的字符串吗?我们还是要用比较代码来显示一下。这两条字符串长度一致,但是除非我们指定用兼容正规形来比较,否则根据前两条规则并不等同。

D: \Projects\PlainText> dotnet run

String s1: ‘ⓟⓛⓐⓘⓝ ⓣⓔⓧⓣ’(length: 10)

String s2: ‘Plain Text’ (length: 10)

s1 == s2: False

FormC: False

FormD:False

FormKC:True

FormKD:True

我个人关于统一码以及字符代码的经历可以追溯到六年前的一个工作日。那天早上我刚一到公司就有同事告诉我:“我觉得咱们公司昨天晚上被人黑了。”这种事在科技公司非常常见。“我们被黑了!”不你没有。你就是不小心右击了一下查看源码。放心,脸书并没有被你玩坏。你的文件都放在桌面上了,不过被那个视窗挡住了而已。”但是也有些时候,业务能力非常强的安全工程师告诉你“我觉得我们被黑了”,那你就必须要问一句:“你为什么觉得我们被被黑了?”他说:“因为我们的事件日志里边出现了中文字符。”我看了一下事件日志,果然里边有中文,我们是一家位于伦敦的公司,绝大部分员工都是说英语的英国人,向主要说英语的英国以及销客户提供英语服务。我们公司的程序员里没有华裔,我们也没有运行过依赖中文的程序,我们的系统并不支持中文。那么这些中文是怎么来的?于是我就按照标准程序处理:“你先查看防火墙日志,看看有没有证据表明有人试图侵入我们的系统;你去检查数据库,看看有没有包含中文的表格,看看是不是注入攻击;你去告诉商务部门的同事,我们正在调查潜在的数据侵入事故,十五分钟之后再向他们同胞最新进展;我身为公司老板先给自己泡杯咖啡,然后赶紧发推特。”这确实是一记妙招,因为还没等他们搞明白怎么回事,推特网友就为我提供了正确答案。在推特上有人回复:“这看上去像是统一码的映射错误。”接下来有一位大牛Fake “Unicode”也回复道:“看来后四位比特都成了空值,所以大概是UTF-16LE被错误地当成了UTF-16BE(或者反过来?)”我心想:“是啊,我大概应该先去查一下这句话究竟说了啥。”于是我花了九十分钟时间恶补统一码,总算搞明白了咋回事。

假设你在微软Windows系统上运行JavaScript或者Java或者其他大多数主流操作系统,然后输入Delete一词,每一个字母都默认用16比特来代表——D/0044,e/0065,l/006C,e/0065,t/0074,e/0065——因为这样比较快。内存相对比较廉价,知道了字符串的长度之后我们就可以说:“如果字符串当中包含这么多字符,那么字节的数量就是字符数的两倍。”这样一来内存管理操作就快多了。顺便说一句,刚才这句话95%正确,还存在一些个案与例外,但是总体来说 Windows、JavaScript和Java用16比特来代表字符串当中的每一个字符。如果将每一个字母的16位比特的前后八位调转一下,就变成了4400,6500,6C00,6500,7400,6500,转化成中文就是“䐀攀氀攀晗攀”。如果你把这些字符发到YouTube上,还会有热心观众告诉你这几个汉字都是是什么意思,第一个汉字是“切割下来的牲畜肢体”,第二个汉字是“抓住东西向上爬”。第三个汉字是“一类纺织物”,第四个汉字是“放在尸体口中的陪葬用玉制品”。我的反应是:“这要么是一封非常瘆人的勒索信,要不然就是出现了技术问题。”从Delete到事件日志里的䐀攀氀攀晗攀,SQL语句的顺序遭到了颠倒。那么接下来的问题就是为什么会颠倒?原来我们设立了一个虚拟网络,里面有若干个数据库和几个网络服务器。其中的一个网络交换机会每过三分钟漏掉一个字节,而且不会告诉任何人,然后所有数据都会向末端移动一个比特的位置,于是上一个字符的后半截与下一个字符的前半截就形成了新字符。

通宝推:sywyang,史文恭,桥上,胡一刀,
全看分页树展 · 主题


有趣有益,互惠互利;开阔视野,博采众长。
虚拟的网络,真实的人。天南地北客,相逢皆朋友

Copyright © cchere 西西河