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 相关文章推荐
jquery 合并内容相同的单元格(示例代码)
Dec 13 Javascript
Javascript排序算法之计数排序的实例
Apr 05 Javascript
innerHTML动态添加html代码和脚本兼容多个浏览器
Oct 11 Javascript
JavaScript编程的单例设计模讲解
Nov 10 Javascript
每天一篇javascript学习小结(基础知识)
Nov 10 Javascript
JavaScript预解析及相关技巧分析
Apr 21 Javascript
JS把内容动态插入到DIV的实现方法
Jul 19 Javascript
javascript读取文本节点方法小结
Dec 15 Javascript
微信小程序 向左滑动删除功能的实现
Mar 10 Javascript
对象不支持indexOf属性或方法的解决方法(必看)
May 28 Javascript
实现图片首尾平滑轮播(JS原生方法—节流)
Oct 17 Javascript
使用live-server快速搭建本地服务器+自动刷新的方法
Mar 09 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读取mysql中文数据出现乱码的解决方法
2013/08/16 PHP
PHP解析目录路径的3个函数总结
2014/11/18 PHP
PHP房贷计算器实例代码,等额本息,等额本金
2017/04/01 PHP
php curl操作API接口类完整示例
2019/05/21 PHP
讨论javascript(一)工厂方式 js面象对象的定义方法
2009/12/15 Javascript
javascript简单性能问题及学习笔记
2014/02/04 Javascript
Jquery的基本对象转换和文档加载用法实例
2015/02/25 Javascript
js设置document.domain实现跨域的注意点分析
2015/05/21 Javascript
JavaScript SweetAlert插件实现超酷消息警告框
2016/01/28 Javascript
jQuery取消特定的click事件
2016/02/29 Javascript
浅谈JavaScript中变量和函数声明的提升
2016/08/09 Javascript
js本地图片预览实现代码
2016/10/09 Javascript
jQuery实现点击某个div打开层,点击其他div关闭层实例分析(阻止冒泡)
2016/11/18 Javascript
Vue 2.0中生命周期与钩子函数的一些理解
2017/05/09 Javascript
VUE搭建手机商城心得和遇到的坑
2019/02/21 Javascript
jQuery利用cookie 实现本地收藏功能(不重复无需多次命名)
2019/11/07 jQuery
Threejs实现滴滴官网首页地球动画功能
2020/07/13 Javascript
[50:27]Secret vs VG 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/20 DOTA
python实现的防DDoS脚本
2011/02/08 Python
Python使用py2exe打包程序介绍
2014/11/20 Python
python识别文字(基于tesseract)代码实例
2019/08/24 Python
pip安装提示Twisted错误问题(Python3.6.4安装Twisted错误)
2020/05/09 Python
Python列表嵌套常见坑点及解决方案
2020/09/30 Python
Python常用断言函数实例汇总
2020/11/30 Python
荷兰多品牌网上鞋店:Stoute Schoenen
2017/08/24 全球购物
新加坡领先的时尚生活方式零售品牌:CHARLES & KEITH
2018/01/16 全球购物
美国最大婚纱连锁店运营商:David’s Bridal
2019/03/12 全球购物
娇韵诗俄罗斯官方网站:Clarins俄罗斯
2020/10/03 全球购物
某公司部分笔试题
2013/11/05 面试题
护理学专业推荐信
2013/12/03 职场文书
个人廉洁自律承诺书
2014/03/27 职场文书
2014矛盾纠纷排查调处工作总结
2014/12/09 职场文书
餐饮服务员岗位职责
2015/02/09 职场文书
校本培训个人总结
2015/02/28 职场文书
pytorch损失反向传播后梯度为none的问题
2021/05/12 Python
APP界面设计技巧和注意事项
2022/04/29 杂记