企信组件库-新增敏感词过滤功能
前言:
随着企信业务的增加,敏感词适用场景已经不局限于内部app、内部论坛发帖留言过滤。
前段时间,有其他业务场景需要用到敏感词过滤功能。借此契机,我提议迁移对应的代码逻辑到企信组件库,封装成对外api服务,方便以后其他系统的调用,无需再次造轮子。
1、代码的搬运工
Ctrl+C、Ctrl+V固然简单,但是在技术领域中我们知其然更要知其所以然。
提到敏感词实现,大部分人第一印象就是实现文字匹配问题。但是当我们面对一个很大的敏感词库时,怎样高效地过滤到对应敏感词呢,数据库匹配?字符串函数匹配?这显然是不太行,在搬运代码的过程中,我发现了一个算法名称:DFA?
2、知其所以然
2.1、DFA是什么呢?
参考网上的说法:DFA即Deterministic Finite Automaton,中文名为确定有穷自动机。
其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。
简单点说就是,它是是通过event和当前的state得到下一个state,即event+state=nextstate。理解为系统中有多个节点,通过传递进入的event,来确定走哪个路由至另一个节点,而节点是有限的。
上面的说法看起来让我似懂非懂,所以可以通过下面的例子,试着深入理解一下:
举例,有业务对“傻大个”和“傻大大的人”这两个敏感词过滤,DFA是怎样让实现的呢?
(1)创建敏感词库SensitiveMap,组成二叉树结构:
(2)用hash表构造为
{"ending": false,"傻": {"ending": false,"大": {"ending": false,"大": {"ending": false,"的": {"ending": false,"人": {"ending": true}}},"个": {"ending": true}}}}
(3)基于SensitiveMap词库的DFA检索流程
2.2、核心代码实现
看了上面基本的原理实现后,再看下核心代码
// 构建词库
/*** 初始化词库* @param int $wordsUpTime* @throws Exception*/public function init($wordsUpTime = 0){//初始化require_once __DIR__ . "/sensitive/SensitiveHelper.php";require_once __DIR__ . "/sensitive/HashMap.php";if (!(self::$instance instanceof SensitiveHelper)) {self::$instance = new SensitiveHelper($this->sysId);$cacheLog = CACHE_PATH . '/sensitive_words/';// 每个系统有一个缓存文件self::$CACHE_FILE = CACHE_PATH . '/sensitive_words/__sensitive_cache_tree_' . $this->sysId . '.dat';// 创建缓存目录if ($cacheLog && !file_exists(self::$CACHE_FILE)) {@mkdir($cacheLog, 0777, true);file_put_contents(self::$CACHE_FILE, '');}//设置缓存文件和缓存时长$cacheMoTime = filemtime(self::$CACHE_FILE);if (!$cacheMoTime || !$wordsUpTime || $cacheMoTime < $wordsUpTime)self::$instance->setCache(self::$CACHE_FILE, 0);elseself::$instance->setCache(self::$CACHE_FILE, (time() - $cacheMoTime + 99));self::$instance->setTree();}}
// 匹配逻辑
/*** 检测内容是否包含敏感词** @param $content* @return bool*/public function isLegal($content){$this->contentLength = mb_strlen($content, 'utf-8');for ($length = 0; $length < $this->contentLength; $length++) {$matchFlag = 0;$tempMap = $this->wordTree->hashTable;for ($i = $length; $i < $this->contentLength; $i++) {$keyChar = mb_substr($content, $i, 1, 'utf-8');// 获取指定节点树$nowMap = $this->wordTree->get($keyChar, $tempMap);// 不存在节点树,直接返回if (empty($nowMap))break;// 存在,则判断是否为最后一个$tempMap = $nowMap;// 找到相应key,偏移量+1$matchFlag++;// 如果为最后一个匹配规则,结束循环,返回匹配标识数if (false === $this->wordTree->get('ending', $nowMap))continue;return true;}// 找到相应keyif ($matchFlag <= 0) {continue;}// 需匹配内容标志位往后移$length = $length + $matchFlag - 1;}return false;}
3、优化迭代
既然迁移到了企信组件库,为了以后更好兼顾到更多系统,所以也尝试在该组件做些优化:
(1)增加多词库功能(每个系统一个词库,多个系统也可复用一个词库)【已处理】;
(2)目前该组件还没有过滤无意义字符(' ', '&', '!', '!', '@', '#', '$', '¥', '*', '^', '%', '?', '?', '<', '>', "《", '》')的问题,后续如有需求用到,也会考虑加入过滤,避免干扰;
(3)自动填充敏感词库,增加用拼音或部分拼音识别敏感词。