初级字典树查找在 Emoji、关键字检索上的运用 Part-1

系列索引

  1. Unicode 与 Emoji
  2. 字典树 TrieTree 与性能测试
  3. 生产实践

前言

通常用户自行修改资料是很常见的需求,我们规定昵称长度在2到10之间。假设用户试图使用表情符号 ‍👩‍👩‍👧‍👧作为用户名,请求是否合法?

打开浏览器控制台,输入 '👩‍👩‍👧‍👧'.length,打印结果是11。

公司项目涉及内容打印的,之前将 Emoji 显示成乱码、框框是家常便饭,而且手机和浏览器、打印物各种不一致也相当折磨人。硬头皮阅读 unicode.org/emoji ,使用哈希查找暂解决了问题。

年前项目遇到敏感词过滤的需求,各种参考,结合之前的 Emoji 方案,方才有桃花源 “复行数十步,豁然开朗”的感悟,解决方案得到了升级。

以下内容是关于字典树-TrieTree的初级使用,并运用到 Emoji 定位查找和敏感词过滤的实际过程。


Unicode

对于我们程序员,Emoji 带来了诸多问题

  • 长度是怎样的?
  • 如何在各种平台显示一致?

解决这些问题不可能脱离 Unicode 字符来谈。

当我们谈论 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,在哪里,怎样替换,怎样自定义显示,需要更多的掌控。

posted @ 2018-08-30 13:30  Jusfr  阅读(915)  评论(0编辑  收藏  举报