Unicode 顏文字(emoji)格式和 Go 代碼處理

前幾天時間測試同窗在咱們的前端輸入了顏文字,以後軟件就出 bug 了。借修 bug 機會我花了點時間學習了一下 Unicode 顏文字(emoji)的一些知識。本文記錄我對 emoji 的一些認識,而且簡單介紹一下我爲此而作的一個 Go 語言顏文字提取庫的用法。前端

Unicode 背景簡介

咱們你們都知道,爲了標準化全世界全部文字的編碼,誕生了 unicode。最先 unicode 的設計者們採用的是一個字(2 Bytes)來表示 unicode 值(UCS-2),覺得總共 65536 個值就能夠表示全部的字符了,也就是咱們常見的 unicode 表示法 U+1234git

然而漢字的博大精深(歷史上的各類漢字實在是太多了)讓 unicode 認識到了錯誤。很快,unicode 的編碼空間就擴展到了21位(注意:略少於3個字節,可是實際上在內存中常用4字節存儲,對應於 UCS-4)。在絕大部分的程序語言/軟件中,使用等效的 uint32 類型就能夠將 unicode 字符一一保存。程序員

好比對應於 MySQL 的 utf8mb4 就是可使用最大 4 個字節來保存 unicode 字符。咱們的 bug 就是出在 DB 中,解決方法很簡單,改爲 utfmb4 就好了。github

Emoji 編碼格式簡介

使用了3個字節來保存 unicode,這讓不少剛接觸 unicode 的程序員很容易誤覺得:那麼一個字確定不會超過 int32 類型了吧?從計算機程序的角度而言,確實如此。可是從文字和語言學的角度而言,一個,其實在程序中並不必定僅對應着一個程序字符golang

首先從傳統的 unicode 字符而言,就存在着 "修飾字符" 和 「組合字符」 的概念,修飾字符和組合字符配合基本字符,能夠組成一個咱們從視覺上看到的單一字符。好比下面這個讓你不會讀的 a,是由五個 unicode 字符組成的;但在視覺和語言學角度上,這只是一個字:正則表達式

修飾字符說明

咱們具體到 emoji 而言,也是相似的狀況:一個視覺上的文字單元,在底層多是由多個 unicode 字符所組成的。好比你們最常常拿來舉例的、表示一家四口的文字 "👨‍👩‍👧‍👦"(<-- 若是你的瀏覽器看到的是四個分離的頭像,那說明你的終端不支持 E2.0 版本 emoji),實際上在底層是由喪心病狂的七個 unicode 字符組成,分別爲:U+1F468U+200DU+1F469U+200DU+1F467U+200DU+1F466瀏覽器

如無特殊說明,下文采用 「字符」 一詞表示一個 unicode 值,而 「文字」 一詞則表示視覺上的一個單一文字。post

固然,emoji 的連字規則並非隨意拼接、徹底自由的。Unicode 標準裏針對 emoji 也規定了幾種格式。下面以本文成文時最新的 unicode 13.0(2020-01-28 發佈)說明以下:學習

基本 emoji

這裏對應着Emoji Sequences 標準書的 「Basic_Emoji」 小節,其中每一行後面都包括了該字符被引入的標準版本。若是讀者在哪一行看到了方塊,那就說明你的系統不支持該版本。基本 emoji 字符包含了兩種類型:測試

  1. 單一 unicode 字符所組成的一個視覺字符。按照 unicode 的規定,終端在展現這些文字時,默認應該以顏文字版(也就是彩色動態版)進行展現。
  2. 以單一 unicode 字符,後接 U+FE0EU+FE0F 所表示的一個文字。其中若是後加 U+FE0F,則與上一規則相同,表示以顏文字模式展現。若是以 U+FE0E,則表示以 text 黑白文本模式展現該文字(但實際上很多終端壓根不理這條規則,亦或者是支持不徹底)。

並非全部的基本 emoji 字符都包含兩種顯示模式,應按照 unicode 標準中列出的組合爲準。總共有 1329 個組合。

Emoji 鍵帽序列(Emoji Keycap Sequence)

這裏對應着Emoji Sequences 標準書的 「Emoji_Keycap_Sequence」 小節,這一類序列總共有12組,這裏其實就對應着電話上的12個按鈕,分別是 0~9 十個字符,外加 # 和 * 開頭,而後後面緊跟着 U+FE0FU+20E3 兩個字符組成的。好比咱們能夠很方便地擺出一個電話鍵盤出來:

1️⃣2️⃣3️⃣
4️⃣5️⃣6️⃣
7️⃣8️⃣9️⃣
*️⃣0️⃣#️⃣

Emoji 國家/地區旗序列

這裏則對應着Emoji Sequences 標準書的 「RGI_Emoji_Flag_Sequence」 小節。其中 RGI 表示 Recommended for General Interchange,推薦可在平常的交互/交流中使用。

這一組文字均由兩個 unicode 字符組成,字符的值爲 U+1F1E6U+1F1FF 的26個字符,一一對應着 A 到 Z。這一組 unicode 文字對應着使用兩個字母的國家/地區碼所對應的國家/地區旗幟,以及用 UN 表示的聯合國旗和 EU 表示的歐盟旗。

合法的旗幟總共有 258 個組合,標準中完整地列出了。須要注意的是,U+1F1E6U+1F1FF 這26個字符不能單獨出現,它們是專門用於這一類旗幟所使用的特殊 unicode 字符。

國家/地區碼可參見 ISO 3166-1

Emoji 標記序列

這一組實際上是 unicode 預留的擴展類別,雖然在 emoji 中定義了所謂 「tag latin letter」 用於此類別,可是目前只有三個合法文字,從展現效果上分別是 英格蘭、蘇格蘭、威爾士旗幟(北愛爾蘭:喵喵喵?)。而 「tag」 字符也是不單獨出現的。

打趣一下,以英格蘭旗爲例,七個字符分別爲:U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F,分別對應如下含義:

  1. 黑色旗幟
  2. 拉丁字母 g
  3. 拉丁字母 b
  4. 拉丁字母 e
  5. 拉丁字母 n
  6. 拉丁字母 g
  7. DELETE 字符

難道這意思是:「黑化的英國英格蘭(劃去)」 ?

Emoji 修飾符序列

Unicode 定義了五個用於 emoji 的膚色字符,分別是:U+1F3FB U+1F3FC U+1F3FD U+1F3FE U+1F3FF,在 unicode 標準中分別表示:

  1. light skin tone
  2. medium-light skin tone
  3. medium skin tone
  4. medium-dark skin tone
  5. dark skin tone

用於與部分基本 emoji 經字符搭配,用於調整相應文字中的膚色。經常使用在須要西方式 「政治正確」 的場合。

這五個字符按照標準而言是不會單獨出現的,必然是跟在一個基本 emoji 後面。這對應着Emoji Sequences 標準書的 「RGI_Emoji_Modifier_Sequence」 小節。

Unicode 總共定義了 580 個 modifier sequences,也就是說有 116 個基本 emoji 字符能夠搭配膚色字符使用。

Emoji ZWJ 序列

ZWJ 也即 Zero Width Joiner,也就是零寬度鏈接符。ZWJ 的 unicode 代碼爲 U+200D,它不會被顯示出來。它的做用是用於鏈接兩個 unicode 字符,組成可視的文字。前文所述的 「👨‍👩‍👧‍👦」 文字,就是使用 ZWJ 將一個男人頭像、一個女人頭像、一個男孩頭像、一個女孩頭像鏈接起來的文字。

並非全部的 emoji 均可以任意鏈接。Unicode 定義了 1122 個 Emoji ZWJ 序列類型的文字。在 Emoji ZWJ Sequences 標準書能夠查閱完整的列表。

在 Go 中提取 unicode emoji 文字

經過前文描述,咱們若是須要從一段 string 中一個個提取出單1、獨立的一個個 emoji 文字(注意是文字而不是分離的 unicode 字符),那麼咱們其中的一個思路,就是按照前文的幾種規則,對 unicode 字符串中的每個子串進行檢查,看是否會出現符合 emoji 規則的子串。

目前我在 Github 上看到有一個 emoji 提取庫用的是正則表達式的方法來提取出字符串中的 emoji 段落。可是這個庫太慢、太老了(2015年),並且並不支持 ZWJ 序列。因而我本身寫了一個

基本原理其實很簡單。讓咱們看看 unicode 官方的兩個主要文檔 Emoji SequenceEmoji 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 國際許可協議 進行許可。

相關文章
相關標籤/搜索