一. 前言
基于JavaScript的分词工具实际上不少, 但是大部分是无法运行在浏览器上的, 多数只能在nodejs上运行, 或者是浏览器端的表现不佳(速度, 效果).
一直试图尝试将机器学习(统计/贝叶斯, 效仿垃圾邮件的过滤机制)引入到JavaScript(tampermonkey)实现对内容相对智能 的拦截, 但是一直找不到比较好的解决方案(特别是分词的处理).
只能通过手动关键词, 正则表达式等相对硬性规则实现对内容的判断, 但这些方式的细腻度都不够, 导致误过滤的情况太严重, 假如放松规则, 则漏网之鱼太多.
(从关键词到url, 进行各层级的拦截)
鉴于互联网正在死去, 砌墙, 垃圾铺天盖地, 不得不为一些常访问的站点建立起内容过滤器, 对于硬性的广告倒不是问题, 问题是大量广告正在转换为软广告, 无法通过传统的方式标记清除.
以B站为例, 虽然其有推荐机制, 虽然其推荐的内容是根据用户行为/偏好推荐的, 但是鉴于推荐算法很垃圾, 以及广告(软)的推送需要, 大量垃圾视频被推送到页面, 这些内容不仅需要浪费大量的时间去筛选, 而且还会引起生理的不适(智障内容过多), 需要辅助过滤功率来减少筛选视频带来的负面影响, 同理在其他站点同样需要类似的机制来减少垃圾.
所谓的大数据时代, 更多时候对于普通用户弊多大于利, 多数情况只能被动接纳各种信息.
二. 分词
在NLP中, 分词可谓是整个处理流程的开端.
在JavaScript(浏览器端)上执行分词是比较难的, JavaScript是单线程执行的, 分词往往需要消耗大量的时间阻塞线程... 总之由于各种限制, 机器学习相关的内容直接部署在浏览器端的还是比较少(部分可以部署, 但是可能阉割掉大量功能).
最近又重新捣鼓相关的脚本, 查阅了一下资料, 偶然发现原来JavaScript居然原生支持分词, amazing......!
实际上很早以前, 在chrome浏览器中就有一个特性, 就是当用户双击某些文本, 浏览器会自动选中部分的内容(词汇).
2.1 原生 API
这个api还是相对比较新的, 在bing上相关搜索页面数很少.
兼容性, chrome87已经开始支持, 但是这么迟才看到这个api, 实在惭愧.
可以看到, 除了Firefox之外, 其他的浏览器都跟进了这个标准.
先来看看性能如何:
{
    console.time();
    const text = '【碰碰车】这游戏究竟怎么玩- - 当年连第一关都过不去';
    console.table(Array.from(new Intl.Segmenter('cn', { granularity: 'word' }).segment(text)));
    console.timeEnd();
}
初步来看, 可以看到执行速度非常快, 分词效果还不错!
来点有难度的, 复杂语义的时候, 分词就无法返回预期的结果了, 目前该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,NumberFormat和DateTimeFormat对象的构造函数是Intl对象的属性. 本页文档内容包括了这些属性, 以及国际化使用的构造器和其他语言的方法等常见的功能.
new Intl.Segmenter()
new Intl.Segmenter(locales)
new Intl.Segmenter(locales, options)
参数
locales可选带有 BCP 47 语言区域标记的一个字符串, 或者一个这样的字符串数组. 对于
locales参数的一般形式和解释, 参见语言区域识别和判定.
options可选带有部分或全部以下属性的一个对象:
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
这段话的分词和上面的原生的完全一致, 但是支持词性的显示.
我
test1.html:13 来到
test1.html:13 中国
test1.html:13 人民大学
另一组分词, 则更接近预期的答案.
虽然中国人民大学在字典中, 但是并没有被完整分离出来.
导入自定义词典:
  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'])
更多设置可以参阅文档, 总体的设置并不算复杂.
整体分词效果还是可以的, 但是对于长词的效果一般.
其他的一些使用
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';
默认配置了几个字典, 核心使用的是盘古的字典.
使用并不是很复杂, 更多细节可以见文档或者直接调试代码.