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