表情符号的字数计算方式 - 别在 SNS 字数限制上吃亏
本文大约需要 5 分钟阅读。
\u201C咦,只放了 5 个表情符号就用掉 30 个字了?\u201D这种违和感背后,有 Unicode 结构性的原因。表情符号看起来是 1 个字,但在系统内部由 2 到 10 个以上的码位组成是常态。 各平台\u201C什么算 1 个字\u201D的规则也不同,同一个表情符号在 Twitter 算 2 个字, 在别的系统可能算 1 个字。本文解析其中的机制和实用的应对方法。
所谓\u201C字\u201D到底是什么
在讨论表情符号的字数之前,需要了解计算机上的\u201C字\u201D有多个层次。
- 码位(Code Point):Unicode 分配的一个编号。例:\u201Cあ\u201D是 U+3042,1 个码位。
- 码元(Code Unit):字符编码处理的最小单位。UTF-16 中为 16 位(2 字节)。
- 字素簇(Grapheme Cluster):人类认为是\u201C1 个字\u201D的单位。可能由多个码位组成。
表情符号的字数问题,就是因为这三个层次不一致而产生的。 不同平台用哪个层次来计数,各有不同。
代理对 - 表情符号变成 2 个字的原因
Unicode 的码位从 U+0000 到 U+10FFFF,表示它们需要 21 位。 然而 JavaScript 的字符串、Java、C# 等许多语言内部采用的 UTF-16, 基本上是用 16 位(1 个码元)来表示字符的设计。
16 位只能表示到 U+FFFF,超过这个范围的码位(大部分表情符号都属于此) 需要用 2 个码元组合的\u201C代理对\u201D来表示。 也就是说,用 "🌸".length 来计数,JavaScript 会返回 2。
"🌸".length // → 2 (代理对)
"あ".length // → 1 (BMP 范围内)
[..."🌸"].length // → 1 (按码位分解)这个差异就是 Web 表单字数验证中\u201C加了表情符号就超限\u201D现象的元凶。 简单用 .length 实现的 UI,实质上是在不利于表情符号用户。
ZWJ 序列 - 一个表情符号占 5-10 个码位
家庭表情符号和职业表情符号,是将多个表情符号用零宽连接符(U+200D)连接起来显示为一个表情符号的机制。
- 👨👩👧 = 👨 + ZWJ + 👩 + ZWJ + 👧 = 5 个码位
- 👨🏽🚀 = 👨 + 肤色 + ZWJ + 🚀 = 4 个码位
- 🏳️🌈 = 旗帜 + 异体字选择器 + ZWJ + 彩虹 = 4 个码位
用 UTF-16 计数的话,家庭表情符号可能超过 11 个码元。 看起来是 1 个字,码位是 5 个,UTF-16 码元是 10 个,字素簇是 1 个。 按哪个粒度计数,对字数限制的影响完全不同。
肤色修饰符和异体字选择器
👋🏽 这样带肤色的手势,由基础表情符号 + 肤色修饰符的 2 个码位组成。 此外,还有在后面跟随强制表情符号显示的异体字选择器(U+FE0F)的模式。
- 👋(素手)= 1 个码位
- 👋🏽(带肤色)= 2 个码位
- ❤️(红心)= 2 个码位(♥ + U+FE0F)
❤️\u201C一个心占 2 个字\u201D的原因就是异体字选择器。 为了与文本显示的 ♥ 区分,附带了一个要求以表情符号形式显示的不可见控制码。
主要平台的计数逻辑
X(原 Twitter)- 加权计数
X 从 2018 年开始采用\u201C加权字符计数\u201D。 ASCII 字符和部分货币符号算 1 个字,其余(包括中文和表情符号)算 2 个字。 中文用户实际只能写 140 个字而非 280 个字,就是这个机制的原因。
表情符号采用\u201C看起来 1 个\u201D算 2 个字的独特实现,即使是 ZWJ 序列,只要看起来是 1 个就算 2 个字。 特点是不按代理对单位,而是按字素簇单位来赋予权重。
Instagram - 图片说明和简介行为不同
Instagram 的图片说明(2200 字)比较宽容,表情符号看起来 1 个基本算 1 个字。 而简介(150 字)更严格,ZWJ 序列或特殊表情符号会导致计数膨胀。 在简介设计中使用 ZWJ 系表情符号时感觉\u201C字数不够\u201D就是这个原因。
LINE - 消息宽容,状态严格
LINE 消息正文的上限(10000 字)实际上不需要担心表情符号数量。 但个人资料的状态消息(500 字)采用接近 UTF-16 码元单位的计数, 大量使用家庭表情符号会比预想消耗更多配额。
Discord - 比较宽松
Discord 内部采用接近码位单位的计数, 表情符号 1 个基本算 1 个字。这就是简介字数限制感觉比较宽松的原因。
独到见解 - 为什么各平台的计数方式不同
平台的计数逻辑是\u201C用户是否能直觉地接受\u201D与 \u201C内部实现的历史遗留\u201D之间博弈的结果。
按字素簇单位的精确计数需要 Unicode 标准库(ICU)或 grapheme-splitter 这样的 专用库。越是历史悠久的服务,内部处理越是基于 UTF-16 构建的, 后来要改成精确的字素单位计数成本很高。结果作为\u201C现实的妥协\u201D, UTF-16 码元或独特的加权方式就保留了下来。
从用户角度看,\u201C同一个表情符号在不同地方算不同字数\u201D令人费解, 但如果理解为各平台内部情况的体现,就能看清为什么会这样。
实用的应对方法
1. 重要发布前先确认计数
对于简介或一条帖子定胜负的图片说明,最可靠的方法是实际粘贴到平台的表单中查看剩余字数显示。 外部字数计数器大多按码位单位计算,可能与平台实际计数不一致。
2. 装饰系表情符号避免 ZWJ
在表情符号艺术或个人资料装饰中,选择简单表情符号比 ZWJ 序列更能节省字数。 🌸 ☕ 📚 ✨ 这样的单码位表情符号作为主角,可以在保持视觉华丽的同时提高字数效率。
3. 用 ♥ 代替 ❤️
在确实想节省 1 个字的场景中,可以选择文本心形 ♥(1 个码位)而非 ❤️(表情符号 2 个码位)。 不过根据平台可能会变成黑白显示,需要权衡外观。
4. 自己实现时使用库
如果在自己的服务中实现文本表单,按字素簇单位计数是最诚实的做法。 JavaScript 中可以使用 Intl.Segmenter(现代浏览器标准)或 grapheme-splitter 库。 不让表情符号用户处于不利地位的设计,正在成为现代 Web 服务的基本礼仪。
总结
表情符号的字数取决于按码位、码元还是字素簇哪个层次来计算。 了解各平台的逻辑,重要发布时在实机上确认剩余量是最可靠的。 如果想在表情符号艺术上下功夫,避免 ZWJ、活用单码位表情符号可以提高字数效率。
EmoArt 的探索页面公开了大量由简单单一表情符号和装饰字符组合而成的 combo。 对于想在控制字数的同时打造令人印象深刻的个人资料的人应该会有帮助。