算法与数据结构 / regex · Unicode 字符模型与一个自研引擎 / RGI:国旗、子地区旗与肤色序列 待审核 13 / 36
RGI · Flag / Tag / Modifier

RGI:国旗、子地区旗与肤色序列

并非所有理论上合法的码点组合都能在各平台正常显示,Unicode 为此圈出一个子集:RGI(Recommended for General Interchange),标准认可、主流平台通常都支持、可在系统之间可靠传输。Basic_Emoji 那一档是 RGI 的一部分,本页拆开它之上的三类多码点序列,最后回到日常业务真正该用的那一个属性。

1 · 国旗:两个区域指示符

U+1F1E6U+1F1FF 这 26 个码点叫区域指示符,与字母 A–Z 一一对应。两个连写即构成一面国旗,字母对取自 ISO 3166-1 的双字母地区码。26 × 26 = 676 种组合里,\p{RGI_Emoji_Flag_Sequence} 只认 259 种;ZZ 这类未分配的组合落空,AADD 这些同字母对也全不成立。每个区域指示符都是补充平面字符,各占一个 UTF-16 代理对,所以一面国旗在 JS 里 .length === 4

图 1-1 · 两个区域指示符拼一面国旗,同时给出 code point 与 UTF-16 码元两层拆解,并实测该序列是否属 RGI 国旗。可换字母对或点预设。

警示 · 序列合法与屏幕上画出旗子是两件事。\p{RGI_Emoji_Flag_Sequence} 只回答这两个区域指示符是不是一面 RGI 国旗,至于渲染成什么由字体与系统决定:中国大陆地区的 iOS 与部分 Android 不把 TWU+1F1F9 + U+1F1FC)渲染成旗子,只回退成两个字母方块,而正则那一侧照样判它合法。判断本机能否画出某面旗,见 emoji 渲染检测

2 · 子地区旗:黑旗加 tag 标签

国旗只能表达国家一级的地区码,苏格兰、英格兰这类子地区另有一套办法:拿黑旗 U+1F3F4 当基底,后面跟一串 tag 字符,末尾用 U+E007F(cancel tag)收尾。tag 字符 U+E0020U+E007E 与 ASCII 可打印字符一一对应,于是不必新增码点就能把地区码「写」在旗上。gbsct 拼出苏格兰旗,全序列共七个 code point。

这套机制看着可以无限扩展,实际收得极紧:把所有双字母地区码配上 1 到 3 位的字母数字子码穷举一遍,\p{RGI_Emoji_Tag_Sequence} 命中的只有 gbenggbsctgbwls 三条,与上游 emoji-sequences.txt 里的三行完全对上。想给别的地区造一面旗,正则这一关就过不去。

图 2-1 · 三面子地区旗的逐码点拆解:黑旗基底、tag 字符对应的 ASCII 串、cancel tag 收尾,并实测是否属 RGI tag 序列。

3 · 肤色:底字符加修饰符

U+1F3FBU+1F3FF 是五个肤色修饰符,接在支持修饰的底字符后面即构成一个肤色序列。底字符限于人、身体部位与人类动作一类,Emoji_Modifier_Base 共 134 个,给 😀🐕 接肤色不成立,因为它们不在其中。而 \p{RGI_Emoji_Modifier_Sequence} 只收 665 条,除以 5 档肤色是 133 个底字符——比属性少一个。逐条比对后落单的是 U+1F46A(👪,一家三口):它带 Emoji_Modifier_Base=Yes,却没有任何一条 RGI 修饰序列,👪 加肤色实测既非修饰序列也不是 RGI_Emoji。属性留着、序列不收,UTS #51 未给出理由。顺着实测再看一层:家庭类整体都不收肤色,写成 👪 或写成 🧑‍🧑‍🧒 这种各人分开的 ZWJ 家庭,一旦带上肤色都不是 RGI;而同样是多人的牵手 🧑🏻‍🤝‍🧑🏻、职业类 🧑🏻‍🚀 却收。

修饰符在多码点序列里的位置容易看错:肤色紧跟在被修饰的那个人后面,而不是缀在整串末尾。🧑‍🚀 加中等肤色写作 🧑 + 肤色 + ZWJ + 🚀,实测它不属于 RGI_Emoji_Modifier_Sequence(那个集合只收「底字符 + 肤色」两码点的形态),却仍是合法的 RGI_Emoji,因为它被归到了 ZWJ 序列一类。旧版本页把这种情形报成「该基底不支持肤色修饰符」,是把两个集合的边界读错了。

图 3-1 · 底字符与五档肤色的组合,逐码点拆解并分别实测修饰符序列与 RGI_Emoji 两个属性。可换底字符或肤色档位。

4 · 整体匹配

日常业务不必逐类去认。\p{RGI_Emoji} 匹配一个完整的 RGI emoji,上面各类序列它全收:单码点、加 VS16、按键、国旗、tag 旗、肤色、ZWJ 序列。它是字符串属性,一次匹配的结果可能横跨好几个码点,因此只能用在 v 模式里,u 模式会直接报错。

数据文件那边可以把这个集合点清:emoji-sequences.txt 展开后有 Basic_Emoji 1400 条、按键 12 条、国旗 259 条、tag 旗 3 条、肤色 665 条,emoji-zwj-sequences.txt 另有 ZWJ 序列 1614 条,合计 3953。而 emoji-test.txt 里标为 fully-qualified 的是 3944 条,两者差 9,正是 5 个肤色修饰符与 4 个发型组件——它们在那份清单里被单列为 component,自己不算一个可用的 emoji。

图 4-1 · 用 RGI_Emoji 字符串属性在 v 模式下全局扫一段混合文本,把每个完整 emoji 整段框出并计数。可改文本观察国旗与 ZWJ 家庭各被算作一个整体。

注 · v 模式(ECMAScript 2024 的 unicodeSets)是字符串属性的前提,自研引擎 只实现到码点属性,所以 \p{Emoji_Presentation} 跑得动、\p{RGI_Emoji} 跑不动。这也是 六个 emoji 属性 那页与本页的分界。

5 · 参考文献

  1. Unicode. UTS #51: Unicode Emoji. RGI 的定义与四类序列的构造规则:区域指示符对、tag 序列、修饰符序列与 ZWJ 序列。unicode.org
  2. Unicode. emoji-sequences.txt(Emoji 17.0). 国旗 259、tag 旗 3、肤色 665、按键 12 与 Basic_Emoji 的全量条目,本页各计数照它统计。unicode.org
  3. Unicode. emoji-zwj-sequences.txt(Emoji 17.0). 1614 条 RGI ZWJ 序列的清单,家庭、职业与彩虹旗都在其中。unicode.org
  4. TC39. RegExp v flag with set notation + properties of strings. 字符串属性与 v 标志的提案仓库(Stage 4,已并入 ES2024),说明 \p{RGI_Emoji} 为何不能用于 u 模式。github.com
  5. Unicode. Full Emoji List. 各厂商字体对同一序列的渲染对照表,可用来核对某面旗在某平台上到底画不画得出来。unicode.org