手写实现戒淫过滤: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)。
也就是同时查找多个关键词,且时间复杂度与关键词数量无关,只与文本长度和节点深度有关。
这里我们要引入两个核心概念:Trie树(字典树):存储敏感词,共享前缀,节省内存。
AC自动机(Aho-Corasick):在Trie树基础上加入fail指针,实现多模匹配。对于“戒淫”这类词汇,往往伴随着变体,如“戒淫网”、“戒淫吧”、“戒淫教程”等。
手写实现的难点不在于建树,而在于如何处理这些变体和同音字/形近字。
这就是为什么我们需要结合机器学习视角的预处理——在输入进入过滤引擎前,先进行归一化。
环境准备:搭建你的测试战场
工欲善其事,必先利其器。
我们使用 Java 17 作为开发环境,因为它在字符串处理和并发性能上有显著提升。
依赖管理:
虽然我们要手写核心算法,但为了演示方便,我们可以引入 fastjson 用于读取敏感词库,JUnit 用于单元测试。
dependenciesdependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion2.0.24/version/dependencydependencygroupIdorg.junit.jupiter/groupIdartifactIdjunit-jupiter/artifactIdversion5.9.3/versionscopetest/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 MapCharacter, TrieNode next = new HashMap();// 失败指针private TrieNode fail;// 如果该节点是某个敏感词的结尾,存储该敏感词private String word;// 标记是否为敏感词结尾private boolean isEnd;
}2. 构建 Trie 树
这一步比较简单,就是把敏感词逐个插入树中。
public class AhoCorasick {private TrieNode root = new TrieNode();private ListTrieNode 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 指针的含义:如果当前字符无法匹配,应该跳到哪个节点继续匹配?
算法步骤:根节点的 fail 指向自身。
根节点的直接子节点,fail 指向根节点。
其他节点,通过 BFS 遍历。对于节点 u,其子节点 v 的 fail 指针,指向 u.fail 节点沿 v 的字符方向能走到的最深节点。public void build() {QueueTrieNode 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.EntryCharacter, 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(ListString 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 ListString filter(String originalText) {// 1. 预处理String cleanText = preprocess(originalText);ListString foundWords = new ArrayList();// 2. 遍历 AC 自动机// 这里需要 AC 自动机提供一个 search 方法,或者我们直接在 AC 类中实现// 为了代码完整性,我们在 AhoCorasick 类中添加 search 方法return acMachine.search(cleanText);}
}我们需要在 AhoCorasick 类中补充 search 方法:
// 在 AhoCorasick 类中添加
public ListString search(String text) {ListString 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) {ListString 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 指针时,如果 failNode 为 null 处理不当,或者根节点的 Fail 指向错误,会导致无限循环。
解决:确保根节点的 fail 指向 null 或自身(视具体实现而定,通常指向自身方便统一处理,但在 search 中要加判断)。在上面的代码中,我们让根节点的子节点 fail 指向 root,而 root 的 fail 默认为 null。在 search 中,while (node != root ...) 这个条件保证了不会无限回溯到 null。
2. 变体漏过
现象:“戒 淫”没有被过滤。
原因:AC 自动机是精确匹配,空格会打断匹配链。
解决:这就是为什么我们在 preprocess 中要去掉非字母数字字符。
进阶:如果业务要求保留空格以区分语义(例如“戒 淫”和“戒淫”可能权重不同),则不能简单去除。这时需要**分词器(Tokenizer)**介入。
推荐参考 GitHub 上的 HanLP 或 jieba 分词器,先分词,再对每个 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 自动机,还理解了:预处理的重要性(NLP 基础)。
算法选型的权衡(Trie vs DAT,Regex vs AC)。
性能优化的思路(Fail 指针优化,内存管理)。在面试中,如果你能画出 AC 自动机的 Fail 指针构建过程,并解释为什么不用 Regex,你的技术深度已经超过了 80% 的初级候选人。
岗位日常职责边界提醒:
在后端开发中,你不需要从头造轮子。
合格标准是:理解原理,能选型,能调试。
通过率取决于:你是否能结合业务场景(如 QPS、内存限制)做出合理选择。
GitHub 上的开源仓库是宝贵的学习资源。
推荐关注 hankcs/ahocorasick 和 darts 仓库,阅读它们的源码,对比自己的实现,找出差距。
你在项目里踩过这个坑吗?
比如,你的敏感词表更新时,如何做到热更新而不重启服务?
或者,你遇到过哪些奇怪的变体词导致漏过?
评论区聊聊,我们一起拆解。
企业数字化 ERP 产品动态
相关推荐
动图gif动态图污源码解析:3招搞定面试原理与实战 动图gif动态图污源码解析:3招搞定面试原理与实战 面试被问GIF动图原理答不上来?别慌,很多开发者只知调用,不知底层。今天拆解【动图gif动态图污】核心机制,通过源码解析让你彻底搞懂。 项目目标… · 2026/9/23 20:52:33
配置环境卡半天?一文搞懂鹰目网源码核心逻辑 配置环境卡半天?一文搞懂鹰目网源码核心逻辑 刚接手鹰目网(EagleEye)相关的监控任务,你是不是也遇到过这种情况:本地跑不起来,依赖冲突一堆,配置文件改了又改,重启服务还是报错。这种“配置环境就卡半天”的绝望感,往往不是代码写错了,而是… · 2026/9/23 20:52:31
阴阳师日和坊面试高频考点与完整示例 阴阳师日和坊面试高频考点与完整示例 面试被问到阴阳师日和坊的核心机制,你是不是脑子一片空白,连最基础的属性影响都说不利索?这种尴尬我太懂了,很多应届生背了一堆八股文,真到了实战场景就掉链子。今天直接把这套逻辑拆解开,给你一份可以直接背诵的完… · 2026/9/23 20:52:24
Kubernetes 迁移传统分布式应用实战:以 Hadoop YARN 为例的完整指南 教程云原生容器编排 【免费下载链接】kubernetes-handbook Kubernetes 架构与生态:从云原生到 AI 原生基础设施的构建指南 项目地址: https://gitcode.com/gh_mirrors/ku/kubernetes-handbook 点击查看 免费下载 本文是 Kubernetes Handbook 中面向&quo… · 2026/9/23 21:29:25
Apache Druid 空间索引与空间过滤器(Spatial Filter)实战指南 数据库OLAP大数据后端 【免费下载链接】druid Apache Druid: a high performance real-time analytics database. 项目地址: https://gitcode.com/gh_mirrors/druid6/druid 点击查看 免费下载 本文围绕 Apache Druid 原生查询语言中的空间过滤能力展开,… · 2026/9/23 21:29:25
Nextion串口屏驱动安装与中文固件刷写:MMDVM热点显示恢复实战 简介:业余无线电数字语音通信中,MMDVM热点板配合Nextion串口屏的用途很广,但不少HAM在安装驱动或刷入中文固件时频频失败。这份资料正是针对该痛点的操作指南,适合已能进入Pi-Star配置页面、想为STM32-DVM热点完善屏幕显示的入门进… · 2026/9/23 21:29:25
开题报告怎么写:从选题表到任务书的完整流程 开题报告怎么写:从选题表到任务书的完整流程
在毕业季临近时,许多学弟学妹们常常感到焦虑,尤其是在填选题表、撰写毕业设计任务书和开题报告时。尤其是选题表即将截止,任务书的字段空白,开题报告还未展开的情况下&… · 2026/9/23 21:29:25
论文写作流程怎么安排?一份从开题到提交的指南 论文写作流程怎么安排?一份从开题到提交的指南
工具不是越多越好,关键是放在正确环节。每位学弟学妹在撰写论文时,都会经历从选题、资料收集、写作到最终提交的各个阶段。在这些环节中,合理利用工具和方法,可以大大提… · 2026/9/23 21:29:19
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29