参考地址
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
模型输入文本:匹配关键词;匹配算法;信息抽取。
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特性,逐字穷举细分敏感词文本。
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. 实现步骤文字描述
- 定义敏感词接口,提供敏感词增删查改等方法;
- 定义DFA敏感词词树帮助类,提供将敏感词列表构建为敏感词词树方法;
- 定义敏感词过滤帮助类,提供根据敏感词查询词数判断文本敏感词内容方法。
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,导致更新操作的复杂性和开销。
问题解决方式:
- 定时同步:所有节点定时向数据库发起查询请求,获取最新敏感词列表随时更新词树;
- 使用分布式事件驱动:在敏感词发生变动时发送MQ,广播方式告诉其他节点,重置词树;
- 使用分布式缓存或数据库存储:存放到Redis缓存,敏感词发生变动时清除缓存,重置词树。
- 其他方式:Nacos缓存... 版本号...
6. 其他方式实现敏感词过滤
6.1. 正则表达式:
- 定义敏感词正则表达式,但是它的匹配效率通常较低。
6.2. 手动校验:
- 遍历配置的敏感词列表,使用String.contains判断敏感词是否存在于文本中。
6.3. AC自动机:
- AC自动机是一种多模式匹配算法,可以同时匹配多个模式串。它在敏感词过滤中的性能通常比DFA算法稍逊一筹。在需要频繁更新敏感词库时使用AC自动机会更好一些。
- 这里以ashe为例
- 我们先用ash匹配,到h了发现:这里ash是一个完整的模式串,好的ans++;
- 然后找下一个e,可是ash后面没字母了,我们就跳到hfail指针指向的那个h继续找;
- 没有匹配到,继续跳;
- 结果当前的h指向的是根节点,又从根节点找,然而还是没有找到e;
- 程序END。
7. 总结
- 虽然DFA算法在大多数情况下表现出色,但我们也要认识到它的一些局限性,如构建复杂性和更新困难性。这些问题可能需要额外的优化策略或结合其他技术来解决。
- 此外,除了DFA算法,还有其他技术可以用于敏感词过滤,如正则表达式、AC自动机和手动校验等。在选择合适的技术时,需要综合考虑匹配效率、资源消耗、可维护性等因素。
- 最后,对于实际应用中的敏感词过滤系统,我们建议根据具体需求和场景选择合适的技术,并不断优化和更新系统,以适应对应业务需求。