参考地址

DFA算法原理:https://blog.csdn.net/imVainiycos/article/details/123234477

DFA算法实现敏感词过滤:https://blog.csdn.net/2301_79957017/article/details/135248993

AC自动机原理:https://blog.csdn.net/bestsort/article/details/82947639

HuTool-DFA算法:https://hutool.cn/docs/#/dfa/DFA%E6%9F%A5%E6%89%BE

1. 什么是DFA算法

1.1. 简介

有穷:状态以及事件的数量都是可穷举的。

DFA算法,即Deterministic Finite Automaton算法,翻译成中文就是确定有穷自动机算法。

1.2. 特征

有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。

用通俗易懂的话来解释,就是将数据库中的敏感词进行建立树结构。

1.3. 关键词链(Keyword Chains)

关键词链是DFA的核心。想象一下,一个巨大的城堡,其中每个房间都是一个字典,门上都标有某个字符。当你跟着这些字符去下一个房间,最终可能会找到一个标记为终点的房间,这就表示你找到了一个关键词(即最后一个字符是叶子节点,只有碰到叶子节点才算一次过滤)。

1.4. DFA算法模型

1.4.1. 示例1

模型输入文本:匹配关键词;匹配算法;信息抽取。

技术分享-DFA算法实现敏感词过滤_敏感词过滤

state_event_dict = { 
    "匹": { 
        "配": { 
            "算": { 
                "法": { 
                    "is_end": True
                },
                "is_end": False
            },
            "关": { 
                "键": { 
                    "词": { 
                        "is_end": True
                    },
                    "is_end": False
                },
                "is_end": False
            },
            "is_end": False
        },
        "is_end": False
    },
    "信": { 
        "息": { 
            "抽": { 
                "取": { 
                    "is_end": True
                },
                "is_end": False
            },
            "is_end": False
        },
        "is_end": False
    }
}

1.4.2. 示例2

构建算法树文本:今天;今天很好;今天真烦。

简单来说就是进行穷举操作,并且利用Map特性,逐字穷举细分敏感词文本。


技术分享-DFA算法实现敏感词过滤_List_02

1.5. DFA算法的基本步骤:

1.5.1. 构建敏感词字典树

将所有敏感词按字符顺序构建成一个字典树,也称为前缀树。每个节点代表一个字符,从根节点到叶子节点的路径表示一个敏感词。

1.5.2. 构建状态转换表

从根节点开始,按照每个字符的转移情况,构建状态转换表。在状态转换表中,每个状态代表当前匹配到的字符串前缀,表格中的每一项表示在当前状态下,接受某个字符后转移到的下一个状态。

1.5.3. 匹配过程

从文本的开头开始,逐个字符地读取文本,并根据状态转换表找到对应的下一个状态。如果找不到对应的状态,表示当前位置不是敏感词的开头,需要从下一个字符重新开始匹配。如果当前状态为敏感词的终止状态,说明匹配到一个敏感词,可以记录下来或进行相应的处理。

2. 优点

  • 高效性:DFA算法能够在线性时间内对文本进行匹配,具有较快的匹配速度。
  • 空间效率:DFA算法只需构建一次有限状态机,在之后的匹配过程中不需要额外的存储空间,相比其他算法更加节省内存。
  • 灵活性:DFA算法可以根据需求扩展,支持添加、删除敏感词等操作,便于维护和管理敏感词库。

3. 缺点

  • 构建复杂性:尽管DFA算法在匹配过程中效率高,但构建DFA有时需要耗费较多的时间和计算资源。特别是对于大规模的敏感词库,构建DFA可能会变得复杂且耗时。
  • 更新困难性:一旦DFA构建完成,对敏感词库的更新可能会比较困难。由于DFA是基于有限状态机的,添加、删除敏感词可能需要重新构建整个DFA,导致更新操作的复杂性和开销。
  • 不适用于动态更新:对于需要频繁动态更新敏感词库的场景,DFA算法可能不太适用,因为频繁的更新操作会带来较大的性能损耗和复杂度。

4. 实际应用

4.1. 实现步骤文字描述

  1. 定义敏感词接口,提供敏感词增删查改等方法;
  2. 定义DFA敏感词词树帮助类,提供将敏感词列表构建为敏感词词树方法;
  3. 定义敏感词过滤帮助类,提供根据敏感词查询词数判断文本敏感词内容方法。

4.2. 自定义实现

4.2.1. 敏感词接口

对外提供增删查改方法

/**
 * 敏感词Controller
*/
public class SensitiveWordsController {
    @Autowired
    private SensitiveWordsService sensitiveWordsService;

    /**
     * 校验敏感词
     */
    @PostMapping("/check")
    public ResultEntity<List<String>> check(@RequestBody String text) {
        try {
            List<String> checkResult = sensitiveWordsService.checkText(text);
            return ResultEntity.success(checkResult);
        } catch (Exception e) {
            return ResultEntity.failed(e.getMessage());
        }
    }
}

4.2.2. 敏感词词树工具类

提供将敏感词列表构建为敏感词词树方法

import java.util.HashMap;
import java.util.Map;

/**
 * @author: fanzhenxi
 * @description 敏感词词树生成器
 */
public class TreeGenerator {

    /**
     * 将指定的词分成字构建到一棵树中。
     *
     * @param tree 树
     * @param sensitiveWord 敏感词词
     * @return 
     */
    public static void addWord2Tree(Map<String, Map> tree, String sensitiveWord) {
        addWord2Tree(tree, sensitiveWord, 0);
    }

    private static Map<String, Map> addWord2Tree(Map<String, Map> tree,
                                                 String word, int index) {
        if (index == word.length()) {
            tree.put(Finder.TREE_END_KEY, generateWordMap(word));
            return tree;
        }
        String next = word.substring(index, index + 1);
        Map<String, Map> subTree = tree.get(next);
        if (subTree == null) {
            subTree = new HashMap<String, Map>();
        }
        tree.put(next, addWord2Tree(subTree, word, index + 1));
        return tree;
    }

    private static Map<String, Object> generateWordMap(String word) {
        Map<String, Object> wordMap = new HashMap<String, Object>();
        wordMap.put(Finder.WORD_VALUE, word);
        wordMap.put(Finder.WORD_LENGTH, word.length());
        return wordMap;
    }
}

4.2.3. 敏感词过滤帮助类

提供根据敏感词查询词数判断文本敏感词内容方法。

/**
 * @author: fanzhenxi
 * @description 敏感词校验
 */
public class TextAnalysis {

	/**
	 * 分析文本,返回搜寻到的所有敏感字
	 * 
	 * @author hymer
	 * @param tree
	 *            敏感字树
	 * @param text
	 *            待分析的文本
	 * @return
	 */
	public Set<String> analysis(Map<String, Map> tree, String text) {
		Set<String> words = new LinkedHashSet<String>();
		if (text != null && text.trim().length() > 0) {
			analysis(tree, text, words);
		}
		return words;
	}

    private void analysis(Map<String, Map> tree, String text, Set<String> words) {
		int index = 0;
		while (index < text.length()) {
			findWord(tree, text, index, words);
			index++;
		}
	}

	private void findWord(Map<String, Map> tree, String text, int index,
			Set<String> words) {
		Map<String, Map> subTree = tree.get(text.substring(index, index + 1));
		if (subTree != null) {
			Map<String, Object> end = subTree.get(Finder.TREE_END_KEY);
			if (end != null) {
				words.add((String) end.get(Finder.WORD_VALUE));
			}
			if ((index + 1) < text.length()
					&& (end == null || subTree.size() > 1)) {
				findWord(subTree, text, index + 1, words);
			}
		}
	}

	/**
	 * 替换文本中的敏感词
	 * 
	 * @param tree
	 *            敏感字树
	 * @param text
	 *            待分析的文本
	 * @param replacement
	 *            替换字符
	 * @return
	 */
	public String replace(Map<String, Map> tree, String text,
			Character replacement) {
		if (replacement == null) {
			replacement = Finder.DEFAULT_REPLACEMENT;
		}
		if (text != null && text.trim().length() > 0) {
			StringBuffer sb = new StringBuffer("");
			replace(tree, text, 0, replacement, sb);
			return sb.toString();
		}
		return text;
	}

	private void replace(Map<String, Map> tree, String text, int index,
			char replacement, StringBuffer sb) {
		int last = 0;
		int textLen = text.length();
		while (index < textLen) {
			String tmp = text.substring(index, index + 1);
			String nexts = text.substring(index);
			String word = "";
			word = findMaxWord(tree, nexts, 0, word);
			if (!"".equals(word)) {
				int replaceLen = 0;
				int wordLen = word.length();
				if (index >= last) {
					replaceLen = wordLen;
				} else {
					replaceLen = index + wordLen - last;
				}
				while (replaceLen > 0) {
					sb.append(replacement);
					replaceLen--;
				}
				last = index + wordLen;
			} else {
				if (index >= last) {
					sb.append(tmp);
				}
			}
			index++;
		}
	}

}

4.3. 通过第三方工具类实现

使用HuTool第三方工具类实现

4.3.1. 引入依赖

<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.27</version>
</dependency>

4.3.2. 构建敏感词树

/**
 * 获取所有敏感词
 * 
 * @return 敏感词文字集合
 */
public static List<String> getAllWords(){
    return Arrays.asList("大傻","大傻子","傻子","傻");
}

/**
 * 构建敏感词树 引用HuTool工具类
 * 
 * @return 敏感词词树
 */
public static WordTree getWordTree(){
    WordTree wordTree = new WordTree();
    // 构建敏感词树
    wordTree.addWords(getAllWords());
    return wordTree;
}

wordTree值参考:

{
    "大": {
        "傻": {
            "子": {
            }
        }
    },
    "傻": {
        "子": {
        }
    }
}

4.3.3. 校验文本敏感词

// 获得第一个匹配的关键字
// 返回值参考:null
FoundWord foundWord = wordTree.matchWord("这是没有敏感词的文本");

// 找出所有匹配的关键字
// 返回值参考:[{"word":"傻","foundWord":"傻","startIndex":0,"endIndex":0}]
List<FoundWord> foundWords = wordTree.matchAllWords("傻-这是有敏感词的文本");

4.3.4. 代码示例

public static void testMap() {
    // 创建DFA算法对象
    WordTree wordTree = new WordTree();
    // 需要校验的文本
    String text = "你妈";
    // 获取敏感词列表
    List<String> sensitiveWords = new ArrayList<>(Arrays.asList("你妈", "你爸", "你爷爷", "你奶奶", "你姥姥", "你姥爷", "你大娘", "你二娘"));
    // 封装敏感词词树
    wordTree.addWords(sensitiveWords);
    // 校验文本,获取所有敏感词
    List<String> sensitiveWords = wordTree.matchAll(text);
    // System.out.println(wordTree.matchAllWords(text));
    if (CollectionUtil.isEmpty(sensitiveWords)) {
        System.out.print("不包含敏感词");
    } else {
        System.out.println("包含敏感词:" + sensitiveWords);
    }
}

5. 有什么问题

5.1. 分布式一致性问题

问题:集群环境下如何保证敏感词词树的一致性。

一旦DFA构建完成,对敏感词库的更新可能会比较困难。由于DFA是基于有限状态机的,添加、删除敏感词可能需要重新构建整个DFA,导致更新操作的复杂性和开销。

问题解决方式:

  1. 定时同步:所有节点定时向数据库发起查询请求,获取最新敏感词列表随时更新词树;
  2. 使用分布式事件驱动:在敏感词发生变动时发送MQ,广播方式告诉其他节点,重置词树;
  3. 使用分布式缓存或数据库存储:存放到Redis缓存,敏感词发生变动时清除缓存,重置词树。
  4. 其他方式:Nacos缓存... 版本号...

6. 其他方式实现敏感词过滤

6.1. 正则表达式:

  • 定义敏感词正则表达式,但是它的匹配效率通常较低。

6.2. 手动校验:

  • 遍历配置的敏感词列表,使用String.contains判断敏感词是否存在于文本中。

6.3. AC自动机:

  • AC自动机是一种多模式匹配算法,可以同时匹配多个模式串。它在敏感词过滤中的性能通常比DFA算法稍逊一筹。在需要频繁更新敏感词库时使用AC自动机会更好一些。

技术分享-DFA算法实现敏感词过滤_敏感词_03

  • 这里以ashe为例
  • 我们先用ash匹配,到h了发现:这里ash是一个完整的模式串,好的ans++;
  • 然后找下一个e,可是ash后面没字母了,我们就跳到hfail指针指向的那个h继续找;
  • 没有匹配到,继续跳;
  • 结果当前的h指向的是根节点,又从根节点找,然而还是没有找到e;
  • 程序END。

技术分享-DFA算法实现敏感词过滤_敏感词_04

7. 总结

  • 虽然DFA算法在大多数情况下表现出色,但我们也要认识到它的一些局限性,如构建复杂性和更新困难性。这些问题可能需要额外的优化策略或结合其他技术来解决。
  • 此外,除了DFA算法,还有其他技术可以用于敏感词过滤,如正则表达式、AC自动机和手动校验等。在选择合适的技术时,需要综合考虑匹配效率、资源消耗、可维护性等因素。
  • 最后,对于实际应用中的敏感词过滤系统,我们建议根据具体需求和场景选择合适的技术,并不断优化和更新系统,以适应对应业务需求。