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中创建类/对象的几种方法总结
Nov 29 Javascript
不到30行JS代码实现Excel表格的方法
Nov 15 Javascript
jQuery实现感应鼠标动画效果自动伸长的输入框实例
Feb 24 Javascript
移动Web中图片自适应的两种JavaScript解决方法
Jun 18 Javascript
JavaScript实现九九乘法表的简单实例
Jun 07 Javascript
jsp 网站引入外部css或者js失效问题解决
Oct 31 Javascript
wap手机端解决返回上一页的js实例
Dec 08 Javascript
JS跳转手机站url的若干注意事项
Oct 18 Javascript
js 提取某()特殊字符串长度的实例
Dec 06 Javascript
利用jqprint插件打印页面内容的实现方法
Jan 09 Javascript
PHP实现基于Redis的MessageQueue队列封装操作示例
Feb 02 Javascript
vue中实现点击变成全屏的多种方法
Sep 27 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+FLASH实现上传文件进度条相关文件 下载
2007/07/21 PHP
php扩展ZF――Validate扩展
2008/01/10 PHP
收集的二十一个实用便利的PHP函数代码
2010/04/22 PHP
php错误提示failed to open stream: HTTP request failed!的完美解决方法
2011/06/06 PHP
PHP带节点操作的无限分类实现方法详解
2016/11/09 PHP
PHP判断密码强度的方法详解
2017/05/26 PHP
实例解析php的数据类型
2018/10/24 PHP
从阿里妈妈发现的几个不错的表单验证函数
2007/09/21 Javascript
父节点获取子节点的字符串示例代码
2014/02/26 Javascript
javascript制作的网页侧边弹出框思路及实现代码
2014/05/21 Javascript
Javascript学习笔记之 对象篇(三) : hasOwnProperty
2014/06/24 Javascript
js使用onmousemove和onmouseout获取鼠标坐标的方法
2015/03/31 Javascript
纯JS代码实现气泡效果
2016/05/04 Javascript
jQuery模仿京东/天猫商品左侧分类导航菜单效果
2016/06/29 Javascript
JS跨域交互(jQuery+php)之jsonp使用心得
2016/07/01 Javascript
浅谈JS继承_借用构造函数 &amp; 组合式继承
2016/08/16 Javascript
AngularJS 自定义指令详解及示例代码
2016/08/17 Javascript
jQ处理xml文件和xml字符串的方法(详解)
2016/11/22 Javascript
详解ES6中的代理模式——Proxy
2018/01/08 Javascript
json数据传到前台并解析展示成列表的方法
2018/08/06 Javascript
vue2.0+SVG实现音乐播放圆形进度条组件
2019/09/21 Javascript
vue输入节流,避免实时请求接口的实例代码
2019/10/30 Javascript
微信小程序scroll-view隐藏滚动条的方法详解
2020/03/25 Javascript
json_decode 索引为数字时自动排序问题解决方法
2020/03/28 Javascript
浏览器JavaScript调试功能无法使用解决方案
2020/09/18 Javascript
python连接mysql数据库示例(做增删改操作)
2013/12/31 Python
Python中处理unchecked未捕获异常实例
2015/01/17 Python
机器学习10大经典算法详解
2017/12/07 Python
解决Django Haystack全文检索为空的问题
2020/05/19 Python
详解Python中的路径问题
2020/09/02 Python
CSS3弹性盒模型开发笔记(一)
2016/04/26 HTML / CSS
AmazeUI底部导航栏与分享按钮的示例代码
2020/08/18 HTML / CSS
非功能性需求都包括哪些方面
2013/10/29 面试题
九年级数学教学反思
2014/02/02 职场文书
护理毕业生自我鉴定
2014/02/11 职场文书
幼儿园教研工作总结2015
2015/05/12 职场文书