初级字典树查找在 Emoji、关键字检索上的运用 Part-1
系列索引
前言
通常用户自行修改资料是很常见的需求,我们规定昵称长度在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,在哪里,怎样替换,怎样自定义显示,需要更多的掌控。