安全开发第二讲-如何实现敏感词组的快速匹配
敏感词词组的例子模式
今天&早上&吃饭
词组模式可以将句子匹配词组化,从而减少纯句子匹配带来的漏报
设待检测文本的大小为分词之后存在500个词语
假如你手上有百万级的检测词组库,你能想到哪些方案快速检测(1秒内响应)?
1、set集合
首先我们来想一想集合是否可行,首先我们可以将词组通过&拆分,然后形成词语,每个词组是由多个词语构成的
AA&BB&CC => AA BB CC
拆分之后的词语对象应该至少包含两个属性:词语名+词语所在的词组id列表,这样的对应关系需要额外的结构来存储。然后百万级的词库将产生百万级的对象(有多少个词就会产生多少个这样的对象),然后我们把这些词语全部存储到一个set集合中。我们知道,在Java8中,HashSet是基于HashMap实现的,而HashMap是基于链表+红黑树实现的,初始时,HashMap内部是链表的结构,当达到一定阈值的时候,就会转成红黑树来存储节点。同时,HashMap为了减少碰撞率,需要通过荷载因子进行扩容(即达到多少的荷载就进行一次扩容),当HashMap中存储的节点越多,扩容带来的影响就越大。在我们的场景中,如果让一个HashMap去存储百万级的对象,暂且不考虑是否会出现OOM或者栈溢出。光是每次扩容带来的影响就很头大了。
集合的平均查找时间复杂度是O(Logn),对于百万级,也就是说一个词语需要查询6-7次,然后再从词语名->词语所在的词组id列表的对应关系中取出(一般使用hashMap存储),也需要6-7次查询。也就是说,一个词语需要12-14次左右的查询。一篇文章大约一共需要12*500=6000次匹配,存储方面需要使用HashSet存储词,用HashMap存储词语和其所在id列表对应关系。
看起来这样的结果却是还是可以令人接受的,那么你还可以提出别的方法吗?
2、Trie树
和上面一样,词组需要进行拆分
AA&BB&CC&AAB => AA BB CC AAB
trie树的每个节点都是一个字儿,对于 AA 和 AAB这两个词,最终的结构将是 root->A->A->B,其中在B和第二个A里面会标记为词语结束节点。每个处于结束位置的节点需要指定当前词组的id,从而形成所属的id列表。在百万级的词库下,我们需要拆分出每个词,也就是节点数大约是3*n(n为词库的大小),不需要额外的其他存储。从查询角度出发,对于一个词,只需要拆分为字序,然后按照顺序匹配即可,如果匹配不到,说明该词不在当前Trie树中,如果匹配结束,但是末尾节点在当前Trie树中不是结尾字符,那么也判定为结果不存在,时间复杂度为O(e),e为词语的长度。可见,Trie树的时间复杂度非常低,完全是依靠空间换时间的方法,在我们的开发应用环境中,内存等物理资源往往都是比较大的,节约时间,提高效率是每一个api提供者必要的修行。
以java为例,我们可以使用apache的包来实现这样的功能
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
这个包里面主要是一些Java容器的衍生实现,包含Trie树的实现,Trie树的实现并不复杂,但是有包可用何乐而不为?自己实现Trie树,无外乎包含建树,增加节点,删除节点等方法。
关于敏感词词库修改同步的问题,只需要写一个CAS定期轮询数据库即可,也就是在固定的时间内定期去重建这棵Trie树就行了,如何规避由于重建树,同时又有大量的查询请求,从而造成不一致的情况?解决方案就是不考虑这样的场景,在敏感词检测的场景中,由于对检测结果(相对于旧词库)的检测时效性要求不高,所以我们完全可以忽略。