前几天时间测试同窗在咱们的前端输入了颜文字,以后软件就出 bug 了。借修 bug 机会我花了点时间学习了一下 Unicode 颜文字(emoji)的一些知识。本文记录我对 emoji 的一些认识,而且简单介绍一下我为此而作的一个 Go 语言颜文字提取库的用法。前端
咱们你们都知道,为了标准化全世界全部文字的编码,诞生了 unicode。最先 unicode 的设计者们采用的是一个字(2 Bytes)来表示 unicode 值(UCS-2),觉得总共 65536 个值就能够表示全部的字符了,也就是咱们常见的 unicode 表示法 U+1234
。git
然而汉字的博大精深(历史上的各类汉字实在是太多了)让 unicode 认识到了错误。很快,unicode 的编码空间就扩展到了21位(注意:略少于3个字节,可是实际上在内存中常用4字节存储,对应于 UCS-4)。在绝大部分的程序语言/软件中,使用等效的 uint32
类型就能够将 unicode 字符一一保存。程序员
好比对应于 MySQL 的 utf8mb4
就是可使用最大 4 个字节来保存 unicode 字符。咱们的 bug 就是出在 DB 中,解决方法很简单,改为 utfmb4
就好了。github
使用了3个字节来保存 unicode,这让不少刚接触 unicode 的程序员很容易误觉得:那么一个字确定不会超过 int32
类型了吧?从计算机程序的角度而言,确实如此。可是从文字和语言学的角度而言,一个字,其实在程序中并不必定仅对应着一个程序字符。golang
首先从传统的 unicode 字符而言,就存在着 "修饰字符" 和 “组合字符” 的概念,修饰字符和组合字符配合基本字符,能够组成一个咱们从视觉上看到的单一字符。好比下面这个让你不会读的 a
,是由五个 unicode 字符组成的;但在视觉和语言学角度上,这只是一个字:正则表达式
咱们具体到 emoji 而言,也是相似的状况:一个视觉上的文字单元,在底层多是由多个 unicode 字符所组成的。好比你们最常常拿来举例的、表示一家四口的文字 "👨👩👧👦"(<-- 若是你的浏览器看到的是四个分离的头像,那说明你的终端不支持 E2.0 版本 emoji),实际上在底层是由丧心病狂的七个 unicode 字符组成,分别为:U+1F468
、U+200D
、U+1F469
、U+200D
、U+1F467
、U+200D
、U+1F466
。浏览器
如无特殊说明,下文采用 “字符” 一词表示一个 unicode 值,而 “文字” 一词则表示视觉上的一个单一文字。post
固然,emoji 的连字规则并非随意拼接、彻底自由的。Unicode 标准里针对 emoji 也规定了几种格式。下面以本文成文时最新的 unicode 13.0(2020-01-28 发布)说明以下:学习
这里对应着Emoji Sequences 标准书的 “Basic_Emoji
” 小节,其中每一行后面都包括了该字符被引入的标准版本。若是读者在哪一行看到了方块,那就说明你的系统不支持该版本。基本 emoji 字符包含了两种类型:测试
U+FE0E
或 U+FE0F
所表示的一个文字。其中若是后加 U+FE0F
,则与上一规则相同,表示以颜文字模式展现。若是以 U+FE0E
,则表示以 text 黑白文本模式展现该文字(但实际上很多终端压根不理这条规则,亦或者是支持不彻底)。并非全部的基本 emoji 字符都包含两种显示模式,应按照 unicode 标准中列出的组合为准。总共有 1329 个组合。
这里对应着Emoji Sequences 标准书的 “Emoji_Keycap_Sequence
” 小节,这一类序列总共有12组,这里其实就对应着电话上的12个按钮,分别是 0~9 十个字符,外加 # 和 * 开头,而后后面紧跟着 U+FE0F
和 U+20E3
两个字符组成的。好比咱们能够很方便地摆出一个电话键盘出来:
1️⃣2️⃣3️⃣ 4️⃣5️⃣6️⃣ 7️⃣8️⃣9️⃣ *️⃣0️⃣#️⃣
这里则对应着Emoji Sequences 标准书的 “RGI_Emoji_Flag_Sequence
” 小节。其中 RGI
表示 Recommended for General Interchange,推荐可在平常的交互/交流中使用。
这一组文字均由两个 unicode 字符组成,字符的值为 U+1F1E6
到 U+1F1FF
的26个字符,一一对应着 A 到 Z。这一组 unicode 文字对应着使用两个字母的国家/地区码所对应的国家/地区旗帜,以及用 UN
表示的联合国旗和 EU
表示的欧盟旗。
合法的旗帜总共有 258 个组合,标准中完整地列出了。须要注意的是,U+1F1E6
到 U+1F1FF
这26个字符不能单独出现,它们是专门用于这一类旗帜所使用的特殊 unicode 字符。
国家/地区码可参见 ISO 3166-1。
这一组实际上是 unicode 预留的扩展类别,虽然在 emoji 中定义了所谓 “tag latin letter” 用于此类别,可是目前只有三个合法文字,从展现效果上分别是 英格兰、苏格兰、威尔士旗帜(北爱尔兰:喵喵喵?)。而 “tag” 字符也是不单独出现的。
打趣一下,以英格兰旗为例,七个字符分别为:U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F
,分别对应如下含义:
难道这意思是:“黑化的英国英格兰(划去)” ?
Unicode 定义了五个用于 emoji 的肤色字符,分别是:U+1F3FB U+1F3FC U+1F3FD U+1F3FE U+1F3FF
,在 unicode 标准中分别表示:
用于与部分基本 emoji 经字符搭配,用于调整相应文字中的肤色。经常使用在须要西方式 “政治正确” 的场合。
这五个字符按照标准而言是不会单独出现的,必然是跟在一个基本 emoji 后面。这对应着Emoji Sequences 标准书的 “RGI_Emoji_Modifier_Sequence
” 小节。
Unicode 总共定义了 580 个 modifier sequences,也就是说有 116 个基本 emoji 字符能够搭配肤色字符使用。
ZWJ 也即 Zero Width Joiner
,也就是零宽度链接符。ZWJ 的 unicode 代码为 U+200D
,它不会被显示出来。它的做用是用于链接两个 unicode 字符,组成可视的文字。前文所述的 “👨👩👧👦” 文字,就是使用 ZWJ 将一个男人头像、一个女人头像、一个男孩头像、一个女孩头像链接起来的文字。
并非全部的 emoji 均可以任意链接。Unicode 定义了 1122 个 Emoji ZWJ 序列类型的文字。在 Emoji ZWJ Sequences 标准书能够查阅完整的列表。
经过前文描述,咱们若是须要从一段 string 中一个个提取出单1、独立的一个个 emoji 文字(注意是文字而不是分离的 unicode 字符),那么咱们其中的一个思路,就是按照前文的几种规则,对 unicode 字符串中的每个子串进行检查,看是否会出现符合 emoji 规则的子串。
目前我在 Github 上看到有一个 emoji 提取库用的是正则表达式的方法来提取出字符串中的 emoji 段落。可是这个库太慢、太老了(2015年),并且并不支持 ZWJ 序列。因而我本身写了一个。
基本原理其实很简单。让咱们看看 unicode 官方的两个主要文档 Emoji Sequence 和 Emoji ZWJ Sequence 能够看出,实际上官方已经把所有合法的、能够组成单一 emoji 文字的 unicode 组合序列所有列出来了。所以,咱们只须要将这两个文件的所有序列导出来,而后在匹配字符串的时候,按照导出来的结果进行匹配就能够了。
个人代码中,将全部合法的序列所有导出成为一棵树。当检查字符串子串的时候,匹配树中所表明的合法的子串就能够了。示例代码以下:
package main import ( "log" "fmt" "github.com/Andrew-M-C/go.emoji" ) func main() { printf := log.Printf s := "👩👩👦🇨🇳" i := 0 final := emoji.ReplaceAllEmojiFunc(s, func(emoji string) string { i++ printf("%02d - %s - len %d", i, emoji, len(emoji)) return fmt.Sprintf("%d-", i) }) printf("final: <%s>", final) return } // Output: // 2009/11/10 23:00:00 01 - 👩👩👦 - len 18 // 2009/11/10 23:00:00 02 - 🇨🇳 - len 8 // 2009/11/10 23:00:00 final: <1-2->
本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。