1秒50万字!js实现关键词匹配


Posted in Javascript onAugust 01, 2016

在论坛和聊天室这样的场景里,为了保证用户体验,我们经常需要屏蔽很多不良词语。对于单个关键词查找,自然是indexOf、正则那样的方式效率比较高。但对于关键词较多的情况下,多次重复调用indexOf、正则的话去匹配全文的话,性能消耗非常大。由于目标字符串通常来说体积都比较大,所以必须要保证一次遍历就得到结果。根据这样的需求,很容易就想到对全文每个字符依次匹配的方式。比如对于这段文字:“Mike Jordan had said "Just do IT", so Mark has been a coder.”,假如我们的关键词是“Mike”“Mark”,那么可以遍历整句话,当找到“M”就接着看能不能匹配到“i”或者“a”,能一直匹配到最后则成功找到一个关键词,否则继续遍历。那么关键词的结构就应该是这样的: 

var keywords = {
 M: {
 i: {
  k: {
  e: {end: true}
  }
 },
 a: {
  r: {
  k: {end: true}
  }
 }
 }
}
 

由上文可以看出这个数据就是一个树结构,而根据关键词组来创建树结构还是比较耗时的,而关键词却又是我们早已给定的,所以可以在匹配前预先创建这样的数据结构。代码如下: 

function buildTree(keywords) {
 var tblCur = {},
 key, str_key, Length, j, i;
 var tblRoot = tblCur;

 for(j = keywords.length - 1; j >= 0; j -= 1) {
 str_key = keywords[j];
 Length = str_key.length;
 for(i = 0; i < Length; i += 1) {
  key = str_key.charAt(i);
  if(tblCur.hasOwnProperty(key)) {
  tblCur = tblCur[key];
  } else {
  tblCur = tblCur[key] = {};
  }
 }
 tblCur.end = true; //最后一个关键字
 tblCur = tblRoot;
 }
 return tblRoot;
}

 这段代码中用了一个连等语句:tblCur = tblCur[key] = {},这里要注意的是语句的执行顺序,由于[]的运算级比=高,所以首先是在 tblCur对象中先创建一个key属性。结合tblRoot = tblCur = {} 看,执行顺序就是: 

var tblRoot = tblCur = {};
tblRoot = tblCur;
tblCur['key'] = undefined; // now tblRoot = {key: undefined}
tblCur['key'] = {};
tblCur = tblCur['key'];
 

通过上面的代码就构建了好了所需的查询数据,下面看看查询接口的写法。 

对于目标字符串的每一字,我们都从这个keywords顶部开始匹配。首先是 keywords[a] ,若存在,则看 keyword[a][b],若最后 keyword[a][b]…[x]=true 则说明匹配成功,若 keyword[a][b]…[x]=undefined,则从下一个位置重新开始匹配 keywords[a] 。 

function search(content) {
 var tblCur,
 p_star = 0,
 n = content.length,
 p_end,
 match, //是否找到匹配
 match_key,
 match_str,
 arrMatch = [], //存储结果
 arrLength = 0; //arrMatch的长度索引

 while(p_star < n) {
 tblCur = tblRoot; //回溯至根部
 p_end = p_star;
 match_str = "";
 match = false;
 do {
  match_key = content.charAt(p_end);
  if(!(tblCur = tblCur[match_key])) { //本次匹配结束
  p_star += 1;
  break;
  } else {
  match_str += match_key;
  }
  p_end += 1;
  if(tblCur.end) //是否匹配到尾部
  {
  match = true;
  }
 } while (true);

 if(match) { //最大匹配
  arrMatch[arrLength] = { 
  key: match_str,
  begin: p_star - 1,
  end: p_end
  };
  arrLength += 1;
  p_star = p_end;
 }
 }
 return arrMatch;
}

以上就是整个关键词匹配系统的核心。这里很好的用到了js的语言特性,效率非常高。我用一篇50万字的《搜神记》来做测试,从中查找给定的300个成语,匹配的效果是1秒左右。重要的是,由于目标文本是一次遍历的,所以目标文本的长短对查询时间的影响几乎不计。对查询时间影响较大的是关键词的数量,目标文本的每个字都遍历一遍关键词,所以对查询有一定影响。

简单分析

看到上文估计你也纳闷,对每个字都遍历一遍所有关键词,就算有些关键词有部分相同,但是完全遍历也是挺耗时的呀。但js中对象的属性是使用哈希表来进行构建的,这种结构的数据跟单纯的数组遍历是有很大不同的,效率要比基于顺序的数组遍历高得多。可能有些同学对数据结构不太熟悉,这里我简单说一下哈希表的相关内容。 

首先看看数据的存储。

数据在内存的存储由两部分组成,一部分是值,另一部分是地址。把内存想象成一本新华字典,那字的解释就是值,而目录就是地址。字典里面是按拼音排序的,比如相同发音的“ni”就排在同一块,也就是说数组整齐排列在一块内存区域里面,这样的结构就是数组,你可以指定“ni” 1号,10号来访问。结构图如下:

1秒50万字!js实现关键词匹配

数组的优势是遍历简单,通过下标就能直接访问相应的数据了。但是它要增删某一项就非常困难。比如你要把第6项删掉,那第5项之后的数据都要向前移一个位置。如果你要删除第一位,整个数组都要移动,消耗非常大。

1秒50万字!js实现关键词匹配

为了解决数组增删的问题,链表就出现了。如果我们将值分成两部分,一部分用来储存原来的值,另一部分用来储存一个地址,这个地址指向另外一个同样的结构,以此类推就构成了一个链表。结构如下:

1秒50万字!js实现关键词匹配

从上图可以明显看出,对链表进行增删非常简单,只要把目标项和前一项的next改写就完成了。但是要查询某个项的值就非常困难了,你必须依次遍历才可以访问到目标位置。

1秒50万字!js实现关键词匹配

 为了整合这两种结构的优势,聪明如你一定想到了下面这种结构。

 1秒50万字!js实现关键词匹配

这种数据结构就是哈希表结构。数组里面存储链表的头地址,就可以形成一个二维数据表。至于数据如何分布,这个就是哈希算法,正规的翻译应该是散列算法。算法虽然有很多种,原理上都是通过一个函数对key进行求解,再根据求解得到的结果安放数据。也就是说key和实际地址之间形成了一个映射,所以这个时候我们不再以数组下标或者单纯的遍历来访问数组,而是以散列函数的反函数来定位数据。js中的对象就是一个哈希结构,比如我们定义一个obj,obj.name通过散列,他在内存中的位置可能是上图中的90,那我们想要操作obj.name的时候,底层就会自动帮我们通过哈希算法定位到90的位置,也就是说直接从数组的12项开始查找链表,而不是从0开始遍历整个内存块。

js中定义一个对象obj{key: value},key是被转换成字符串然后经过哈希处理得到一个内存地址,然后将值放入其中。这就可以理解为什么我们可以随意增删属性,也能理解为什么在js中还能为数组赋属性,而且数组也没有所谓的越界了。

在数据量较大的场合,哈希表具有非常明显的优势,因为它通过哈希算法减少了很多不必要的计算。所谓性能优化,其实就是让计算机少运算;最大的优化,就是不计算!

算法的优化

现在理解算法底层实现,回过头来就可以考虑对算法进行优化了。不过在优化前还是要强调一句:不要盲目追求性能!比如本案例中,我们最多就是5000字的匹配,那现有算法足矣,所有优化都是不必要的。之所以还来说说优化,就是为了提高自己对算法对程序的理解,而不是真的要去做那1ms的优化。

我们发现我们的关键词都没有一个字的,那我们按照一个字的单位进行关键词遍历显然就是一个浪费了。这里的优化就是预先统计关键词的最大最小长度,每次以最小长度为单位进行查找。比如说我测试用例的关键词是成语,最短都是4个字,那么我每次匹配都是4个字一起匹配,如果命中就继续深入查找到最大长度。也就是说我们最开始构造树的时候首先是以最小长度构建的,然后再逐字增加。

1秒50万字!js实现关键词匹配

 简单计算一下,按照我们的测试用例,300个成语,我们匹配一个词只需一次对比,而单字查询的话我们需要对比4次,而每次对比我们都要访问我们的树结构,这就是可避免的性能消耗。更重要的是,这里的对比并不是字符串对比,这里我们的关键字都是作为key存在的,效果就是 和key in obj一样的,都是对key进行哈希变换然后访问相应的地址!所以千万不要纠结对比一个字和对比4个字的差异,我们没对比字符串!

 关于多关键词的匹配就说到这里了,优化版代码我就不贴了,因为一般也用不到。

Javascript 相关文章推荐
JavaScript中this详解
Sep 01 Javascript
js闭包引起的事件注册问题介绍
Mar 29 Javascript
微信小程序手势操作之单触摸点与多触摸点
Mar 10 Javascript
Bootstrap 网格系统布局详解
Mar 19 Javascript
JavaScript实现一个空中避难的小游戏
Jun 06 Javascript
vue2.0s中eventBus实现兄弟组件通信的示例代码
Oct 25 Javascript
纯JS实现可用于页码更换的飞页特效示例
May 21 Javascript
React-router4路由监听的实现
Aug 07 Javascript
原生JS实现简单的无缝自动轮播效果
Sep 26 Javascript
JS实现从对象获取对象中单个键值的方法示例
Jun 05 Javascript
JS中如何轻松遍历对象属性的方式总结
Aug 06 Javascript
vue el-table实现自定义表头
Dec 11 Javascript
jQuery实现点击表格单元格就可以编辑内容的方法【测试可用】
Aug 01 #Javascript
JS实现数字格式千分位相互转换方法
Aug 01 #Javascript
AngularJS ng-controller 指令简单实例
Aug 01 #Javascript
js实现千分符和保留几位小数的简单实例
Aug 01 #Javascript
浅谈jQuery中ajaxPrefilter的应用
Aug 01 #Javascript
AngularJS基础 ng-cloak 指令简单示例
Aug 01 #Javascript
全面接触神奇的Bootstrap导航条实战篇
Aug 01 #Javascript
You might like
PHP strstr 函数判断字符串是否否存在的实例代码
2013/09/28 PHP
浅谈本地WAMP环境的搭建
2015/05/13 PHP
详解php魔术方法(Magic methods)的使用方法
2016/02/14 PHP
在jQuery 1.5中使用deferred对象的代码(翻译)
2011/03/10 Javascript
jquery Mobile入门—多页面切换示例学习
2013/01/08 Javascript
drag-and-drop实现图片浏览器预览
2015/08/06 Javascript
angularJS与bootstrap结合实现动态加载弹出提示内容
2015/10/16 Javascript
JS实现点击按钮获取页面高度的方法
2015/11/02 Javascript
javascript数组去重小结
2016/03/07 Javascript
JS中取二维数组中最大值的方法汇总
2016/04/17 Javascript
微信小程序 loading(加载中提示框)实例
2016/10/28 Javascript
js绑定事件和解绑事件
2017/04/27 Javascript
Angular2中select用法之设置默认值与事件详解
2017/05/07 Javascript
实战node静态文件服务器的示例代码
2018/03/08 Javascript
JS中数据结构与算法---排序算法(Sort Algorithm)实例详解
2019/06/17 Javascript
layui form表单提交之后重新加载数据表格的方法
2019/09/11 Javascript
理解Proxy及使用Proxy实现vue数据双向绑定操作
2020/07/18 Javascript
vue 页面回退mounted函数不执行的解决方案
2020/07/26 Javascript
[40:06]DOTA2亚洲邀请赛 4.3 突围赛 Liquid vs VGJ.T 第一场
2018/04/04 DOTA
疯狂上涨的Python 开发者应从2.x还是3.x着手?
2017/11/16 Python
对Python3中的input函数详解
2018/04/22 Python
python 地图经纬度转换、纠偏的实例代码
2018/08/06 Python
python求最大值,不使用内置函数的实现方法
2019/07/09 Python
Pycharm-community-2020.2.3 社区版安装教程图文详解
2020/12/08 Python
基于Html5实现的语音搜索功能
2019/05/13 HTML / CSS
亚洲领先的旅游体验市场:Voyagin
2019/11/23 全球购物
考博自荐信
2013/10/25 职场文书
信息管理专业推荐信
2013/10/29 职场文书
给导游的表扬信
2014/01/10 职场文书
乡镇消防工作实施方案
2014/03/27 职场文书
《小猪家的桃花树》教学反思
2014/04/11 职场文书
检查机关领导群众路线教育实践活动个人整改措施
2014/10/28 职场文书
工程安全生产协议书
2014/11/21 职场文书
心灵捕手观后感
2015/06/02 职场文书
中学语文教学反思
2016/02/16 职场文书
想创业成功,需要掌握这些要点
2019/12/06 职场文书