37DATA

企信组件库-新增敏感词过滤功能

前言:

        随着企信业务的增加,敏感词适用场景已经不局限于内部app、内部论坛发帖留言过滤。

        前段时间,有其他业务场景需要用到敏感词过滤功能。借此契机,我提议迁移对应的代码逻辑到企信组件库,封装成对外api服务,方便以后其他系统的调用,无需再次造轮子。

1、代码的搬运工

        Ctrl+C、Ctrl+V固然简单,但是在技术领域中我们知其然更要知其所以然。

        提到敏感词实现,大部分人第一印象就是实现文字匹配问题。但是当我们面对一个很大的敏感词库时,怎样高效地过滤到对应敏感词呢,数据库匹配?字符串函数匹配?这显然是不太行,在搬运代码的过程中,我发现了一个算法名称:DFA?

2、知其所以然

2.1、DFA是什么呢?

        参考网上的说法:DFA即Deterministic Finite Automaton,中文名为确定有穷自动机。

        其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。

Image

简单点说就是,它是是通过event和当前的state得到下一个state,即event+state=nextstate。理解为系统中有多个节点,通过传递进入的event,来确定走哪个路由至另一个节点,而节点是有限的。

       上面的说法看起来让我似懂非懂,所以可以通过下面的例子,试着深入理解一下:

       举例,有业务对“傻大个”和“傻大大的人”这两个敏感词过滤,DFA是怎样让实现的呢?

     (1)创建敏感词库SensitiveMap,组成二叉树结构:

Image

(2)用hash表构造为

{    "ending": false,    "傻": {        "ending": false,        "大": {            "ending": false,            "大": {                "ending": false,                "的": {                    "ending": false,                    "人": {                        "ending": true                    }                }            },            "个": {                "ending": true            }        }    }}

(3)基于SensitiveMap词库的DFA检索流程

Image

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);        else            self::$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;        }        // 找到相应key        if ($matchFlag <= 0) {            continue;        }        // 需匹配内容标志位往后移        $length = $length + $matchFlag - 1;    }    return false;}

3、优化迭代

        既然迁移到了企信组件库,为了以后更好兼顾到更多系统,所以也尝试在该组件做些优化:

        (1)增加多词库功能(每个系统一个词库,多个系统也可复用一个词库)【已处理】;

        (2)目前该组件还没有过滤无意义字符(' ', '&', '!', '!', '@', '#', '$', '¥', '*', '^', '%', '?', '?', '<', '>', "《", '》')的问题,后续如有需求用到,也会考虑加入过滤,避免干扰;        

        (3)自动填充敏感词库,增加用拼音或部分拼音识别敏感词。