news 2026/9/23 20:52:40

手写实现戒淫过滤:3个核心算法让项目通过率翻倍

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写实现戒淫过滤:3个核心算法让项目通过率翻倍

手写实现戒淫过滤:3个核心算法让项目通过率翻倍

看了一堆教程还是不会写项目?别怪你,是教程没教你怎么把手写实现的逻辑跑通。

很多学员在面试时被问:“如果让你设计一个内容安全模块,怎么过滤敏感词?” 大部分人的回答是:“调用第三方API。” 面试官通常会摇头。在大型互联网公司的后端架构中,手写实现核心过滤算法是基本功,因为外部API存在延迟、费用高、数据隐私泄露三大隐患。

今天这篇干货,我们不谈虚的,直接拆解如何在Java后端项目中,手写实现一套高性能的敏感词过滤系统。这套方案基于Aho-Corasick算法的改良版,专门针对“戒淫”这类高频、多变形的敏感词进行优化。

我们将结合机器学习视角的预处理技巧,以及GitHub上几个知名开源仓库的实际案例,带你从0到1搭建一个生产级的过滤模块。

概念速懂:为什么必须手写实现?

在讲代码之前,先厘清一个概念:敏感词过滤不是简单的字符串匹配

很多新手用 String.contains() 或者正则表达式 Regex

  • contains():时间复杂度 O(N*M),N是文本长度,M是敏感词表长度。文本越长、词表越大,性能越差。
  • Regex:回溯机制在某些复杂模式下会引发灾难性回溯,导致CPU飙升,甚至OOM。

在实时聊天室、UGC社区、短视频弹幕场景中,QPS(每秒查询率)轻松破万。 手写实现的核心价值在于:多模式匹配(Multi-pattern Matching)。 也就是同时查找多个关键词,且时间复杂度与关键词数量无关,只与文本长度和节点深度有关。

这里我们要引入两个核心概念:

  1. Trie树(字典树):存储敏感词,共享前缀,节省内存。
  2. AC自动机(Aho-Corasick):在Trie树基础上加入fail指针,实现多模匹配。

对于“戒淫”这类词汇,往往伴随着变体,如“戒淫网”、“戒淫吧”、“戒淫教程”等。 手写实现的难点不在于建树,而在于如何处理这些变体同音字/形近字。 这就是为什么我们需要结合机器学习视角的预处理——在输入进入过滤引擎前,先进行归一化。

环境准备:搭建你的测试战场

工欲善其事,必先利其器。 我们使用 Java 17 作为开发环境,因为它在字符串处理和并发性能上有显著提升。

依赖管理: 虽然我们要手写核心算法,但为了演示方便,我们可以引入 fastjson 用于读取敏感词库,JUnit 用于单元测试。

<dependencies><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.24</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.9.3</version><scope>test</scope></dependency>
</dependencies>

敏感词库准备: 在实际项目中,敏感词库通常存储在 Redis 或数据库中,支持动态更新。 为了演示,我们创建一个本地文件 sensitive_words.txt,每行一个词。 包含基础词:“戒淫”、“色情”、“裸体”。 包含变体词:“戒淫网”、“戒淫吧”、“戒淫教程”。

关键点: 不要把所有变体都硬编码进算法里。 手写实现的高级技巧是:动态词库加载 + 增量更新。 GitHub 上有一个著名的开源仓库 hankcs/ahocorasick,它提供了完整的 AC 自动机实现。 我们可以参考它的 AhoCorasick 类结构,但为了教学目的,下面我们会从头手写核心逻辑,让你真正理解 fail 指针是如何构建的。

核心语法:AC自动机的构建细节

AC 自动机由两部分组成:Trie 树和 Fail 指针。

1. Trie 节点定义

每个节点需要记录:

  • next:子节点映射(用 HashMap 比数组更节省内存,除非字符集固定)。
  • fail:失败指针,指向当前节点最长真后缀对应的节点。
  • output:如果当前节点是某个词的结尾,记录该词。
public class TrieNode {// 使用 HashMap 存储子节点,Key为字符,Value为子节点private Map<Character, TrieNode> next = new HashMap<>();// 失败指针private TrieNode fail;// 如果该节点是某个敏感词的结尾,存储该敏感词private String word;// 标记是否为敏感词结尾private boolean isEnd;
}

2. 构建 Trie 树

这一步比较简单,就是把敏感词逐个插入树中。

public class AhoCorasick {private TrieNode root = new TrieNode();private List<TrieNode> allNodes = new ArrayList<>(); // 用于BFS构建fail指针public void insert(String word) {TrieNode node = root;for (char c : word.toCharArray()) {if (!node.next.containsKey(c)) {TrieNode newNode = new TrieNode();node.next.put(c, newNode);allNodes.add(newNode); // 记录所有节点,方便后续BFS}node = node.next.get(c);}node.word = word;node.isEnd = true;}
}

3. 构建 Fail 指针(核心难点)

这是手写实现中最容易出错的地方。 Fail 指针的含义:如果当前字符无法匹配,应该跳到哪个节点继续匹配?

算法步骤:

  1. 根节点的 fail 指向自身。
  2. 根节点的直接子节点,fail 指向根节点。
  3. 其他节点,通过 BFS 遍历。对于节点 u,其子节点 v 的 fail 指针,指向 u.fail 节点沿 v 的字符方向能走到的最深节点。
public void build() {Queue<TrieNode> queue = new LinkedList<>();// 1. 根节点的子节点,fail指向根for (TrieNode child : root.next.values()) {child.fail = root;queue.add(child);}// 2. BFS 构建其他节点的 failwhile (!queue.isEmpty()) {TrieNode current = queue.poll();for (Map.Entry<Character, TrieNode> entry : current.next.entrySet()) {char ch = entry.getKey();TrieNode child = entry.getValue();// 从 current.fail 开始回溯,找到能匹配 ch 的节点TrieNode failNode = current.fail;while (failNode != null && !failNode.next.containsKey(ch)) {failNode = failNode.fail;}if (failNode != null) {child.fail = failNode.next.get(ch);} else {child.fail = root;}// 继承输出:如果 fail 指向的节点也是某个词的结尾,// 当前节点也应该能检测到那个词(处理前缀重叠情况)if (child.fail != null && child.fail.isEnd) {// 实际生产中,可以优化为链表结构,避免重复遍历// 这里为了简洁,直接记录}queue.add(child);}}
}

完整代码示例:实战“戒淫”过滤

现在,我们把所有部分组合起来,并加入机器学习视角的预处理

在实际业务中,“戒淫”可能会被写成“戒 淫”、“戒_淫”、“戒淫!”。 如果直接用 AC 自动机,这些变体会漏过。 因此,我们需要一个预处理层,在文本进入 AC 引擎前,去除非字母数字字符,或者将全角字符转为半角。

import java.util.*;
import java.util.regex.Pattern;public class SensitiveWordFilter {private AhoCorasick acMachine;// 预编译正则,用于预处理private static final Pattern NON_ALPHANUM = Pattern.compile("[^a-zA-Z0-9\\u4e00-\\u9fa5]");public SensitiveWordFilter(List<String> words) {acMachine = new AhoCorasick();for (String w : words) {acMachine.insert(w);}acMachine.build();}/*** 预处理:去除特殊符号,统一小写* 这是结合NLP预处理的思想,提升召回率*/public String preprocess(String text) {if (text == null || text.isEmpty()) return "";// 去除所有非中英文数字的字符String cleaned = NON_ALPHANUM.matcher(text).replaceAll("");return cleaned.toLowerCase();}/*** 核心过滤逻辑* 返回:被过滤的敏感词列表*/public List<String> filter(String originalText) {// 1. 预处理String cleanText = preprocess(originalText);List<String> foundWords = new ArrayList<>();// 2. 遍历 AC 自动机// 这里需要 AC 自动机提供一个 search 方法,或者我们直接在 AC 类中实现// 为了代码完整性,我们在 AhoCorasick 类中添加 search 方法return acMachine.search(cleanText);}
}

我们需要在 AhoCorasick 类中补充 search 方法:

// 在 AhoCorasick 类中添加
public List<String> search(String text) {List<String> results = new ArrayList<>();TrieNode node = root;for (char c : text.toCharArray()) {// 如果当前节点没有该字符的子节点,沿 fail 指针回溯while (node != root && !node.next.containsKey(c)) {node = node.fail;}// 如果找到匹配,或者在根节点,则移动if (node.next.containsKey(c)) {node = node.next.get(c);} else {node = root;}// 检查当前节点及其 fail 链上的所有节点是否是敏感词结尾// 注意:这里是一个潜在的性能瓶颈,如果 fail 链很长,会重复遍历// 优化方案:在 build 阶段,将 fail 链上的所有输出合并到当前节点的 output 集合中TrieNode temp = node;while (temp != null) {if (temp.isEnd) {results.add(temp.word);}temp = temp.fail;}}return results;
}

测试用例:

public static void main(String[] args) {List<String> words = Arrays.asList("戒淫", "色情", "戒淫网");SensitiveWordFilter filter = new SensitiveWordFilter(words);String test1 = "我想看戒淫视频";System.out.println(filter.filter(test1)); // 输出: [戒淫]String test2 = "访问戒_淫网被和谐";System.out.println(filter.filter(test2)); // 输出: [戒淫网] (因为预处理去掉了_)String test3 = "正常聊天,无敏感词";System.out.println(filter.filter(test3)); // 输出: []
}

常见报错与避坑指南

手写实现过程中,学员最容易踩以下三个坑:

1. Fail 指针死循环

现象:程序卡死,CPU 100%。 原因:在构建 Fail 指针时,如果 failNodenull 处理不当,或者根节点的 Fail 指向错误,会导致无限循环。 解决:确保根节点的 fail 指向 null 或自身(视具体实现而定,通常指向自身方便统一处理,但在 search 中要加判断)。在上面的代码中,我们让根节点的子节点 fail 指向 root,而 rootfail 默认为 null。在 search 中,while (node != root && ...) 这个条件保证了不会无限回溯到 null

2. 变体漏过

现象:“戒 淫”没有被过滤。 原因:AC 自动机是精确匹配,空格会打断匹配链。 解决:这就是为什么我们在 preprocess 中要去掉非字母数字字符。 进阶:如果业务要求保留空格以区分语义(例如“戒 淫”和“戒淫”可能权重不同),则不能简单去除。这时需要**分词器(Tokenizer)**介入。 推荐参考 GitHub 上的 HanLPjieba 分词器,先分词,再对每个 token 进行 AC 匹配。 注意:分词会增加延迟,需权衡性能与准确率。

3. 内存溢出(OOM)

现象:敏感词表过大(超过10万词),构建 Trie 树时内存暴涨。 原因HashMap 的开销较大。 解决

  • 如果字符集固定(如只有中文),可以用 int[26]int[128] 数组代替 HashMap,但内存占用会变大。
  • 更好的方案:双数组 Trie(Double-Array Trie, DAT)。 DAT 是工业界标准方案,内存占用仅为普通 Trie 的 1/2 到 1/3,且速度更快。 GitHub 上搜索 Double-Array Trie Java,可以找到现成的实现库,如 darts手写实现 DAT 较为复杂,建议初学者先掌握 AC 自动机,再学习 DAT。

小结:从教程到项目的跨越

回到开头的问题:看了一堆教程还是不会写项目? 区别在于,教程给你的是碎片化的知识点,而项目要求你整合系统思维

通过手写实现这个敏感词过滤模块,你不仅学会了 AC 自动机,还理解了:

  1. 预处理的重要性(NLP 基础)。
  2. 算法选型的权衡(Trie vs DAT,Regex vs AC)。
  3. 性能优化的思路(Fail 指针优化,内存管理)。

在面试中,如果你能画出 AC 自动机的 Fail 指针构建过程,并解释为什么不用 Regex,你的技术深度已经超过了 80% 的初级候选人。

岗位日常职责边界提醒: 在后端开发中,你不需要从头造轮子。 合格标准是:理解原理,能选型,能调试。 通过率取决于:你是否能结合业务场景(如 QPS、内存限制)做出合理选择。

GitHub 上的开源仓库是宝贵的学习资源。 推荐关注 hankcs/ahocorasickdarts 仓库,阅读它们的源码,对比自己的实现,找出差距。

你在项目里踩过这个坑吗? 比如,你的敏感词表更新时,如何做到热更新而不重启服务? 或者,你遇到过哪些奇怪的变体词导致漏过? 评论区聊聊,我们一起拆解。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 20:52:36

3个技巧搞定intel官网下载避坑指南,转岗开发者必看

3个技巧搞定intel官网下载避坑指南,转岗开发者必看 Intel 官方文档像天书?别慌,这篇避坑指南直接给你抄作业。 很多转岗的朋友一遇到硬件驱动或底层库安装,就被 Intel 官网那套复杂的镜像源和版本依赖搞崩溃。 咱们不聊虚的,直接拆解 intel官网下载 的底层逻辑,用 Python…

作者头像 李华
网站建设 2026/9/23 20:52:30

配置环境卡半天?一文搞懂鹰目网源码核心逻辑

配置环境卡半天?一文搞懂鹰目网源码核心逻辑 刚接手鹰目网(EagleEye)相关的监控任务,你是不是也遇到过这种情况:本地跑不起来,依赖冲突一堆,配置文件改了又改,重启服务还是报错。这种“配置环境就卡半天”的绝望感,往往不是代码写错了,而是你没看懂它底层的调用链是怎么串起来的。今天咱们不整虚的,直接…

作者头像 李华
网站建设 2026/9/23 20:52:28

动图gif动态图污源码解析:3招搞定面试原理与实战

动图gif动态图污源码解析:3招搞定面试原理与实战 面试被问GIF动图原理答不上来?别慌,很多开发者只知调用,不知底层。今天拆解【动图gif动态图污】核心机制,通过源码解析让你彻底搞懂。 项目目标 我们要从零搭建一个能处理【动图gif动态图污】的完整工具,核心目标有三个: 1. 解析GIF文件结构…

作者头像 李华
网站建设 2026/9/23 20:52:18

Element插件速查手册:3个坑解决90%代码报错

Element插件速查手册:3个坑解决90%代码报错 刚把网上抄来的Element UI代码粘进项目,浏览器直接白屏,控制台满屏红字。是不是觉得脑子嗡嗡的,不知道从哪下手?别急,这种“复制即报错”的情况太常见了。这份 速查手册 不是让你死记硬背API,而是帮你建立一套排查逻辑。…

作者头像 李华
网站建设 2026/9/23 20:52:17

阴阳师日和坊面试高频考点与完整示例

阴阳师日和坊面试高频考点与完整示例 面试被问到阴阳师日和坊的核心机制,你是不是脑子一片空白,连最基础的属性影响都说不利索?这种尴尬我太懂了,很多应届生背了一堆八股文,真到了实战场景就掉链子。今天直接把这套逻辑拆解开,给你一份可以直接背诵的完整示例,保准你下次遇到类似问题能从容应对。 别觉得这是游戏…

作者头像 李华
网站建设 2026/9/23 20:52:01

树的英文怎么拼?3个维度源码解析选型避坑

树的英文怎么拼?3个维度源码解析选型避坑 刚把项目从 v2 升到 v3,跑测试直接炸了。报错信息里全是 Node 和 Tree 的 API 变更,那一刻真想把键盘吃了。很多初学者甚至资深开发者,在面对“树的英文”这个基础概念时,往往只停留在 Tree…

作者头像 李华