表情符号的组合改变含义 - 1 个字符与 2 个以上字符传递的信息量差异
当你从手机的表情键盘选择"👨👩👧👦"时,你以为自己输入了 1 个表情符号。然而这个家庭表情的真面目是"👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦"这 7 个码位的连接。外观是 1 个字符,内部是 7 个。在表情符号的世界里,组合方式不同,1 个字符的含义和字符数都会发生巨大变化。
ZWJ 序列 - 用看不见的胶水合并表情符号
ZWJ(Zero Width Joiner)是分配给 Unicode 码位 U+200D 的"零宽度连接符"。它在屏幕上不显示任何内容,但起着将前后表情符号粘合为一体的胶水作用。
机制很简单。将表情 A + ZWJ + 表情 B 排列在一起,如果操作系统或应用有对应这个组合的字形(渲染图像),A 和 B 就会合并显示为一个表情符号。如果没有对应的字形,A 和 B 就原样并排显示。也就是说,ZWJ 序列是一种"能合就合,不能合就照原样"的灵活机制。
| ZWJ 序列 | 组成要素 | 码位数 | 显示 |
|---|---|---|---|
| 👨👩👧👦 | 👨 + ZWJ + 👩 + ZWJ + 👧 + ZWJ + 👦 | 7 | 家庭(父母女儿儿子) |
| 👩💻 | 👩 + ZWJ + 💻 | 3 | 女性技术人员 |
| 🏳️🌈 | 🏳️ + ZWJ + 🌈 | 4 | 彩虹旗 |
| 👨🍳 | 👨 + ZWJ + 🍳 | 3 | 男性厨师 |
| 🧑🚀 | 🧑 + ZWJ + 🚀 | 3 | 宇航员 |
| ❤️🔥 | ❤️ + ZWJ + 🔥 | 4 | 燃烧的心 |
| 👩❤️👨 | 👩 + ZWJ + ❤️ + ZWJ + 👨 | 5 | 情侣 |
家庭表情是 ZWJ 序列中码位数最多的类别之一。4 人家庭的"👨👩👧👦"有 7 个码位,用 UTF-8 编码后达 25 字节。仅仅 1 个表情符号就消耗了相当于 25 个英文字母的数据量。
ZWJ 序列有趣的地方在于,理论上可以尝试连接任意两个表情符号。输入"🐱 + ZWJ + 🐉"(猫和龙)这样未定义的组合也不会报错。由于没有对应的字形,猫和龙只是并排显示而已。Unicode 联盟官方定义的 ZWJ 序列约有 600 种,但厂商有时也会自行添加支持。
国旗表情 - 2 个地区指示符绘制 1 面旗帜
🇯🇵(日本国旗)看起来是 1 个表情符号,实际上是"地区指示符号字母 J"(U+1F1EF)和"地区指示符号字母 P"(U+1F1F5)这 2 个字符的组合。用专用的 Unicode 字符表示 ISO 3166-1 alpha-2 国家代码"JP"。
| 国旗 | 国家代码 | 地区指示符 | 码位 |
|---|---|---|---|
| 🇯🇵 | JP | 🇯 + 🇵 | U+1F1EF U+1F1F5 |
| 🇺🇸 | US | 🇺 + 🇸 | U+1F1FA U+1F1F8 |
| 🇬🇧 | GB | 🇬 + 🇧 | U+1F1EC U+1F1E7 |
| 🇫🇷 | FR | 🇫 + 🇷 | U+1F1EB U+1F1F7 |
| 🇧🇷 | BR | 🇧 + 🇷 | U+1F1E7 U+1F1F7 |
| 🇰🇷 | KR | 🇰 + 🇷 | U+1F1F0 U+1F1F7 |
地区指示符号从 A 到 Z 共 26 个,理论上可以有 26 × 26 = 676 种组合。但实际显示为国旗的只有 ISO 3166-1 中注册的约 250 个国家和地区代码。未注册的组合(如"🇽🇽")在不同平台上可能显示为"XX"文本或空白。
这种设计蕴含着政治考量。Unicode 联盟为了避免"哪些地区算作国家"这一政治判断,没有直接定义国旗表情,而是委托给了现有的国际标准 ISO 3166-1。如果新国家独立并在 ISO 3166-1 中注册,无需修改 Unicode 规范就能自动使用该国旗表情。
正如URL 字符数限制中提到的,国家代码在互联网的各种场景中都有使用。ccTLD(国家代码顶级域名)的".jp"也基于同一个 ISO 3166-1 国家代码。
肤色修饰符 - 1 个表情变成 5 种颜色的机制
2015 年 Unicode 8.0 引入的肤色修饰符(Emoji Modifier)用于改变人物表情的肤色。基于皮肤科使用的 Fitzpatrick 量表,提供了 5 个等级的修饰符。
| 修饰符 | 码位 | Fitzpatrick 分类 | 示例(👋 + 修饰符) |
|---|---|---|---|
| 🏻 | U+1F3FB | Type I-II(浅肤色) | 👋🏻 |
| 🏼 | U+1F3FC | Type III(较浅肤色) | 👋🏼 |
| 🏽 | U+1F3FD | Type IV(中等肤色) | 👋🏽 |
| 🏾 | U+1F3FE | Type V(较深肤色) | 👋🏾 |
| 🏿 | U+1F3FF | Type VI(深肤色) | 👋🏿 |
添加肤色修饰符后,1 个表情变成 2 个码位。"👋"(U+1F44B)是 1 个码位,而"👋🏽"是"U+1F44B U+1F3FD"的 2 个码位。UTF-8 下为 4 字节 + 4 字节 = 8 字节。仅指定肤色就使数据量翻倍。
当 ZWJ 序列与肤色修饰符组合时,码位数会爆炸式增长。例如肤色不同的情侣表情"👩🏻❤️👨🏿"由 👩 + 🏻 + ZWJ + ❤️ + VS16 + ZWJ + 👨 + 🏿 组成,达 8 个码位。外观是 1 个表情,内部数据量却与英文"Hi there!"(9 个字符)几乎相同。
表情俚语 - 组合产生的隐语世界
表情符号不仅有官方含义,在用户社区中也被当作独特的俚语使用。单独看无害的表情符号,组合后可能产生完全不同的含义。
最著名的大概是 🍑🍆 的组合。桃子和茄子这两个食物表情,在社交媒体上被广泛认知为性暗示。2019 年 Instagram 限制了包含这一组合的帖子的搜索结果。
| 表情组合 | 字符数 | 官方含义 | 俚语含义 |
|---|---|---|---|
| 🍑🍆 | 2 字符 | 桃子和茄子 | 性暗示 |
| 🧢 | 1 字符 | 棒球帽 | 谎言(cap = 说谎) |
| 💀 | 1 字符 | 骷髅 | 笑死了 |
| 🐐 | 1 字符 | 山羊 | GOAT(Greatest Of All Time) |
| 👁️👄👁️ | 3 字符 | 眼睛和嘴巴 | 震惊、困惑的表情 |
| 🫠 | 1 字符 | 融化的脸 | 尴尬、害羞 |
| 🤡 | 1 字符 | 小丑 | 做了蠢事的人 |
"👁️👄👁️"是用 3 个表情排列成脸的"表情艺术":眼 + 嘴 + 眼,表达"难以言喻的表情"。这个 3 字符组合从 2020 年左右开始在 TikTok 上流行,用于表达震惊、困惑或"看到了不该看的东西"的意味。
表情俚语因世代和地区而异。在日本,🙏 用于表示"拜托"或"谢谢",而在欧美有时被解读为"击掌"。正如表情符号的 Unicode 与字符数计算中所述,表情符号的技术字符数与人类感受到的"含义量"完全是不同维度的事情。
各 SNS 平台的表情字符计数方式差异
表情符号的字符数计算在不同平台间差异很大。同一个表情发布在 Twitter(X)和 Instagram 上,消耗的字符数不同。
| 平台 | 表情计数方式 | 👨👩👧👦 的计数 | 🇯🇵 的计数 |
|---|---|---|---|
| Twitter(X) | NFC 规范化后的字符数 | 1 字符(= 消耗 2 字符) | 1 字符(= 消耗 2 字符) |
| UTF-16 代码单元数 | 11 字符 | 4 字符 | |
| LINE | 独自计数 | 1 字符 | 1 字符 |
| SMS | UCS-2(16 位) | 7 字符 | 2 字符 |
| JavaScript | UTF-16 代码单元 | .length = 11 | .length = 4 |
Twitter(X)比较宽容,ZWJ 序列的家庭表情和国旗表情都按视觉上的 1 个表情处理(但内部按 2 字符计算权重)。正如Twitter 的字符数限制中详细介绍的,在 280 字符限制内,每个表情按 2 字符计算。
而 JavaScript 的 .length 属性返回 UTF-16 代码单元数,因此包含代理对的表情会返回比视觉字符数更大的值。家庭表情"👨👩👧👦"的 .length 为 11。要获得准确的字符数,可以使用 Array.from(str).length 或 [...str].length,但这些方法也会分解 ZWJ 序列返回 7。要按字素簇(grapheme cluster)计数,需要使用 Intl.Segmenter API。
也可以参考SNS 字符数限制汇总。了解各平台的计数方式差异,可以减少发布大量表情时遇到"字符数超限"的困扰。
表情接龙和电影名猜谜 - 游戏中的字符数
用表情符号玩的游戏从字符数角度来看也有有趣的发现。
"表情接龙"是用表情符号的名称玩日语接龙游戏。🍎(苹果)→ 🦍(大猩猩)→ 🍛(拉面)……以此类推。规则简单,但不知道表情的正式名称就意外地难。例如 🫥 的正式名称是"Dotted Line Face"(虚线脸)。约 3,600 个表情符号全部在 Unicode 的 CLDR(Common Locale Data Repository)中定义了各语言名称。
"用表情猜电影名"也很受欢迎。例如"🦁👑"是"狮子王"(2 个字符表达 3 个字的标题),"👻👻👻🔫"是"捉鬼敢死队"(4 个字符表达 5 个字的标题),"🧙♂️💍🌋"是"指环王"(3 个字符表达 3 个字的标题)。表情符号的组合可以说是一种大幅压缩字符数同时传递含义的"超压缩语言"。
在编程中获取表情的"真实字符数"
对开发者来说,表情符号的字符数计算是个头疼的问题,因为不同语言和运行时返回的值各不相同。
| 语言 / 环境 | 方法 | "👨👩👧👦"的结果 | 计数单位 |
|---|---|---|---|
| JavaScript | "👨👩👧👦".length | 11 | UTF-16 代码单元 |
| JavaScript | [..."👨👩👧👦"].length | 7 | 码位 |
| Python 3 | len("👨👩👧👦") | 7 | 码位 |
| Swift | "👨👩👧👦".count | 1 | 字素簇 |
| Rust | "👨👩👧👦".len() | 25 | 字节(UTF-8) |
| Go | len("👨👩👧👦") | 25 | 字节(UTF-8) |
只有 Swift 返回"1",因为 Swift 采用字素簇(grapheme cluster)作为字符单位。这是最接近人类直觉的结果,但内部处理成本较高。在 JavaScript 中要获得相同结果,需要使用 Intl.Segmenter。
正如Unicode 基础知识中所述,"字符数"的定义因上下文而异。表情符号的组合将这个问题展现得最为鲜明。与全角和半角的字符数计算差异一样,请记住表情的计数方式也因平台和语言而异。
表情符号的未来 - 无限的组合可能性
截至 Unicode 16.0(2024 年),表情符号总数约为 3,790 个。但加上 ZWJ 序列和肤色修饰符的组合,可表达的变体达数万种。
2024 年引入了"方向修饰符",可以改变人物表情的朝向。给 🏃(跑步的人)加上方向修饰符变成 🏃➡️(向右跑的人)。这也是增加码位数的因素之一。
表情符号的组合大幅提升了文本通信的表达力。日常聊天中不需要在意 1 个表情内部由多少个码位构成。但在有字符数限制的社交媒体发帖、数据库字符数设计、编程中的字符串处理时,"视觉字符数"与"内部字符数"的差距可能成为意想不到的陷阱。
正如LINE 消息字符数一文中提到的,大量使用表情的消息比纯文本消息数据量更大。下次选择表情时,不妨想象一下那 1 个字符背后隐藏着多少个码位。
如果对表情符号和 Unicode 的机制产生了兴趣,可以在 Amazon 上查找相关书籍。