敏感词过滤--DFA算法及代码案例

我们应该都遇见过敏感词过滤,比如当我们输入一些包含暴力或者色情的文本,系统会阻止信息提交。敏感词过滤就是检查用户输入的内容有没有敏感词,检查之后有两个策略。

  1. 直接阻止信息保存,接口返回错误信息
  2. 允许信息保存,但是会把敏感词替换为***

不管是哪种策略,首先都得找到是否包含敏感词,这个判断一般是在服务端完成的。

要判断用户输入有无敏感词,首先要知道哪些词语是敏感词,也就是得有个敏感词库。比如现在敏感词库里记录了两个敏感词:“abc”和“cde”,如何判断用户输入的内容里是否包含这两个敏感词呢?最容易想到的方法是遍历敏感词库,依次判断输入内容是否有“abc”和“cde”。这种方法是可靠的,但是真实的敏感词库里存放的敏感词是非常多的,这时候遍历敏感词库的性能比较低,而且大部分情况下用户输入的内容都是不包含敏感词的,这时需要完全遍历敏感词库,也就是说大部分情况下遇到的都是这个算法复杂度最高的情形。

有没有一种比较高效的方法来查找敏感词呢?DFA算法可以做到。

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

敏感词过滤很适合用DFA算法,用户每次输入都是状态的切换,如果出现敏感词,既是终态,就可以结束判断。

我们把数组形式的敏感词整理为一个树状结构,准确的说是一个森林。

这样查找敏感词就变成了一个查找路径的问题,如果用户输入的内容中包含一个从根节点到叶子节点的完整路径,就说明包含敏感词。

算法实现逻辑是循环用户输入的字符串,依次查找每个字符是否出现在树的节点上,比如用户输入“打倒日本人”,从第一个字开始判断,“打”不在树的根节点上,进入下一步,“倒”也不在根节点上,进入下一步,“日”出现在了根节点上,这时状态切换,下一步的查找范围变为“日”的子节点;“本”出现在子节点中,状态再次切换为“本”的子节点;“人”出现在子节点中,并且为叶子节点,所以包含敏感词。

上述过程只是基本的思路,具体实现还要考虑中途退出时的状态重置,完整代码放在下面。(html代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    #text{
        width: 100%;
    }
    .output{
        margin: 30px 0;
        font-size: 16px;
        line-height: 28px;
    }
  </style>
</head>
<body>
<textarea id="text"></textarea>
<div>
  <button id="button">提交</button>
</div>
 
<div class="output"></div>
 
</body>
</html>
<script>
 
const dirtyTextArr = ['日本人', '日本鬼子', '日本男人']
console.log('敏感词个数 -> ', dirtyTextArr.length)
const dirtyRoot = new Map()
const buildMap = function (text) {
  let dirtyMap = dirtyRoot
  let origin = true
  if (!text) { // 敏感词不能为空
    return
  }
  for (let a = 0; a < text.length; a++) {
    if (origin && dirtyMap.get(text[a])) {
      // 当前字符已存在
      if (dirtyMap.get(text[a]) === true) {
        // 已经存在更短的敏感词
        return;
      }
      let oldDirtyMap = dirtyMap
      dirtyMap = dirtyMap.get(text[a])
      // 当前字符是最后一个字, 将dirtyMap置为true
      if (a === text.length - 1) {
        oldDirtyMap.set(text[a], true)
        // oldDirtyMap.
      }
    } else {
      origin = false
      if (a === text.length - 1) {
        // 最后一个字
        dirtyMap.set(text[a], true)
      } else {
        // console.log(text[a])
        dirtyMap.set(text[a], new Map())
        dirtyMap = dirtyMap.get(text[a])
      }
    }
  }
}
for (let b = 0; b < dirtyTextArr.length; b++) {
  buildMap(dirtyTextArr[b])
}
 
console.log('整理为map结构 -> ', dirtyRoot)
 
 
 
const dirtyCheckDFA = function (word){
  if (!word || !dirtyRoot) {
    return {dirty: false}
  }
  let dirty = false
  let dirtyMap = dirtyRoot
  let start = null
  let end = 0
  let origin = true
  for (let i = 0; i < word.length; i++) {
    // console.log(word[i])
    const aMap = dirtyMap.get(word[i])
    if (aMap) {
      // 在敏感词中匹配到该字符,且该字符为最后一个字
      if (aMap === true) {
        // 敏感词长度为1
        if (start === null) {
          start = i
        }
        end = i
        dirty = true
        break
      } else {
        // 在敏感词中匹配到该字符,但不是结尾
        if (origin) {
          start = i
        }
        // console.log(aMap)
        dirtyMap = aMap
        origin = false
        if (i === word.length - 1) {
          i = start
          origin = true
          dirtyMap = dirtyRoot
        }
      }
 
    } else {
      if (!origin) {
        // 在树结构中途退出,重新从进入字符的下一个字符检查
        i = start
      }
      origin = true
      start = null
      dirtyMap = dirtyRoot
    }
  }
  if (dirty) {
    return { dirty: true, start, end}
  }
  return {dirty: false}
}
 
const dirtyCheckLoop  = function (word) {
  if (!word) {
    return {dirty: false}
  }
  for (let a = 0; a < dirtyTextArr.length; a++) {
    const startIndex = word.indexOf(dirtyTextArr[a])
    if (startIndex !== -1) {
      return {dirty: true, start: startIndex, end: startIndex + dirtyTextArr[a].length - 1}
    }
  }
  return {dirty: false}
}
 
function submit() {
  console.log('**********************')
  const text = document.querySelector('#text').value
  const textLength = text.length
  // console.log('待检测字符串长度',)
 
  console.time('dirtyCheckDFA')
  const checkResult = JSON.stringify(dirtyCheckDFA(text))
  console.timeEnd('dirtyCheckDFA')
 
 
  console.time('dirtyCheckLoop')
  const checkResult2 = JSON.stringify(dirtyCheckLoop(text))
  console.timeEnd('dirtyCheckLoop')
  const str = `输入字符串长度:${textLength}<br>dirtyCheckDFA运行结果:${checkResult}<br>dirtyCheckLoop运行结果:${checkResult2}`
  document.querySelector('.output').innerHTML = str
}
document.querySelector('#button').addEventListener('click', submit)
 
 
</script>

  

上述例子中还比较了DFA算法和遍历词库算法的运行速度,由于敏感词库只有三个敏感词,所以DFA性能并不占优势,但是当敏感词库很大时,DFA优势会很明显,截图如下。

从catchadmin框架下找到以下代码:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
<?php
declare(strict_types=1);
 
use think\facade\Cache;
 
class Trie
{
    protected  $tree = [];
 
    protected $end = 'end';
 
    protected $sensitiveWord = '';
 
    protected $sensitiveWords = [];
 
    /**
     * add
     *
     * @time 2020年06月17日
     * @param string $word
     * @return $this
     */
    public function add(string $word)
    {
        $words = mb_str_split($word);
 
        $array = [];
 
        $len = count($words);
 
        $end = true;
        while ($len > 0) {
             if ($end) {
                 $array[] = [
                     $words[$len - 1] => ['end' => true],
                 ];
             } else {
                $latest = array_pop($array);
                $array[] = [
                    $words[$len-1] => $latest,
                ];
             }
             $end = false;
             $len--;
        }
 
        $this->tree = array_merge_recursive($this->tree, array_pop($array));
 
        return $this;
    }
 
    /**
     * 获取
     *
     * @time 2020年06月17日
     * @throws \Psr\SimpleCache\InvalidArgumentException
     * @return array|bool
     */
    public function getTries()
    {
        if (!empty($this->tree)) {
            return $this->tree;
        }
 
        return Cache::store('redis')->get("trie_tree");
    }
 
    /**
     * 获取敏感词
     *
     * @time 2020年06月17日
     * @param array $trieTree
     * @param string $content
     * @param bool $all
     * @return array|string
     */
    public function getSensitiveWords(array $trieTree, string $content, $all = true)
    {
        $words = mb_str_split($content);
        $len = count($words);
        for ($start = 0; $start < $len; $start++) {
            // 未搜索到
            if (!isset($trieTree[$words[$start]]))  {
                continue;
            }
            $node = $trieTree[$words[$start]];
            $this->sensitiveWord = $words[$start];
            // 从敏感词开始查找内容中是否又符合的
            for ($i = $start+1; $i< $len; $i++) {
                $node = $node[$words[$i]] ?? null;
                $this->sensitiveWord .= $words[$i];
                if (isset($node['end'])) {
                    if ($all) {
                        $this->sensitiveWords[] = $this->sensitiveWord;
                        $this->sensitiveWord = '';
                    } else {
                       break 2;
                    }
                }
                if (!$node) {
                    $this->sensitiveWord = '';
                    $start = $i-1;
                    break;
                }
            }
            // 防止内容比敏感词短 导致验证过去
            // 使用敏感词【傻子】校验【傻】这个词
            // 会提取【傻】
            // 再次判断是否是尾部
            if (!isset($node['end'])) {
                $this->sensitiveWord = '';
            }
        }
 
        return $all ? $this->sensitiveWords : $this->sensitiveWord;
    }
 
    /**
     * replace
     *
     * @time 2020年06月17日
     * @param $tree
     * @param string $content
     * @return string|string[]
     */
    public function replace($tree, string $content)
    {
        $sensitiveWords = $this->getSensitiveWords($tree, $content);
 
        $replace = [];
 
        foreach ($sensitiveWords as $word) {
            $replace[] = str_repeat('*', mb_strlen($word));
        }
 
        return str_replace($sensitiveWords, $replace, $content);
    }
 
    /**
     * cache
     *
     * @time 2020年06月17日
     */
    public function cached()
    {
       return Cache::store('redis')->set("trie_tree", $this->tree);
    }
}

  用法:

1
2
3
4
5
6
7
8
9
10
11
12
$trie = new Trie();
$trie->add("敏感词");
 
// 获取 trie tree
$trieTree = $trie->getTries();
 
//getSensitiveWords 方法可以获取到内容里面存在的敏感词汇
//$content为用户提交的内容
$trie->getSensitiveWords($trieTree, $content);
 
//replace 方法可以替换到内容里面存在的敏感词。
$trie->replace(['敏感词111','敏感词2222'], $content);

  

posted @   dreamboycx  阅读(475)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示