1. 程式人生 > >初級字典樹查找在 Emoji、關鍵字檢索上的運用 Part-1

初級字典樹查找在 Emoji、關鍵字檢索上的運用 Part-1

行修改 諸多 解決 enume ref 數值 長度 關於 定位

系列索引

  1. Unicode 與 Emoji
  2. 字典樹 TrieTree 與性能測試
  3. 生產實踐

前言

通常用戶自行修改資料是很常見的需求,我們規定昵稱長度在2到10之間。假設用戶試圖使用表情符號 ????????????作為用戶名,請求是否合法?

打開瀏覽器控制臺,輸入 ‘???????????‘.length,打印結果是11。

公司項目涉及內容打印的,之前將 Emoji 顯示成亂碼、框框是家常便飯,而且手機和瀏覽器、打印物各種不一致也相當折磨人。硬頭皮閱讀 unicode.org/emoji ,使用哈希查找暫解決了問題。

年前項目遇到敏感詞過濾的需求,各種參考,結合之前的 Emoji 方案,方才有桃花源 “復行數十步,豁然開朗”的感悟,解決方案得到了升級。

以下內容是關於字典樹-TrieTree的初級使用,並運用到 Emoji 定位查找和敏感詞過濾的實際過程。


Unicode

對於我們程序員,Emoji 帶來了諸多問題

  • 長度是怎樣的?
  • 如何在各種平臺顯示一致?

解決這些問題不可能脫離 Unicode 字符來談。

當我們談論 Unicode 時,我們在說什麽?

  • 談談 Emoji 和字符編碼 篇幅不長,對Emoji 是什麽,和 Unicode 字符有什麽關系做了比較好的開篇;
  • 字符集和字符編碼(Charset & Encoding) 相對學院派,系統介紹了字符的發展;
  • Unicode與JavaScript詳解 雖然講的是 JavaScript 中的 Unicode,但可以引申到各種語言。

由於 JavaScript 只能處理 UCS-2 編碼,造成所有字符在這門語言中都是2個字節,如果是4個字節的字符,會當作兩個雙字節的字符處理。JavaScript 的字符函數都受到這一點的影響,無法返回正確結果。

在閱讀完以上資料後,想必前面的兩個問題有了初步概念。以下是 Unicode 字符"??"在部分編程語言及版本中的體現。

編程語言 字符集 編碼 字符"??"的字面量
C# Unicode UTF-16 "\ud834\udf06"
Java Unicode UTF-16 "\ud834\udf06"
ECMAScript 5 Unicode UCS-2 "\ud834\udf06"
ECMAScript 6 Unicode UCS-2, UTF-16 "\ud834\udf06", "\u{1d306}"
Python ? ? ? u‘\U0001d306‘

概括來說,UTF-16使用一組規則擴充了字符集。

  • 如果字符編碼U小於0x10000,也就是十進制的0到65535之內,則直接使用兩字節表示;
  • 如果字符編碼U大於0x10000,由於UNICODE編碼範圍最大為0x10FFFF,從0x10000到0x10FFFF之間 共有0xFFFFF個編碼,也就是需要20個bit就可以標示這些編碼。用U‘表示從0-0xFFFFF之間的值,將其前 10 bit作為高位和16 bit的數值0xD800進行 邏輯or 操作,將後10 bit作為低位和0xDC00做 邏輯or 操作,這樣組成的 4個byte就構成了U的編碼。

部分編程語言對 4字節 Unicode 的支持

Java

String str = "\ud834\udf06";
System.out.printf("str: %s, length: %d", str, str.length());

// str: ??, length: 2

C#

 mString str = "\ud834\udf06";
Console.WriteLine("str: {0}, length: {1}", str, str.Length);

// str: ??, length: 2

JavaScript

> let str = "\ud834\udf06";
> str
< "??"
> console.log("str: %s, length: %d", str, str.length);
  str: ??, length: 2

Python 3

>>> s = "\ud834\udf06"
>>> s
'\ud834\udf06'
>>> len(s)
2

Python 2

>>> s = "\ud834\udf06"
>>> s
'\\ud834\\udf06'
>>> len(s)
12

>>> s = u'\ud834\udf06' 
>>> s
u'\U0001d306'
>>> len(s)
2

多數的編程語言的"字符串長度"表達的是"字符串占用字節的長度"。可視字符的長度計算和檢索需要先將字節序列轉化為 Unicode 字符序列。采用UTF-16 的編程語言有能力能夠理解上述規則,但由於歷史問題等基於 UCS-2 的 ECMAScript 5 及 Python2 就悲劇了。

C# Char.IsHighSurrogateStringInfo

//獲取 unicode 碼點
public static IEnumerable<Int32> CodePoints(this String s) {
  for (int i = 0; i < s.Length; ++i) {
    yield return Char.ConvertToUtf32(s, i);
    if (Char.IsHighSurrogate(s, i))
      i++;
  }
}   

public static IEnumerable<String> TextElements(String s) {
  var enumerator = StringInfo.GetTextElementEnumerator(s);
  while (enumerator.MoveNext()) {
    yield return enumerator.GetTextElement();
  }
}

ECMAScript 6 String.prototype.codePointAt(index: number)

需要註意的是,對於4字節碼點字符,如果參數大於Unicode字符數時,String.prototype.codePointAt 函數仍然生效但退化成了 String.prototype.charCodeAt 的實現。

故不能簡單實現成 let codePoints = s => Array.from([...s].keys()).map(i => s.codePointAt(i));

let s = '????????';
let codePoints = s => Array.from([...s].keys()).map(i => s.codePointAt(i));
codePoints(s)

//[128104, 56424, 8205, 128105, 56425] ERROR!!!

正確的做法

let s = '????????';
let codePoints = s => [...s].length === 1 
  ? Array.from([...s].keys()).map(i => s.codePointAt(i)) 
  : Array.prototype.concat.call(...[...s].map(codePoints));
codePoints(s)
//(5) [128104, 8205, 128105, 8205, 128102]

Emoji

Emoji 最早在日本興起,然後由 Apple 引入,目前是國際標準,見於Unicode? Emoji 。這個過程帶來了各種歷史遺留問題(後邊會提提及),而Emoji 本身也在持續發展中,今天的資料可能變成明日黃花。

有了對 Unicode 的科普在前,我們現在知道 Emoji 只是 Unicode 字符或序列,文本渲染引擎遇到它們時進行解析和替換成自有實現。

  • 部分 Emoji 可以用2字節字符表示
  • 部分 Emoji 可以用4字節字符表示
  • 部分 Emoji 可以是一套 Unicode 字符組合
  • 部分 Emoji 是其他 Emoji 的組合,可能存在退化方案

略微提及,macOS 和 Android 分別使用的解決方案關鍵字是 AppleColorEmojiNotoColorEmoji`,涉及TTF字休編程等,如有需要請自行搜索。

由此可見,Emoji 長度雖然確定但不能目測;如何顯示是文本渲染引擎的工作,但不同的平臺、 瀏覽器、 廠商甚至各個版本之間都有巨大的差異。

長度是怎樣的?

探究 emoji 字符長度 有一段代碼演示了 Emoji 字符長度的表現。

// neutral family
// U+1F46A
// length: 2
> ??
// ZWJ sequence: family (man, woman, boy)
// U+1F468 + U+200D + U+1F469 + U+200D + U+1F466
// ??? + U+200D + ??? + U+200D + ??
// length: 8
> ?????????
// ZWJ sequence: family (woman, woman, girl)
// U+1F469 + U+200D + U+1F469 + U+200D + U+1F467
// ??? + U+200D + ??? U+200D + ??
// length: 8
> ?????????
// ZWJ sequence: family (woman, woman, girl, girl)
// U+1F469 + U+200D + U+1F469 + U+200D + U+1F467 + U+200D + U+1F467
// ??? + U+200D + ??? + U+200D + ??? + U+200D + ??
// length: 11
> ????????????

這段文本可能因為瀏覽器版本等原因看到表情序列而不是組合,故我在 Chrome 下對顯示效果作了截圖

如何在各種平臺顯示一致?

Twitter 對 Emoji 跨平臺一致顯示的解決方案在 twitter/twemoji。它有以下問題:

  • 按年度月份更新, 像框中間有數字的 emoji 字符 ‘\u0031\ufe0f\u20e3‘ 還不支持
  • 以其 CDN 資源作為結果輸果.

我們想知道一段文本裏邊有什麽 Emoji,在哪裏,怎樣替換,怎樣自定義顯示,需要更多的掌控。

初級字典樹查找在 Emoji、關鍵字檢索上的運用 Part-1