敏感词检测 评论敏感词过滤 DFA算法 AC自动机

DFA,全称 Deterministic Finite Automaton 即确定有穷自动机:从一个状态通过一系列的事件转换到另一个状态,即 state -> event -> state。

确定:状态以及引起状态转换的事件都是可确定的,不存在“意外”。
有穷:状态以及事件的数量都是可穷举的。
计算机操作系统中的进程状态与切换可以作为 DFA 算法的一种近似理解。如下图所示,其中椭圆表示状态,状态之间的连线表示事件,进程的状态以及事件都是可确定的,且都可以穷举。

<?php
namespace Module\Vendor\File;

use Module\Vendor\BaseVendor;

/**
 * Created by PhpStorm.
 * Auth: aoshi
 * Date: 2021/04/28
 * Time: 13:01
 * 敏感词过滤器
 * public 方法
 *      checkText:敏感词匹配检测
 *      addKeywords:添加敏感词
 *      clearkeywors:清空敏感词
 * 描述: 兼容单字符关键词、重复关键词、关键词包含关系 例如: 青 青年 中国 中国青年 青年 国青 年  单字符:清 年 重复关键词:青年  包含关系:中国青年  中国  青年
 * 支持无效字符剔除
 * 只要命中过滤词 每一次命中都会被反馈,反馈格式: 关键词 开始位置  结束位置
 */

/**
 *
 *
 *
 * */
class GreenFilterVender extends BaseVendor {

    protected $nodes;       //敏感词节点列表
    protected $invalidChar = array(',',';',';',',','-','"','“','、');     //无效字符

    public function __construct()
    {
        parent::__construct();
        $this->_initialize();
    }

    /**
     * 对象关键属性初始化
     * */
    protected function _initialize() {
        $this->nodes = $this->getKeywords();
    }

    /**
     * 敏感词匹配检测 兼容单个词过滤
     * @param   string      要搜索的关键词
     * @return  mixed       false:未命中 array:命中的关键词
     * */
    public function checkText($keyWord) {
        if(!$keyWord) {
            return false;
        }

        //过滤无效字符
        if($this->invalidChar) {
            $keyWord = str_replace($this->invalidChar,'',$keyWord);   
        }
        $charList = $this->get_chars($keyWord);
        $nodes = $this->nodes;
        $charNum = count($charList);
        $nodeKey = 0;
        $nodeLevel = 0;
        $foundList = array();
        for($key = 0;$key < $charNum;$key++) {
            $val = $charList[$key];
            if($nodeKey == 0) {     //命中首个敏感关键词
                if(in_array($val,array_keys($nodes[$nodeKey]))) {
                    $nodeKey = $nodes[$nodeKey][$val];
                }
            } else {
                if(in_array($val,array_keys($nodes[$nodeKey]))) {
                    if(in_array('',array_keys($nodes[$nodeKey]))) {     //单字符匹配  存在空字符key 就满足
                        $matchkey = $nodes[$nodeKey][''];
                        $foundList[] = array($matchkey['keyword'],$key-1,$key);
                    }
                    $matchkey = $nodes[$nodeKey][$val];
                    $nodeLevel = $matchkey['level'];     //当前判断深度 回滚需要
                    if(isset($matchkey['is_end']) && $matchkey['is_end'] == 1) {        //存在末尾结束符
                        $foundList[] = array($matchkey['keyword'],$key-count($matchkey['keyword']),$key+1);             //映射数据结构 array(命中次,开始位置,结束位置)
                        if(isset($matchkey['next_pointer']) && $matchkey['next_pointer']) {     //如果还有多余的字符
                            $nodeKey = $matchkey['next_pointer'];
                        } else {        //命中敏感词结束 回退到关键词开始的第二个位置
                            $key = $key - $nodeLevel;
                            $nodeLevel = 0;
                            $nodeKey = 0;
                        }
                    } else {
                        if(isset($matchkey['next_pointer']) && $matchkey['next_pointer']) {     //没有结束符 继续匹配
                            $nodeKey = $matchkey['next_pointer'];
                        }
                    }
                } else {
                    if(in_array('',array_keys($nodes[$nodeKey]))) {     //单字符匹配
                        $matchkey = $nodes[$nodeKey][''];
                        $foundList[] = array($matchkey['keyword'],$key-1,$key);
                    }
                    if($nodeLevel) {        //命中首字母 没有命中全部 回退多减1 (多了一次无效循环)
                        $key = $key - $nodeLevel - 1;
                    }
                    $nodeLevel = 0;
                    $nodeKey = 0;
                }
            }
        }

        return $foundList;
    }

    /**
     * 添加敏感词 可以考虑储存到redis
     * @param   array|string    关键词
     * @return  bool    false:失败  true:成功
     * */
    public function addKeywords($keyWords) {
        if(!is_array($keyWords)) {
            $keyWords = array($keyWords);
        }
        $nodes = $this->nodes;
//        is_end:终止符号 keyword:关键词  next_pointer:下一个指针  level:层次
        foreach($keyWords as $keyWord) {
            $charList = $this->get_chars($keyWord);
            $lastKey = count($charList) - 1;
            foreach($charList as $key => $val) {
                if($key == 0) {        //过滤词的第一个元素 添加一个元素
                    if(!in_array($val,array_keys($nodes[0]))) {
                        $nodeKey = count($nodes);
                        $nodes[$nodeKey] = array();
                        $nodes[0][$val] = $nodeKey;
                        if($key == $lastKey) {      //首字符不存在  单字符过滤
                            if(!isset($nodes[$nodeKey][''])) {      //创建单字符终结
                                $nodes[$nodeKey]['']['is_end'] = 1;
                                $nodes[$nodeKey]['']['level'] = $key;
                                $nodes[$nodeKey]['']['keyword'] = $keyWord;
                            }
                        }
                    } else {
                        $nodeKey = $nodes[0][$val];
                        if($key == $lastKey) {      //首字符存在 单字符过滤
                            if(!isset($nodes[$nodeKey][''])) {      //创建单字符终结
                                $nodes[$nodeKey]['']['is_end'] = 1;
                                $nodes[$nodeKey]['']['level'] = $key;
                                $nodes[$nodeKey]['']['keyword'] = $keyWord;
                            }
                        }
                    }
                } else {
                    if(!in_array($val,array_keys($nodes[$nodeKey]))) {      //新增字符
                        if($key == $lastKey) {      //此处为结束字符 创建结束标识&关键词结束  兼容字符截断  例如: 积分少了 积分
                            $nodes[$nodeKey][$val]['is_end'] = 1;
                            $nodes[$nodeKey][$val]['level'] = $key;
                            $nodes[$nodeKey][$val]['keyword'] = $keyWord;
                        } else {        //创建下一个节点 回写下个节点的指针标识
                            $newKey = count($nodes);        //下一个字段指针
                            $nodes[$newKey] = array();
                            $nodes[$nodeKey][$val]['next_pointer'] = $newKey;
                            $nodes[$nodeKey][$val]['level'] = $key;
                            $nodeKey = $newKey;         //指针后移
                        }
                    } else {        //字符存在
                        $nodeInfo = $nodes[$nodeKey][$val];
                        if($key == $lastKey) {      //字符被终结  创建结束标识&关键词结束
                            $nodes[$nodeKey][$val]['is_end'] = 1;
                            $nodes[$nodeKey][$val]['keyword'] = $keyWord;
                        } else {    //字符未被终结
                            if($nodeInfo['next_pointer']) { //指针后移
                                $nodeKey = $nodeInfo['next_pointer'];
                            } else {        //创建后续指针
                                $newKey = count($nodes);
                                $nodes[$newKey] = array();
                                $nodes[$nodeKey][$val]['next_pointer'] = $newKey;
                                $nodeKey = $newKey;
                            }
                        }
                    }

                }
            }
        }
        //todo 节点列表写入存储  redis mysql都可以
        $this->nodes = $nodes;

//        $this->_initialize();
    }

    /**
     * 清空敏感词
     * */
    public function clearkeywors() {
        $nodes = array(array());
        //todo 节点列表写入存储  redis mysql都可以
        $this->nodes = $nodes;
    }

    /**
     * 获取敏感词
     * @return  array       关键词节点
     * */
    protected function getKeywords() {
        //todo 节点列表获取  redis mysql都可以
        $nodes = array();
        if(!$nodes) {
            $nodes = array(array());
        }
        return $nodes;
    }


    /**
     * 将字符串转换成字符数组
     * @param   string      字符串
     * @return  array
    * */
    protected function get_chars($utf8_str){
        $s = $utf8_str;
        $len = strlen($s);
        if($len == 0) return array();
        $chars = array();
        for($i = 0;$i < $len;$i++){
            $c = $s[$i];
            $n = ord($c);
            if(($n >> 7) == 0){       //0xxx xxxx, asci, single
                $chars[] = $c;
            }
            else if(($n >> 4) == 15){     //1111 xxxx, first in four char
                if($i < $len - 3){
                    $chars[] = $c.$s[$i + 1].$s[$i + 2].$s[$i + 3];
                    $i += 3;
                }
            }
            else if(($n >> 5) == 7){  //111x xxxx, first in three char
                if($i < $len - 2){
                    $chars[] = $c.$s[$i + 1].$s[$i + 2];
                    $i += 2;
                }
            }
            else if(($n >> 6) == 3){  //11xx xxxx, first in two char
                if($i < $len - 1){
                    $chars[] = $c.$s[$i + 1];
                    $i++;
                }
            }
        }
        return $chars;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容