1. 敏感词过滤:从业务痛点说起
做内容审核或者社交平台开发的朋友,肯定对敏感词过滤不陌生。用户发帖、评论、聊天,但凡涉及到用户生成内容(UGC)的地方,都得过这一关。我最早接触这需求,是在做一个社区论坛的时候,那会儿图省事,直接用了最“朴素”的方法:遍历。
具体来说,就是从数据库里把敏感词全捞出来,组成一个List<String>,然后用户每提交一段文本,我就用这段文本去跟列表里的每一个词做匹配。听起来是不是很简单?刚开始词库小,几十上百个词,感觉还行。但后来业务扩张,敏感词库蹭蹭往上涨,到了几千甚至上万个的时候,问题就来了。
最直观的感受就是“慢”。用户发个帖子,要转半天圈圈才能提交成功,体验极差。一查性能监控,CPU在匹配环节飙得老高。这还只是单次请求,想象一下高峰期的并发量,服务器根本扛不住。更头疼的是,这种遍历匹配的方法,对于“拆词”或者“变体”基本无能为力。比如敏感词是“今天”,用户写个“今 天”中间加个空格,或者用拼音“jintian”,甚至用形近字、谐音字,这套方法就完全失效了。我们当时就经常被用户用各种方式“绕过”,审核同事天天手动补漏,苦不堪言。
所以,一个高效、准确、能应对各种“花招”的敏感词过滤系统,就成了刚需。这就是为什么我们需要引入更专业的算法,而不是停留在简单的字符串查找上。在众多方案里,DFA(确定有穷自动机)算法以其在敏感词过滤场景下的优异表现,成为了很多开发者的首选。它核心解决的就是我上面提到的两个痛点:速度和准确性。接下来,我们就深入聊聊,怎么用Java把这个算法从理论落地成一套可用的系统。
2. DFA算法:化繁为简的树形智慧
DFA,全称Deterministic Finite Automaton,翻译过来叫“确定有穷自动机”。这名字听起来挺唬人,像是编译原理里的高深概念。但其实把它应用到敏感词过滤上,其思想非常直观,我们可以暂时忘掉那些状态转换的学术定义,用一个更形象的模型来理解它:一棵多叉树。
假设我们的敏感词库里有三个词:“今天”、“今天很好”、“今天真烦”。用DFA的思想来构建,过程是这样的:
- 我们把每个敏感词拆分成单个字符。
- 从树根开始,第一个字“今”作为根节点下的一个子节点。
- “今”后面是“天”,那么“天”就作为“今”这个节点的子节点。
- 对于“今天”这个词,到“天”这里就已经结束了,所以我们在“天”节点上打一个标记,比如
isEnd=true,表示从根到这儿走完了一条完整的敏感词路径。 - 对于“今天很好”,在“天”节点之后,它还有“很”这个子节点,接着是“好”,并在“好”节点标记
isEnd=true。 - 同理,“今天真烦”会形成“天” -> “真” -> “烦”的路径,在“烦”节点标记结束。
最终,这棵敏感词树看起来就像是一个层层展开的目录结构。它的魔力在于,无论你有1万个还是10万个敏感词,很多词都会有共同的前缀(比如都以“今天”开头),这些前缀在树里是共享的,这就极大地压缩了存储空间,避免了简单列表那种重复存储的浪费。
匹配过程就更体现它的高效了。当我们要检查“我觉得今天还行”这句话时:
- 我们从“我”开始,在树的第一层(根的子节点)找“我”,没找到,跳过。
- 接着是“觉”,也没找到,跳过。
- 直到“今”,在树的第一层找到了“今”节点。
- 然后看下一个字“天”,检查“今”节点的子节点里有没有“天”,有!并且我们发现“天”节点被标记了
isEnd=true。好,这意味着“今天”是一个完整的敏感词,命中! - 命中后,我们把“今天”替换成
**。接下来,匹配指针并不需要回溯到“天”之后的下一个字重新从树根开始,而是直接从“天”的下一个字“还”开始,重新从树根进行匹配。因为“还”不在根的子节点中,所以匹配结束。
整个过程,对输入文本只遍历了一次,就在这次遍历中完成了所有可能敏感词的探测和匹配。这种时间复杂度近似O(n)的效率,比起简单遍历的O(n*m)(n是文本长度,m是词库大小),简直是天壤之别。这就是DFA算法在敏感词过滤中无可替代的优势:一次扫描,全程搞定。
3. 核心实现:构建属于你的敏感词过滤器
理论懂了,接下来咱们动手实现。我会带你一步步搭建一个比基础版更健壮、更实用的DFA敏感词过滤器。我们不追求大而全的框架,而是聚焦核心,让你能透彻理解每一行代码的作用。
3.1 数据结构设计:用Map模拟树
在Java里,我们可以用嵌套的Map来完美地表示这棵敏感词树。Map的键(Key)是当前字符,值(Value)是另一个Map,代表后续的字符节点。同时,我们需要一个特殊的键来标记某个节点是否是某个敏感词的结尾。
import java.util.HashMap;
import java.util.Map;
/**
* 敏感词树构建器
*/
public class SensitiveWordTreeBuilder {
// 定义一个常量,作为敏感词结束的标记键
private static final String IS_END = "isEnd";
/**
* 将一组敏感词构建成DFA树
* @param sensitiveWords 敏感词数组
* @return 构建好的DFA树(根节点Map)
*/
public Map<String, Object> buildTree(String[] sensitiveWords) {
// 这是树的根节点,它是一个Map
Map<String, Object> rootNode = new HashMap<>();
for (String word : sensitiveWords) {
if (word == null || word.trim().isEmpty()) {
continue;
}
Map<String, Object> currentNode = rootNode;
char[] chars = word.toCharArray();
for (int i = 0; i < chars.length; i++) {
String currentChar = String.valueOf(chars[i]);
// 获取当前字符对应的子节点Map
Map<String, Object> childNode = (Map<String, Object>) currentNode.get(currentChar);
if (childNode == null) {
// 如果不存在,则创建一个新的节点(也是一个Map)

350

被折叠的 条评论
为什么被折叠?



