在后端开发中,敏感词过滤是内容安全、评论风控、私聊校验、社区发帖等场景的标配能力。不管是电商、社交还是内容平台,一套高效、稳定、可扩展的敏感词过滤模块,都是后端工程师必须掌握的实用技能。
同时这也是面试高频考点:敏感词过滤为什么不用暴力匹配?
敏感词过滤核心目标:
一个合格的敏感词模块,至少要具备:
com.xxx.sensitive
├── SensitiveFilter.java # 核心过滤入口
├── TrieNode.java # 字典树节点
├── SensitiveWordConfig.java # 敏感词配置/加载
├── SensitiveCache.java # 缓存(本地/Redis)
└── SensitiveUtil.java # 工具类:替换、特殊字符清洗
Trie(前缀树/字典树)是敏感词过滤的标准方案,核心思想是用字符路径表示敏感词,共享前缀,大幅减少比较次数。
public class TrieNode {
// 子节点 key:字符 value:节点
private Map<Character, TrieNode> children;
// 是否是词的结尾
private boolean isEnd;
public TrieNode() {
this.children = new HashMap<>();
this.isEnd = false;
}
// getter/setter
}
// 批量插入敏感词
public void addSensitiveWord(String word) {
TrieNode node = root;
for (char c : word.toCharArray()) {
if (!node.getChildren().containsKey(c)) {
node.getChildren().put(c, new TrieNode());
}
node = node.getChildren().get(c);
}
node.setEnd(true);
}
public String filter(String text) {
if (text == null || text.isEmpty()) {
return text;
}
StringBuilder result = new StringBuilder();
int length = text.length();
int i = 0;
while (i < length) {
TrieNode node = root;
int j = i;
boolean hit = false;
// 沿着 Trie 树往下走
while (j < length && node.getChildren().containsKey(text.charAt(j))) {
node = node.getChildren().get(text.charAt(j));
if (node.isEnd()) {
hit = true;
break;
}
j++;
}
if (hit) {
// 命中敏感词,替换为 ***
result.append("***");
i = j + 1;
} else {
result.append(text.charAt(i));
i++;
}
}
return result.toString();
}
实际工程中,还会增加:
这道题几乎是后端面试必考题,下面给你一套标准且完整的回答逻辑。
暴力匹配就是:
String.contains() / indexOf伪代码类似:
for (String word : sensitiveList) {
if (text.contains(word)) {
return true;
}
}
比如敏感词:赌博、赌球、赌钱暴力匹配会重复匹配前缀“赌”三次。 而 Trie 树共享前缀,只匹配一次。
用户常输入:赌 博、赌~博、du博暴力匹配需要对每个词做清洗、替换、再匹配,复杂度再次飙升。 Trie 树可以在一次遍历中完成清洗 + 匹配。
评论接口、发帖接口、消息发送都是高频接口。 暴力匹配在 QPS 上涨后,接口耗时会从几毫秒涨到几百毫秒,直接导致服务雪崩。
新增敏感词、删除敏感词、热更新时,暴力匹配没有缓存优势,每次都要全量遍历。 Trie 树可以重建、增量更新、本地缓存。
面试一句话总结:
暴力匹配时间复杂度高,敏感词库大时性能极差,存在大量重复匹配,不适合高并发场景;而 Trie/DFA/AC 自动机可以实现线性时间匹配,共享前缀,效率提升几个数量级,因此生产环境不会使用暴力匹配。
敏感词过滤看似简单,实则非常考验算法思维 + 工程实践。
在实际业务里,一个好的敏感词模块,既能保证内容安全,又不会拖累接口性能,才是真正合格的设计。