前言
最近项目需要用到敏感词过滤功能,最开始想的是使用正则匹配和mysql存储敏感词来对敏感词来进行过滤操作,但是这两种方法都感觉不好。大家都知道正则性能问题一直都是一个很大的问题,而使用mysql的话虽然可以实现,但是给数据库增加了额外的压力。后面经过Google了解到DFA(有穷自动机)算法可以解决我的问题。
什么是DFA算法?
DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。简单点说就是,它是是通过event和当前的state得到下一个state,即event+state=nextstate。理解为系统中有多个节点,通过传递进入的event,来确定走哪个路由至另一个节点,而节点是有限的。
说了这么多是不是还是一脸懵逼?没关系,下面我们直接来撸代码,里面都有进行注释。如果还是不懂的话,可以去google搜索dfa算法的详细介绍。
算法DEMO
class Sensitive
{
private static $instance = null;
/**
* 替换符号
* @var string
*/
private static $replaceSymbol = "*";
/**
* 敏感词树
* @var array
*/
private static $sensitiveWordTree = [];
private function __construct(){}
/**
* 获取实例
*/
public static function getInstance()
{
if (!(self::$instance instanceof Sensitive)) {
return self::$instance = new self;
}
return self::$instance;
}
/**
* 添加敏感词,组成树结构。
* 例如敏感词为:傻子是傻帽,白痴,傻蛋 这组词组成如下结构。
* [
* [傻] => [
* [子]=>[
* [是]=>[
* [傻]=>[
* [帽]=>[false]
* ]
* ]
* ],
* [蛋]=>[false]
* ],
* [白]=>[
* [痴]=>[false]
* ]
* ]
* @param $file_path 敏感词库文件路径
*/
public static function addSensitiveWords(string $file_path) :void
{
foreach (self::readFile($file_path) as $words) {
$len = mb_strlen($words);
$treeArr = &self::$sensitiveWordTree;
for ($i = 0; $i < $len; $i++) {
$word = mb_substr($words, $i, 1);
//敏感词树结尾记录状态为false;
$treeArr = &$treeArr[$word] ?? $treeArr = false;
}
}
}
/**
* 执行过滤
* @param string $txt
* @return string
*/
public static function execFilter(string $txt) :string
{
$wordList = self::searchWords($txt);
if(empty($wordList))
return $txt;
return strtr($txt,$wordList);
}
/**
* 搜索敏感词
* @param string $txt
* @return array
*/
private static function searchWords(string $txt) :array
{
$txtLength = mb_strlen($txt);
$wordList = [];
for($i = 0; $i < $txtLength; $i++){
//检查字符是否存在敏感词树内,传入检查文本、搜索开始位置、文本长度
$len = self::checkWordTree($txt,$i,$txtLength);
//存在敏感词,进行字符替换。
if($len > 0){
//搜索出来的敏感词
$word = mb_substr($txt,$i,$len);
$wordList[$word] = str_repeat(self::$replaceSymbol,$len);
}
}
return $wordList;
}
/**
* 检查敏感词树是否合法
* @param string $txt 检查文本
* @param int $index 搜索文本位置索引
* @param int $txtLength 文本长度
* @return int 返回不合法字符个数
*/
private static function checkWordTree(string $txt, int $index, int $txtLength) :int
{
$treeArr = &self::$sensitiveWordTree;
$wordLength = 0;//敏感字符个数
$flag = false;
for($i = $index; $i < $txtLength; $i++){
$txtWord = mb_substr($txt,$i,1); //截取需要检测的文本,和词库进行比对
//如果搜索字不存在词库中直接停止循环。
if(!isset($treeArr[$txtWord])) break;
if($treeArr[$txtWord] !== false){//检测还未到底
$treeArr = &$treeArr[$txtWord]; //继续搜索下一层tree
}else{
$flag = true;
}
$wordLength++;
}
//没有检测到敏感词,初始化字符长度
$flag ?: $wordLength = 0;
return $wordLength;
}
/**
* 读取文件内容
* @param string $file_path
* @return Generator
*/
private static function readFile(string $file_path) :Generator
{
$handle = fopen($file_path, 'r');
while (!feof($handle)) {
yield trim(fgets($handle));
}
fclose($handle);
}
private function __clone()
{
throw new \Exception("clone instance failed!");
}
private function __wakeup()
{
throw new \Exception("unserialize instance failed!");
}
}
测试
require './Sensitive.php';
$instance = Sensitive::getInstance();
$instance->addSensitiveWords('./word.txt');//引入你的敏感词库文件
$txt = "我擦,——————————你麻痹的——————,尼玛的,你个傻逼";//需要过滤的文本
echo $instance->execFilter($txt);
测试结果
参考博客
https://blog.csdn.net/qq_36827957/article/details/74357283