程序员用分号结束一行代码,隐藏诗人用句号结束一句诗。代码但有些符号从不抛头露面,符号它们藏在字符编码表深处,隐藏像墙角里的代码蚂蚁,安静地搬运着信息。符号
我第一次注意到这个问题,隐藏是代码在调试一段JSON数据的时候。接口返回的符号字符串看起来完全正常,但解析死活报错。隐藏折腾了半小时,代码把字符串逐字节打印出来,符号才发现里面混了一个U+200B——零宽空格。隐藏屏幕上什么都看不见,代码光标移过去也不占位置。符号它就那么蹲在两个单词之间,像一个不买票的乘客。
这类字符有个统称:不可见字符。零宽空格、零宽非连接符、零宽连接符、字节顺序标记、软连字符……它们原本都有正经用途。零宽连接符在阿拉伯文和印度语系里控制字母的连写形态,软连字符告诉排版引擎“这个词如果换行可以在这里断开”。问题是,它们太容易被滥用了。
有人在爬虫抓取的文本里故意插入零宽字符,让复制粘贴出来的内容带上隐形水印。你从某个网站复制一段文字发到别处,那段文字里可能藏着你的用户ID。原理不复杂:把用户标识编码成零宽字符的二进制序列,穿插进正文。肉眼完全看不出来,但程序一读就能还原。零宽空格代表0,零宽非连接符代表1,一段两百字的文本能藏几十个字节的信息。够用了。
这不是什么新鲜技术。2016年就有安全研究者演示过用零宽字符在Twitter上传递隐藏消息。后来有人做了在线工具,输入一段话和一个暗号,生成一段看起来一模一样但暗藏玄机的文本。你甚至可以把整个Base64编码的压缩包塞进一篇微博里。
更日常的场景是代码里的坑。从网页复制一段示例代码到编辑器,编译报错,盯着屏幕看半天,每个字符都对。最后发现行首有个U+FEFF——BOM头。它本来是UTF-8编码用来标识字节序的,但很多编辑器不显示它。一个BOM就能让Shell脚本第一行`#!/bin/bash`失效,因为内核看到的是`\xEF\xBB\xBF#!`,不认识。
我后来养成了一个习惯:处理任何外部输入的文本,先跑一遍清洗。用正则把所有`\u200b-\u200f`、`\u2028-\u202f`、`\ufeff`范围内的字符过滤掉。看起来有点偏执,但省下来的调试时间值这个价。
这些符号的存在提醒我一件事:我们看到的文本和机器读到的文本,从来就不是同一个东西。屏幕是机器好心翻译给我们看的,底层是一串数字。数字和数字之间,还挤着很多不打算让人看见的房客。
它们不说话,但它们一直在。