博客 / 詳情

返回

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

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

Unicode 背景簡介

我們大家都知道,為了標準化全世界所有文字的編碼,誕生了 unicode。最早 unicode 的設計者們採用的是一個字(2 Bytes)來表示 unicode 值(UCS-2),以為總共 65536 個值就可以表示所有的字符了,也就是我們常見的 unicode 表示法 U+1234

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

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

Emoji 編碼格式簡介

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

首先從傳統的 unicode 字符而言,就存在着 "修飾字符" 和 “組合字符” 的概念,修飾字符和組合字符配合基本字符,可以組成一個我們從視覺上看到的單一字符。比如下面這個讓你不會讀的 a,是由五個 unicode 字符組成的;但在視覺和語言學角度上,這只是一個字:

修飾字符説明

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

如無特殊説明,下文采用 “字符” 一詞表示一個 unicode 值,而 “文字” 一詞則表示視覺上的一個單一文字。

當然,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 中一個個提取出單一、獨立的一個個 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->

參考資料

  • 轉-寫給程序員的 Unicode 入門介紹
  • 簡單來談談Unicode與emoji
  • Emoji, 沒想到你是這樣的...
  • “組合字符”和“修飾字母”有什麼區別?
  • Unicode® 13.0 Emoji
  • emojipedia
  • 空白修飾字母 Unicode字符表
  • 修改MySQL的字符集為utf8mb4

本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。

  • 原作者: amc,歡迎轉載,但請註明出處。
  • 原文標題:Unicode 顏文字(emoji)格式和 Go 代碼處理
  • 發佈日期:2020-03-21
  • 原文鏈接:https://cloud.tencent.com/developer/article/1602547。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.