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 相关文章推荐
JS 无法通过W3C验证的处理方法
Mar 09 Javascript
javascript教程之不完整的继承(js原型链)
Jan 13 Javascript
jquery插件开发之实现google+圈子选择功能
Mar 10 Javascript
AngularJS基础教程之简单介绍
Sep 27 Javascript
改变checkbox默认选中状态及取值的实现代码
May 26 Javascript
js微信分享API
Oct 11 Javascript
纯原生js实现贪吃蛇游戏
Apr 16 Javascript
vue2.0之多页面的开发的示例
Jan 30 Javascript
JS中使用new Option()实现时间联动效果
Dec 10 Javascript
vue分页器组件编写方法详解
Jun 28 Javascript
JavaScript中EventBus实现对象之间通信
Oct 18 Javascript
解决vue中使用less/sass及使用中遇到无效的问题
Oct 24 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中的内存管理问题
2011/08/31 PHP
PHP格式化MYSQL返回float类型的方法
2016/03/30 PHP
详解Yii2高级版引入bootstrap.js的一个办法
2017/03/21 PHP
通过JAVAScript实现页面自适应
2007/01/19 Javascript
JQuery 国际象棋棋盘 实现代码
2009/06/26 Javascript
JS判断页面加载状态以及添加遮罩和缓冲动画的代码
2012/10/11 Javascript
ie支持function.bind()方法实现代码
2012/12/27 Javascript
javascript动态的改变IFrame的高度实现自动伸展
2013/10/12 Javascript
JavaScript中使用Substring删除字符串最后一个字符
2013/11/03 Javascript
iScroll.js 使用方法参考
2016/05/16 Javascript
javascript设计模式之module(模块)模式
2016/08/19 Javascript
JS for...in 遍历语句用法实例分析
2016/08/24 Javascript
vuejs+element UI点击编辑表格某一行时获取内容填入表单的示例
2018/10/31 Javascript
nodejs实现日志读取、日志查找及日志刷新的方法分析
2019/05/20 NodeJs
vue发送websocket请求和http post请求的实例代码
2019/07/11 Javascript
typescript nodejs 依赖注入实现方法代码详解
2019/07/21 NodeJs
微信小程序 scroll-view 实现锚点跳转功能
2019/12/12 Javascript
[15:15]教你分分钟做大人:狙击手
2014/10/30 DOTA
python简易远程控制单线程版
2018/06/20 Python
python爬虫模拟浏览器访问-User-Agent过程解析
2019/12/28 Python
Python socket服务常用操作代码实例
2020/06/22 Python
如何通过python计算圆周率PI
2020/11/11 Python
python 基于Apscheduler实现定时任务
2020/12/15 Python
意大利会呼吸的鞋:Geox健乐士
2017/02/12 全球购物
美国电力供应商店/电气批发商:USESI
2018/10/12 全球购物
大整数数相乘的问题
2012/07/22 面试题
班主任工作年限证明
2014/01/12 职场文书
会计专业导师推荐信
2014/03/08 职场文书
加强作风建设工作总结
2014/10/23 职场文书
2015年元旦联欢晚会活动总结
2014/11/28 职场文书
师德标兵事迹材料
2014/12/19 职场文书
大三学生英语考试作弊检讨书
2015/01/01 职场文书
幼儿园开学家长寄语(2016秋季)
2015/12/03 职场文书
《合作意向书》怎么写?
2019/08/20 职场文书
vue.js Router中嵌套路由的实用示例
2021/06/27 Vue.js
ubuntu下常用apt命令介绍
2022/06/05 Servers