JavaScript(浏览器端)分词

一. 前言

基于JavaScript的分词工具实际上不少, 但是大部分是无法运行在浏览器上的, 多数只能在nodejs上运行, 或者是浏览器端的表现不佳(速度, 效果).

一直试图尝试将机器学习(统计/贝叶斯, 效仿垃圾邮件的过滤机制)引入到JavaScript(tampermonkey)实现对内容相对智能 的拦截, 但是一直找不到比较好的解决方案(特别是分词的处理).

只能通过手动关键词, 正则表达式等相对硬性规则实现对内容的判断, 但这些方式的细腻度都不够, 导致误过滤的情况太严重, 假如放松规则, 则漏网之鱼太多.

img

(从关键词到url, 进行各层级的拦截)

鉴于互联网正在死去, 砌墙, 垃圾铺天盖地, 不得不为一些常访问的站点建立起内容过滤器, 对于硬性的广告倒不是问题, 问题是大量广告正在转换为软广告, 无法通过传统的方式标记清除.

img

B站为例, 虽然其有推荐机制, 虽然其推荐的内容是根据用户行为/偏好推荐的, 但是鉴于推荐算法很垃圾, 以及广告(软)的推送需要, 大量垃圾视频被推送到页面, 这些内容不仅需要浪费大量的时间去筛选, 而且还会引起生理的不适(智障内容过多), 需要辅助过滤功率来减少筛选视频带来的负面影响, 同理在其他站点同样需要类似的机制来减少垃圾.

img

所谓的大数据时代, 更多时候对于普通用户弊多大于利, 多数情况只能被动接纳各种信息.

二. 分词

NLP中, 分词可谓是整个处理流程的开端.

JavaScript(浏览器端)上执行分词是比较难的, JavaScript是单线程执行的, 分词往往需要消耗大量的时间阻塞线程... 总之由于各种限制, 机器学习相关的内容直接部署在浏览器端的还是比较少(部分可以部署, 但是可能阉割掉大量功能).

最近又重新捣鼓相关的脚本, 查阅了一下资料, 偶然发现原来JavaScript居然原生支持分词, amazing......!

img

实际上很早以前, 在chrome浏览器中就有一个特性, 就是当用户双击某些文本, 浏览器会自动选中部分的内容(词汇).

2.1 原生 API

Intl - JavaScript | MDN

这个api还是相对比较新的, 在bing上相关搜索页面数很少.

img

img

兼容性, chrome87已经开始支持, 但是这么迟才看到这个api, 实在惭愧.

可以看到, 除了Firefox之外, 其他的浏览器都跟进了这个标准.


先来看看性能如何:

{
    console.time();
    const text = '【碰碰车】这游戏究竟怎么玩- - 当年连第一关都过不去';
    console.table(Array.from(new Intl.Segmenter('cn', { granularity: 'word' }).segment(text)));
    console.timeEnd();
}

img

初步来看, 可以看到执行速度非常快, 分词效果还不错!

来点有难度的, 复杂语义的时候, 分词就无法返回预期的结果了, 目前该api不支持自定义词典.

{
    const text = '我来到中国人民大学';
    Array.from(new Intl.Segmenter('cn', { granularity: 'word' }).segment(text)).forEach(e => console.log(e.segment));
}
VM196:3 我
VM196:3 来到
VM196:3 中国
VM196:3 人民
VM196:3 大学

需要注意, 分词这个模块是位于Intl这个对象之下, 在这个对象之下还有不少和标准有关的内容.

Intl 对象是 ECMAScript 国际化 API 的一个命名空间, 它提供了精确的字符串对比, 数字格式化, 和日期时间格式化. Collator, NumberFormatDateTimeFormat 对象的构造函数是 Intl 对象的属性. 本页文档内容包括了这些属性, 以及国际化使用的构造器和其他语言的方法等常见的功能.

new Intl.Segmenter()
new Intl.Segmenter(locales)
new Intl.Segmenter(locales, options)

参数

带有 BCP 47 语言区域标记的一个字符串, 或者一个这样的字符串数组. 对于 locales 参数的一般形式和解释, 参见语言区域识别和判定.

带有部分或全部以下属性的一个对象: granularity 可选字符串. 可选值如下: "grapheme"( 默认) 根据语言区域, 将输入值按字( 用户可以感知的字符) 划分边界. "word"根据语言区域, 将输入值按词划分边界. "sentence"根据语言区域, 将输入值按句划分边界. localeMatcher 可选将要使用的语言区域匹配算法. 可选值如下: "best fit"( 默认) 运行时可能会选择一个可能比查找算法的结果更加合适的语言区域. "lookup"使用 BCP 47 查找算法来从 locales 参数中选择语言区域. 对于 locales 参数中的每一个语言区域, 会返回第一个运行时支持的语言区域( 有可能会移除用于限制区域的子标记, 来找到一个支持的语言区域. 换句话说, 如果运行时支持 "de" 但不支持 "de-CH", 用户传入的 "de-CH" 可能就会以 "de" 为结果进行使用) .

返回值

一个新的 Intl.Segmenter 实例.

由于该api提供的内容还非常有限, 其他内容自行参阅文档即可, 没什么好说的.

2.2 segmentit

GitHub - linonetwo/segmentit: 任何 JS 环境可用的中文分词包, fork from leizongmin/node-segment

注意cdn的地址: https://cdn.jsdelivr.net/npm/segmentit@2.0.3/dist/umd/segmentit.js(由于朝廷过于关心人民群众, 墙越来越高), 建议将地址改为: fastly.jsdelivr.net, 这个地址是相对稳定的.

简单测试一下:

<!DOCTYPE html>
<html>

<head>
    <script type="text/javascript" src="https://fastly.jsdelivr.net/npm/segmentit@2.0.3/dist/umd/segmentit.js"></script>
</head>

<body>
    <script>
        console.time();
        // Segmentit.useDefault 载入默认的配置
        const segmentit = Segmentit.useDefault(new Segmentit.Segment());
        // 字典, 以及各个模块会导入, 不需要手动导入
        const result = segmentit.doSegment('【碰碰车】这游戏究竟怎么玩- - 当年连第一关都过不去');
        console.log(result);
        console.timeEnd()
    </script>
</body>

</html>
【
test1.html:13 碰碰车
test1.html:13 】
test1.html:13 这
test1.html:13 游戏
test1.html:13 究竟
test1.html:13 怎么
test1.html:13 玩
2test1.html:13 -
test1.html:13 当年
test1.html:13 连
test1.html:13 第一
test1.html:13 关
test1.html:13 都
test1.html:13 过不去
test1.html:14 default: 198.593994140625 ms

这段话的分词和上面的原生的完全一致, 但是支持词性的显示.

img

我
test1.html:13 来到
test1.html:13 中国
test1.html:13 人民大学

另一组分词, 则更接近预期的答案.

img

虽然中国人民大学在字典中, 但是并没有被完整分离出来.

导入自定义词典:

  loadDict = (
    dict: string | string[],
    type = 'TABLE',
    convertToLower: boolean = false,
  )

必须是这种格式: 词|词性|权重

segmentit.loadDict(['中国人民大学|0x0020|31064'])

调整关键词的权重之后, 可以分离出中国人民大学, 需要足够大的权重.

其他方式初始化, 将不会载入默认配置:

const segmentit = new Segmentit.Segment();
// 载入各个组件
segmentit.use(Segmentit.modules);
// 载入词典, 否则为空字典
segmentit.loadDict([...Segmentit.dicts, '中国人民大学|0x0020|31064'])

更多设置可以参阅文档, 总体的设置并不算复杂.

整体分词效果还是可以的, 但是对于长词的效果一般.

img

其他的一些使用

import { Segment, modules, dicts, synonyms, stopwords } from 'segmentit';

const segmentit = new Segment();
segmentit.use(modules);
segmentit.loadDict(dicts);
segmentit.loadSynonymDict(synonyms);
segmentit.loadStopwordDict(stopwords);

全部模块:

import {
  Segment,
  ChsNameTokenizer,
  DictOptimizer,
  EmailOptimizer,
  PunctuationTokenizer,
  URLTokenizer,
  ChsNameOptimizer,
  DatetimeOptimizer,
  DictTokenizer,
  ForeignTokenizer,
  SingleTokenizer,
  WildcardTokenizer,
  pangu,
  panguExtend1,
  panguExtend2,
  names,
  wildcard,
  synonym,
  stopword,
} from 'segmentit';

默认配置了几个字典, 核心使用的是盘古的字典.

使用并不是很复杂, 更多细节可以见文档或者直接调试代码.