初級字典樹查找在 Emoji、關鍵字檢索上的運用 Part-1
系列索引
- Unicode 與 Emoji
- 字典樹 TrieTree 與性能測試
- 生產實踐
前言
通常用戶自行修改資料是很常見的需求,我們規定昵稱長度在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.IsHighSurrogate
和 StringInfo
//獲取 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 分別使用的解決方案關鍵字是 AppleColorEmoji 和 NotoColorEmoji`,涉及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