聊聊Unicode和emoji
这两天,在和一个老友研究一个和emoji相关的技术问题。
早上又刚好看到一个google的invitation,在Gmail页面(macOS下的Safari浏览器)和iOS下的Gmail APP里,显示的效果不一样,甚至在同一个Safari浏览器下,Gmail页面和Google calendar页面的效果也不一样。如图:

这就激起了我的好奇心,想深入分析了一下Unicode和emoji的一些细节。正式开始前,先叠个甲:这里面的技术细节水非常深,我今天的“初探”难免会有错误和不完整的地方,欢迎留言讨论交流。
一、从“字符”到 Unicode:码点与编码
原始意义的字符,应该是“指代特定含义的一个图像符号”,但在计算机里,字符并不是“图像”,而是一个抽象的编号(码点,code point)。而 Unicode 作为最常见的编号方式,为所有常见文字与符号分配唯一的码点,范围是从U+0000到U+10FFFF(一共21个bits),例如:
A→U+0041中→U+4E2D🥶(cold face)→U+1F976
但这个码点说白了只是一个数字;要落到字节序列(便于计算机处理、保存和传输),还需选择一种编码(encoding),常见的编码方式有:
- UTF‑8:变长(1–4 个字节),ASCII 区域兼容 7 位 ASCII;覆盖全部 1,112,064 个有效码点。是今天 Web 的事实标准。
- UTF‑16:变长(1 或 2 个 16‑bit 单元;超出 BMP 的码点用代理对表示),Windows API 与许多移动平台内部大量使用。
二、emoji 也是 Unicode 字符
很多人以为 emoji 是个图片(虽然有时候真的是个图片,后续会讲);实际上它们同样是Unicode字符(或字符序列)。显示时,浏览器/操作系统通过文本去选择字体并渲染。- Apple 平台:Apple Color Emoji 字体(PS:这个字体超大,有180MB,在
/System/Library/Fonts/Apple Color Emoji.ttc) - Android/ChromeOS:Noto Color Emoji
- Windows:Segoe UI Emoji
更细一点,从彩色字体格式看,各家也走了不同路线,有的是位图派的,有的是矢量派的,还有一些是混合的,这个不详细展开。
三、Web 上的显示方案
其实,文章开头的问题,本来到此就可以结案了的,但细想一下,不对啊,Safari和iOS APP都是苹果生态,为啥显示还是不一样呢?这就要说到以 twemoji 为代表的前端库,这类库希望统一emoji在不同系统下的显示效果。
于是就想到用脚本把字符替换成图片/SVG,代表是 Twitter(X)开源的 twemoji。它会扫描 DOM,把文本节点里的 emoji 替换为相应的<img> 或 <svg>。
以下是一个示例:
|
1 2 3 4 5 |
<script src="https://unpkg.com/twemoji@latest/dist/twemoji.min.js" crossorigin="anonymous"></script> <script> // 把页面中的 emoji 替换为 SVG twemoji.parse(document.body, { folder: 'svg', ext: '.svg' }); </script> |
PS:这里有个工具,可以搜索emoji,然后查看emoji在各平台下的样子。
四、变体选择符
变体选择符(Variation Selector)其实是一系列的Unicode字符的统称,一共有16个,从 VS1(U+FE00)一直到 VS16(U+FE0F),和emoji相关的主要是 VS15(U+FE0E)和 VS16(U+FE0F)(其他部分VS还和汉字的变体有关系,也非常有趣,有兴趣的朋友可以自行研究下)。
还是通过几个例子来解释吧:
U+2699⚙ vsU+2699 U+FE0F⚙️U+2708✈ vsU+2708 U+FE0F✈️U+2764♥ vsU+2764 U+FE0F❤️
U+FE0F)作为一个字符的“后缀”加上以后,可以把一个文本字符“修饰”成emoji。而VS15是“指定为文本字符”的,由于通常这就是默认值,所以一般不用特殊指定。
这里还要注意,也不是每个emoji都是通过VS16修饰来的,有的emoji并没有对应的文本字符,比如🔥,这类就不需要VS16了。
在计算字符串长度的时候,也需要注意两种emoji的区别,还是上🌰吧:|
1 2 3 4 5 6 7 8 9 |
Python 3.13.7 (main, Aug 14 2025, 11:12:11) [Clang 17.0.0 (clang-1700.0.13.3)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> len('⚙') 1 >>> len('⚙️') 2 >>> len('🔥') 1 >>> |
五、ZWJ 连接符
你以为 VS16 就够魔性了么?不不不,还有更厉害的:两个或者多个emoji,可以通过一个特殊的码点(ZWJ,Zero Width Joiner,U+200D)连接起来,产生“复合 emoji”。
当平台支持的时候,显示为单个图像;不支持时,ZWJ 会被忽略,显示为分开的若干 emoji——这是规范推荐的回退行为,算是向后兼容了。
ZWJ的连接,可以分为几类,咱们分布用例子来说明,注意,我尽量挑支持度比较广的例子,但也不能保证每个人看到的效果,这个得视系统和平台而定:
五.1、普通连接
- ❤️ + 🔥 = ❤️🔥(燃烧的心,
U+2764 U+FE0F U+200D U+1F525)
- 🏴 + ☠️ = 🏴☠️(海盗旗,
U+1F3F4 U+200D U+2620 U+FE0F)
- 🧑 + 🦽 = 🧑🦽(使用轮椅的人,
U+1F9D1 U+200D U+1F9BD)
要注意的是:ZWJ的前后,必须都是emoji,比如第一个,用 ♥+🔥就无法得到❤️🔥,因为♥属于文本字符,必须先用VS16修饰以后,才能用ZWJ连接(下同)。从此也能看出,VS16的“运算优先级”比ZWJ高。
五.2、性别修饰
这里包括了性别+职业、性别+活动等,如:
- 👩 + 💻 = 👩💻(女程序员,
U+1F469 U+200D U+1F4BB)
- 👨 + 🚀 = 👨🚀(男宇航员,
U+1F468 U+200D U+1F680)
- 🏃 + ♂️ = 🏃♂️(男性跑步者,
U+1F3C3 U+200D U+2642 U+FE0F)
- 🚴 + ♀️ = 🚴♀️(骑自行车的女子,
U+1F6B4 U+200D U+2640 U+FE0F)
这一类,也能扩展出很多。
五.3、肤色修饰
这大概是因为要政治正确整出来的幺蛾子吧!
U+2764 U+FE0F U+200D U+1F525)U+1F3F4 U+200D U+2620 U+FE0F)U+1F9D1 U+200D U+1F9BD)- 👩 + 💻 = 👩💻(女程序员,
U+1F469 U+200D U+1F4BB) - 👨 + 🚀 = 👨🚀(男宇航员,
U+1F468 U+200D U+1F680) - 🏃 + ♂️ = 🏃♂️(男性跑步者,
U+1F3C3 U+200D U+2642 U+FE0F) - 🚴 + ♀️ = 🚴♀️(骑自行车的女子,
U+1F6B4 U+200D U+2640 U+FE0F)
五.3、肤色修饰 这大概是因为要政治正确整出来的幺蛾子吧!
肤色修饰符(Emoji Modifier Fitzpatrick)有5个,分别是从🏻 U+1F3FB 到🏿 U+1F3FF,对应着从浅到深的5个肤色。
- 👍 + 🏽 = 👍🏽(中肤色点赞,
U+1F44D U+1F3FD) 这个不需要ZWJ,直接修饰 - 🙌 + 🏿 = 🙌🏿(深肤色举手,
U+1F64C U+1F3FF) 这个不需要ZWJ,直接修饰 - 👩 + 🏿 = 👩🏿(深肤色女性,
U+1F469 U+1F3FF) 这个不需要ZWJ,直接修饰 - 🤝🏻 + 🤝🏿 = 🤝🏻🤝🏿(浅肤色 + 深肤色握手,
U+1F91D U+1F3FB U+200D U+1F91D U+1F3FF) ZWJ出现了 - 👩🏻 + ❤️ + 👨🏿 = 👩🏻❤️👨🏿(浅肤色女性 + 深肤色男性情侣,
U+1F469 U+1F3FB U+200D U+2764 U+FE0F U+200D U+1F468 U+1F3FF) ZWJ出现了两次,也就是“连加”操作 - 👨🏽 + ❤️ + 💋 + 👨🏼 = 👨🏽❤️💋👨🏼(男同接吻(中肤 + 浅肤),
U+1F468 U+1F3FD U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468 U+1F3FC) ZWJ出现了三次,加出一个脏东西
五.4、家庭修饰
这部分,很多系统的支持都还挺有限的,所以大家看到的不一定是对的,就自行脑补吧。
- 👨 + 👩 + 👦 = 👨👩👦(父母加儿子,
U+1F468 U+200D U+1F469 U+200D U+1F466)
- 👨 + 👩 + 👧 = 👨👩👧(父母加女儿,
U+1F468 U+200D U+1F469 U+200D U+1F467)
- 👨 + 👩 + 👧 + 👦 = 👨👩👧👦(儿女双全的普通家庭,
U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) ZWJ三连加
- 👩 + 👩 + 👧 + 👦 = 👩👩👧👦(儿女双全的母母不正常家庭,
U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) 也是ZWJ三连加
- 👩 + 👦 = 👩👦(单亲母子家庭,
U+1F469 U+200D U+1F466)
类似这样,各种组合都有,可花了~
六、后记
至此,我了解到的东西基本上讲完了,后续还有好玩的我可能会继续补充。
U+1F468 U+200D U+1F469 U+200D U+1F466)U+1F468 U+200D U+1F469 U+200D U+1F467)U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) ZWJ三连加U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466) 也是ZWJ三连加U+1F469 U+200D U+1F466)我写这篇文章的时候,突然发现,我的Wordpress数据库编码还是用的utf8mb3,导致本文里的emoji都没法正常显示,于是不得不中断写作,去升级数据库,哈哈!其实,关于数据库的utf8mb3和utf8mb4,也是一个挺有趣的话题,本文鉴于篇幅,就不展开了吧!
我想说的是,在自己的平台上写内容,确实会遇到各种问题,但自己去解决也未尝不是一种乐趣。毕竟,在大部分平台上写文章,很难插入一个“可交互的表格”,对吧?
全文完。
发表回复