1. 5秒结论:看起来是一只兔子,内部其实是三个码点叠起来
主角是这个:
ꪔ̤̮
它看起来像一只小兔子正盯着你,但 Unicode 并没有把它编码成“兔子颜文字”。底层是 U+AA94 TAI VIET LETTER LOW TO,后面跟着 U+0324 COMBINING DIAERESIS BELOW 和 U+032E COMBINING BREVE BELOW。[1][2]
也就是说,屏幕上是“小动物”,数据里却是“傣越文字母 + 下方组合附加符号 + 下方组合附加符号”。
长得可爱,户口本很复杂。
2. 30秒概要:扔进“草里”之后,它真的开始栖息了
最开始只是一个装饰颜文字:
- ̗̀ ꪔ̤̥ꪔ̤̮ꪔ̤̫ ̖́-
把它放进缅甸文字里:
မြန်မာမြန်မာမြန်မာ ꪔ̤̮ မြန်မာမြန်မာ
对于不识这种文字的人来说,圆润而密集的字形可能像一片草丛,兔子脸就像从里面探头。
再放进泰文:
ภาษาไทยꪔ̤̮ภาษาไทย
第一反应往往变成:“等一下,里面是不是有只兔子?”
于是当然要放十只。
ภาษาไทยꪔ̤̮ภาษาꪔ̤̥ไทยภาษาꪔ̤̫ไทยꪔ̤̮กำลังꪔ̤̥เขียนอยู่ꪔ̤̫ภาษาไทยꪔ̤̮ภาษาꪔ̤̥ไทยภาษาꪔ̤̫ไทยꪔ̤̮
到这里已经不是阅读,而是野生动物数量调查。
3. “兔子”到底是什么
| 外观 | 码点 |
|---|---|
ꪔ̤̥ |
U+AA94 + U+0324 + U+0325 |
ꪔ̤̮ |
U+AA94 + U+0324 + U+032E |
ꪔ̤̫ |
U+AA94 + U+0324 + U+032B |
U+AA94 是真实存在的 Tai Viet(傣越文)辅音字母。[1] 下面叠加的是各自独立的 Unicode 组合附加符号。[2]
所以不是 Unicode 官方偷偷设计了兔子,而是人的视觉系统从字符组合里认出了一张脸。
颜文字文化顺手就把它收编了。
4. 为什么泰文特别像高强度伪装
这并不是说泰文字“奇怪”。这是不会读泰文的人在看字形时产生的视觉玩笑。
泰文有不少符号会出现在基字的上方或下方,而且普通泰文并不像英语那样在每一个单词之间都插入空格。Unicode 的换行规范也把泰文列为需要语言相关上下文分析来判断断行机会的复杂文字之一。[4]
这时再塞进一个自身就在基字下方叠了两个组合符号的 ꪔ̤̮。
会泰文的人当然能看出这里混进了别的文字;不会的人却容易把整行看成连续的曲线和上下标记,于是兔子脸获得了意外的迷彩。
ภาษาไทยꪔ̤̮ภาษาไทย
视觉翻译就是:草、草、草……嗯?刚才是不是跟什么东西对上眼了?
5. 缅甸文、高棉文、马拉雅拉姆文、僧伽罗文也能参加
缅甸文:
မြန်မာမြန်မာ ꪔ̤̮ မြန်မာမြန်မာ
高棉文:
ខ្មែរខ្មែរអក្សរ ꪔ̤̮ ខ្មែរខ្មែរ
马拉雅拉姆文:
മലയാളം മലയാളം ꪔ̤̮ മലയാളം
僧伽罗文:
සිංහල භාෂාව ꪔ̤̮ සිංහල
泰文:
ภาษาไทยกำลังเขียนอยู่ ꪔ̤̮ ภาษาไทย
所谓“谁最强”完全是主观玩笑。这些都是正常、完整、真实使用的文字系统,不是背景花纹。笑点来自“不识字的人会把陌生字形先当成纹理”,然后兔子脸恰好混进去。
泰文只是这个无聊项目里的强力种子选手。
6. 经过翻译后真的出现了异常空格,但不能直接把锅扣给 Unicode 规范化
十只兔子版本经过某种翻译流程后,曾观察到类似这样的异常显示:
ภาษาไทยꪔ̤̮ภาษาꪔ̤̥ไทยภ าษาꪔ̤̫ไทยꪔ̤̮กำลังꪔ̤̥เข ียนอยู่...
说“这是 Unicode 边界案例”没有问题,但说“Unicode 规范化自动插入了空格”就证据不足。
Unicode 规范化主要定义等价字符串如何分解、按规范顺序排列以及重新组合。[5] 翻译产品在此之外还可能执行句子切分、词边界识别、分词、未知字符处理、存储转换、字体回退、布局和复制粘贴等操作。
泰文和缅甸文的分割本来就比按空格切英语更依赖上下文。[4] 再混入“另一个文字体系的基字 + 多个组合附加符号”,很适合把整条文本处理链逼到墙角。
要定位真正的变化点,需要逐层比较输入、存储、API请求、翻译结果、响应解析和最终渲染时的确切码点序列。
兔子很可爱,故障分析一点都不可爱。
7. 屏幕上的“一个字”不一定等于一个 Unicode 码点
Unicode 文本处理中有 grapheme cluster(字素簇) 的概念,用来近似用户感知的一个字符单位。[3]
一个带重音的字母,可能是单个预组字符,也可能是“基础字母 + 一个或多个组合符号”。画面上都是一个字,内部却可能完全不同。
这只兔子也一样:
- 视觉上:1只兔子
- 内部:3个码点
- 字符串长度:取决于编程语言和API怎么算
- 光标移动:理想情况下按用户感知单位移动
- Backspace:不同实现可能表现不同
- 搜索与比较:会受到规范化策略影响
- 字体:字体回退和组合符号定位可能改变外观
原来野生动物登记系统是按码点记账的。
8. 玩笑归玩笑,它其实是不错的 Unicode 测试字符串
ꪔ̤̮ 可以顺手测试:
- UTF-8 往返是否保真
- 复制粘贴会不会丢组合符号
- 长度计算是否区分码点和字素簇
- 光标与删除行为
- NFC/NFD 比较策略
- 字体回退和附加符号位置
- 与泰文、缅甸文、高棉文混排时的换行
- 翻译 API 往返
- 搜索索引
- 不同浏览器和手机的渲染
但不要无理由把它撒进生产内容。把这类极端字符串放进明确的测试夹具,并记录预期码点序列更安全。
不要往生产数据库里放生野兔。
9. 多语言网站的实际对策
- 统一使用 UTF-8。
- 明确规范化策略。Web 内容常用 NFC,但也要定义何时必须保持作者原始文本。[5]
- 区分字节、代码单元、码点和字素簇。
- 正确设置
lang,测试对应字体和行高。 - 对泰文、缅甸文、高棉文等不能靠简单空格切分的文字做真机测试。[4]
- 翻译导入时不要静默改写作者原文。
- QA 不只检查乱码,还要检查附加符号位置、换行、选择、复制、搜索和可访问性。
- 出现异常时,比较每一层的码点序列,而不是一句“Unicode有毒”就结束。
Unicode 多半只是按规范办事。真正危险的是软件偷偷假设“肉眼一个字 = 内部一个简单单位”。
兔子就是从这个假设的缝里钻出来的。
10. 结论:草丛里真正藏着的是人类的模式识别能力和 Unicode 的深度
故事从一个可爱的 Simeji 风格颜文字开始。
ꪔ̤̮
放进缅甸文,它像藏在草里;放进泰文,它像本来就住在那里;放十只,变成种群;再送进翻译流程,直接变成 Unicode 调试课。
笑话底下其实有几条很正经的结论:
- 视觉上的一个字符可以由多个码点组成。
- 组合附加符号会依附基础字符渲染。
- 某些文字需要语言相关的分割处理。
- 规范化、分割、换行、字体、翻译和渲染是不同层。
- 一张奇怪截图无法证明是哪一层犯错。
而且只要你把下面这一行看成过一次“草里有兔子”:
ภาษาไทยꪔ̤̮ภาษาไทย
以后就很难恢复正常了。
本来是来学 Unicode 的,最后做完了兔子栖息地确认。
