灵感来自[嘉兴](https://segmentfault.com/a/1190000019137933《Trie树php实现敏感词过滤》)感谢分享。这篇文章主要是为上面增加了自己的理解,并增加了通过屏蔽级别灵活控制敏感词过滤。代码适用场景:1、特殊时期需要对某些敏感词进行大规模的敏感词检测。2.敏感词除了精确匹配还需要模糊匹配,比如sillylkajmelon。3、针对不同时期(如重大节假日),或不同级别的项目,对敏感词的校验严格程度不同,并进行进一步处理。实现逻辑采用前缀树/字典树算法,利用字符串的公共前缀来节省存储空间。例如当前的敏感词数组为:['fool','fool','fool']当要匹配的字符串中包含'fool'和'fool'时,下图字典树示例中红色边框为对应的结束节点。字典树如图所示:首先需要通过敏感词字典文件用敏感词初始化字典树,然后在字典树上搜索添加的字符串。步骤如下:1.从根节点开始查找。2、获取要查找的字符串的第一个字符,根据该字符选择对应的字符路径继续向下查找。3、字符串查找完成后,判断当前是否为敏感字符对应路径的终端节点。如果是,则意味着该字符串包含在字典树中。无论如何,这意味着不包含该字符串。4.如果要添加模糊匹配,可以在相应的字符路径判断逻辑中添加允许跳过的字符串长度判断。敏感词级别处理通过敏感词级别的验证,可以更灵活的控制屏蔽词的强度。只要序列中包含一级屏蔽词校验串中的屏蔽词,就全部被屏蔽。比如敏感词:“傻子”、“你是傻子吗?”-->"Areyou*laba*"publicfunctionindex(){$logic=newfilterWords();$str=$logic->filter('你傻吗?',1);echo'验证结果:'.$海峡;}验证结果:Areyou*labaji*whichsecondarymaskingwordverificationstring只要有n个字符以内的掩码词依次被掩码。比如敏感词:“fool”,屏蔽在2个字符以内。“你傻吗?”-->“你不是*la*吗?”“你傻吗?”-->“你傻吗?”publicfunctionindex(){$logic=newfilterWords();$str=$logic->filter('你傻吗?',2,2);echo'验证结果:'.$海峡;}验证结果:你是*la?*whichthree-levelmaskingwordcheckstring,只要整个单词匹配到maskingword,就会被屏蔽。比如敏感词:“傻瓜”。"你是傻子吗"-->"你是**吗""你是傻子吗"-->"你是傻子吗"publicfunctionindex(){$logic=newfilterWords();$str=$logic->filter('你是傻瓜吗',3);echo'验证结果:'.$海峡;}验证结果:Areyou**whichidea流程图:封装到工具类中:filterWords.phploadDataFormFile();}/***从文件Dictionary中加载敏感词*/protectedfunctionloadDataFormFile(){//这里可以修改为读取文件。一般敏感词都是文件形式,一行对应一个敏感词。//如果调用频繁,也可以通过缓存(redis,memcache)等方式处理,这里不做详细处理$arr=['Stupid','Fool',];//给这个节点添加敏感词foreach($arras$value){$this->addWords(trim($value));}}/***splittext*@param$str*@returnarray[]|false|string[]*/protectedfunctionsplitStr($str){//将字符串拆分为其组成字符//其中/u表示要按照unicode(utf-8)匹配(主要针对汉字等多字节字符),否则容易出现默认按照ascii码的乱码returnpreg_split("//u",$str,-1、PREG_SPLIT_NO_EMPTY);}/***添加敏感信息word到node*@param$words*/protectedfunctionaddWords($words){//1.拆分字典$wordArr=$this->splitStr($words);$curNode=&$this->dict;foreach($wordArras$char){如果(!isset($curNode)){$curNode[$char]=[];}$curNode=&$curNode[$char];}//将当前节点的全路径标记为“敏感”字"$curNode['end']++;}/***敏感字验证*@param$str;待验证字符串*@paramint$level;屏蔽词验证级别1-只要订单包含全部屏蔽;2-skipDistance中间的字符将被屏蔽;3-全词匹配将被屏蔽*@paramint$skipDistance;允许敏感词的最大距离跳过,比如stupidaaafool等*@parambool$isReplace;是否需要替换,如果不需要,返回是否有敏感词,否则返回替换后的字符串*@paramstring$replace;替换字符*@returnbool|string*/publicfunctionfilter($str,$level=1,$skipDistance=2,$isReplace=true,$replace='*'){//允许跳过的最大距离if($level==1){$maxDistance=strlen($str)+1;}elseif($level==2){$maxDistance=max($skipDistance,0)+1;}else{$maxDistance=2;}$strArr=$this->splitStr($str);$strLength=count($strArr);$isSensitive=false;for($i=0;$i<$strLength;$i++){//判断当前敏感词是否有对应节点$curChar=$strArr[$i];如果(!isset($this->dict[$curChar])){继续;}$isSensitive=true;//引用匹配的敏感词节点$curNode=&$this->dict[$curChar];$距离=0;$matchIndex=[$i];//是否匹配后续字符串匹配剩余敏感词for($j=$i+1;$j<$strLength&&$dist<$maxDistance;$j++){if(!isset($curNode[$strArr[$j]])){$dist++;继续;}//如果匹配到的话,存储对应字符的位置,方便后续替换敏感词$matchIndex[]=$j;//继续引用$curNode=&$curNode[$strArr[$j]];}//判断是否到了敏感词字典的末尾,如果是,则替换敏感词if(isset($curNode['end'])&&$isReplace){foreach($matchIndexas$index){$strArr[$index]=$replace;}$i=max($matchIndex);}}if($isReplace){return内爆('',$strArr);}else{返回$isSensitive;}}}
