敏感词过滤--DFA算法及代码案例
我们应该都遇见过敏感词过滤,比如当我们输入一些包含暴力或者色情的文本,系统会阻止信息提交。敏感词过滤就是检查用户输入的内容有没有敏感词,检查之后有两个策略。
- 直接阻止信息保存,接口返回错误信息
- 允许信息保存,但是会把敏感词替换为***
不管是哪种策略,首先都得找到是否包含敏感词,这个判断一般是在服务端完成的。
要判断用户输入有无敏感词,首先要知道哪些词语是敏感词,也就是得有个敏感词库。比如现在敏感词库里记录了两个敏感词:“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 ); |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)