
简介这是一套面向Java中高级开发者与文本处理系统学习者的敏感词过滤实战源码解决Web应用中常见的内容安全审核、论坛/评论区违禁词拦截等实际问题。资源包共1454个文件涵盖306个核心Java类含Trie树构建、AC自动机实现、分词集成与多线程过滤模块、458个前端交互文件JSHTMLCSS支持敏感词高亮、批量导入导出及可视化配置界面以及235个HTML页面和百余张UI资源图PNG/JPG/SVG整体压缩后仅16.01MB结构清晰、前后端分离明确。已有266人下载学习代码中完整实现了HanLP分词对接、正则动态匹配、敏感词库热加载、异常日志记录及JUnit单元测试用例配套XML配置与Properties参数化管理便于快速集成到Spring Boot或传统Servlet项目中是理解文本过滤工程化落地的优质参考范例。1. 项目缘起为什么我们需要一个“敏感词筛选系统”最近在整理一个社区项目的后台时遇到了一个挺典型的问题用户提交的评论、昵称、帖子内容里时不时会冒出一些不合规的词汇。手动审核效率低漏网之鱼多还容易因为审核标准不一引发争议。这让我意识到一个高效、准确、可维护的敏感词过滤机制对于任何涉及用户生成内容UGC的互联网应用来说都不是“锦上添花”而是“雪中送炭”的基础设施。你可能也遇到过类似场景一个运营活动因为一条包含不当信息的用户留言而被迫下线一个社交功能因为昵称审核不严导致不良传播。这些问题的背后都指向了内容安全这个核心环节。市面上的通用解决方案要么集成复杂要么定制性差无法完全贴合自身业务对敏感信息的定义比如某些行业术语、内部黑话也可能需要过滤。因此拥有一个自主可控、深度定制的敏感词筛选系统就显得尤为重要。基于这个实际需求我动手实现了一套Java敏感词筛选系统。它不只是一个简单的关键词匹配工具而是包含了多级过滤策略、高性能匹配算法、以及灵活的热更新机制。今天我就把这套系统的设计思路、核心源码实现以及我在开发中踩过的坑毫无保留地分享出来。无论你是正在为内容安全发愁的开发者还是对算法优化感兴趣的同学相信都能从中获得一些直接的启发和可复用的代码。2. 系统核心设计从“字典树”到“多模匹配”的演进之路一个敏感词过滤系统核心在于“匹配”。最朴素的想法是遍历所有敏感词逐个去目标文本里查找。这种方法在敏感词库很小的时候没问题但一旦词库膨胀到成千上万性能就会急剧下降时间复杂度是O(n*m)n是文本长度m是词库大小完全不可接受。所以我们必须引入更高效的数据结构和算法。这里字典树Trie树几乎是所有高性能敏感词过滤系统的基石。2.1 为什么是字典树字典树是一种专门用于处理字符串匹配的树形数据结构。它的核心优势在于利用字符串的公共前缀来减少查询时间。对于敏感词过滤来说这意味着我们不需要对每个敏感词都从头开始扫描文本。举个例子假设我们有敏感词“苹果”、“苹果汁”、“香蕉”。用字典树构建后它们会共享前缀“苹”和“苹果”的路径。当我们在文本中扫描到“苹”字时才需要进入“苹”这个分支继续探查如果下一个字是“果”我们就匹配到了“苹果”同时还可以继续看下一个字是不是“汁”以匹配更长的“苹果汁”。如果文本中是“苹果电脑”匹配到“苹果”后下一个字是“电”不在“果”节点的后续分支里匹配就结束了。这个过程极大地减少了不必要的字符比较。在Java中我们可以用一个简单的类来表示字典树的节点public class TrieNode { // 子节点映射Key是字符Value是对应的子节点 private MapCharacter, TrieNode children new HashMap(); // 标记当前节点是否为一个敏感词的结尾 private boolean isEndOfWord false; // 失败指针用于AC自动机算法后续会详细解释 private TrieNode failover; // 省略getter和setter... }初始时我们只有一个根节点root。插入敏感词“苹果”时我们从根节点开始检查子节点中是否有‘苹’没有则创建然后移动到‘苹’节点再检查其子节点是否有‘果’没有则创建最后将‘果’节点的isEndOfWord标记为true。这样一条从根到‘果’节点的路径就代表了“苹果”这个词。2.2 单模匹配的局限与AC自动机的登场使用基本的字典树我们可以实现单模匹配即一次匹配一个模式串敏感词。但我们的需求是多模匹配在文本的一次扫描中找出所有出现的敏感词。如果对每个敏感词都单独用字典树跑一遍效率依然不高。这时就需要AC自动机Aho-Corasick automaton算法。它是在字典树的基础上增加了failover失败指针的经典多模匹配算法。你可以把它理解成在字典树上构建了一个状态机failover指针的作用是当在某个节点匹配失败时即当前字符不在该节点的子节点中不用回到根节点重新开始而是跳转到failover指针指向的节点继续尝试匹配。构建failover指针的过程通常用BFS广度优先搜索实现是AC自动机的核心。它确保了匹配过程是线性的时间复杂度接近O(n)其中n是文本长度与敏感词库的大小几乎无关。这对于动辄数万敏感词的场景来说是质的飞跃。在我的系统实现中TrieNode类里的failover字段就是为此准备的。构建好AC自动机后过滤一段文本的伪代码如下public ListMatchResult filter(String text) { ListMatchResult results new ArrayList(); TrieNode current root; for (int i 0; i text.length(); i) { char c text.charAt(i); // 如果当前节点没有对应字符的子节点则通过failover指针跳转 while (current ! root !current.getChildren().containsKey(c)) { current current.getFailover(); } // 跳转后再次尝试匹配子节点 current current.getChildren().getOrDefault(c, root); // 检查当前节点及其failover链路上的所有节点看是否为敏感词结尾 TrieNode temp current; while (temp ! root) { if (temp.isEndOfWord()) { // 找到一个匹配记录位置和词 results.add(new MatchResult(i, temp.getWord())); } temp temp.getFailover(); } } return results; }通过这种方式我们只需要对文本进行一次遍历就能找出所有出现的敏感词及其位置效率极高。3. 源码深度解析构建一个工业级的过滤引擎理解了核心算法我们来看具体实现。一个完整的系统不能只有算法还要考虑工程细节。我的源码结构主要分为以下几个核心部分3.1 核心过滤器的实现我定义了一个SensitiveWordFilter接口这是系统的门面主要方法就是filter(String text)。然后提供了基于AC自动机的默认实现ACAutomatonFilter。public interface SensitiveWordFilter { /** * 过滤文本返回过滤后的文本通常用*号替换敏感词 */ String filter(String text); /** * 检查文本是否包含敏感词 */ boolean containsSensitiveWord(String text); /** * 查找文本中所有敏感词及其位置 */ ListMatchResult findAll(String text); }ACAutomatonFilter类在初始化时会加载敏感词库并构建AC自动机。这里有一个关键点敏感词库的热更新。我们不可能每次更新词库都重启服务。因此我采用了“双缓冲”机制。维护两个AC自动机实例一个正在提供服务的current实例一个用于构建新版本的preparing实例。当管理员通过后台新增或删除敏感词后系统在preparing实例上重新构建完整的AC自动机构建完成后通过一个原子操作比如AtomicReference将current指向新的实例。这样过滤服务的切换是无感知、无锁的对性能影响极小。3.2 敏感词库的设计与加载敏感词库的格式很重要。我选择了最简单的每行一个词的文本格式便于运营人员维护。同时支持从数据库、远程配置中心如Nacos、Apollo或本地文件加载。加载器WordLoader被设计成可插拔的通过策略模式可以轻松切换来源。public interface SensitiveWordLoader { /** * 加载敏感词集合 */ SetString load() throws LoadException; } // 文件加载器示例 public class FileWordLoader implements SensitiveWordLoader { private String filePath; Override public SetString load() { SetString words new HashSet(); try (BufferedReader reader new BufferedReader(new FileReader(filePath))) { String line; while ((line reader.readLine()) ! null) { line line.trim(); if (!line.isEmpty() !line.startsWith(#)) { // 支持以#开头的注释行 words.add(line); } } } catch (IOException e) { throw new LoadException(Failed to load sensitive words from file: filePath, e); } return words; } }注意加载时一定要做去重和trim操作避免空白字符和重复词影响树的结构和匹配效率。3.3 匹配策略与处理逻辑找到敏感词只是第一步如何处理它们直接替换为***是最常见的但有时业务需要更精细的控制。我设计了MatchStrategy匹配策略和ReplaceStrategy替换策略。匹配策略可以是最小匹配找到最短的敏感词就返回或最大匹配尽可能匹配最长的词比如“苹果汁”优先于“苹果”。AC自动机天然支持最大匹配只需在遍历failover链路时记录最长的匹配词即可。替换策略除了固定字符替换如*还可以是首尾字符保留中间替换、全词替换、或者甚至触发回调函数进行更复杂的业务处理如记录日志、发送告警、异步审核等。// 一个简单的字符替换策略 public class AsteriskReplaceStrategy implements ReplaceStrategy { private char replacement *; Override public String replace(String word) { // 生成与敏感词等长的替换字符串 char[] chars new char[word.length()]; Arrays.fill(chars, replacement); return new String(chars); } }在实际调用时filter方法会先findAll然后根据匹配到的位置和长度结合替换策略拼接出最终的安全文本。4. 性能优化与生产环境下的“坑”理论很美好但把系统放到真实的高并发、大流量环境下才会遇到真正的挑战。下面是我在开发和压测中总结的几个关键点和踩过的坑。4.1 内存与CPU的权衡AC自动机虽然查询快但构建过程和内存占用是需要关注的。一个包含10万敏感词的词库构建成的字典树可能占用几十到上百MB内存取决于字符集和节点结构。对于JVM应用我们需要确保有足够的堆空间。节点结构优化最初的TrieNode使用HashMapCharacter, TrieNode存储子节点在节点数巨大时HashMap的Entry对象会带来不小的内存开销。对于子节点数量通常不会特别多的情况可以改用数组如果字符集有限如纯英文或者更紧凑的结构如Trove库的TCharObjectHashMap。延迟构建与缓存服务启动时如果词库很大构建AC自动机可能耗时数秒导致服务启动慢或首次请求超时。可以采用异步构建或者将构建好的AC自动机状态经过序列化优化缓存起来下次启动直接加载缓存极大加快启动速度。4.2 高并发下的线程安全我们的ACAutomatonFilter在构建好后内部状态主要是root节点及其整个树结构是只读的因此filter、containsSensitiveWord等方法本身是线程安全的可以放心被多线程调用。但是双缓冲切换这个动作需要保证原子性和可见性。我使用了AtomicReferenceSensitiveWordFilter来持有当前的过滤器实例。构建新实例在后台线程完成完成后通过atomicRef.compareAndSet(old, new)进行原子替换。确保所有工作线程都能立即看到新的过滤器实例。public class DynamicFilterManager { private final AtomicReferenceSensitiveWordFilter currentFilterRef new AtomicReference(); public void refreshFilter(SetString newWords) { // 在后台构建新的过滤器 SensitiveWordFilter newFilter buildFilter(newWords); // 原子替换 SensitiveWordFilter oldFilter currentFilterRef.getAndSet(newFilter); // 可选清理旧的过滤器帮助GC // oldFilter null; } public SensitiveWordFilter getCurrentFilter() { return currentFilterRef.get(); } }4.3 匹配的准确性与“误伤”这是业务层面最关心的问题。敏感词过滤最难的不是技术而是如何平衡“拦截”和“误伤”。特殊字符绕过用户可能会用“苹-果”、“苹*果”来绕过。简单的解决方案是在匹配前对文本进行归一化处理。比如移除所有空白字符、将全角字符转为半角、将类似1和l、0和o的形近字进行统一替换。这需要在过滤前增加一个预处理环节。拆词与组合比如敏感词是“坏人”用户写“坏的人”中间加了个“的”。对于这种情况简单的AC自动机无法处理。一种进阶方案是引入分词语义分析但这会极大增加系统复杂度和计算成本。通常的折中方案是维护一个“干扰词”列表如“的”、“了”、“和”在预处理时将其移除后再进行匹配但这需要谨慎评估以免过度处理导致语义扭曲。上下文相关有些词单独看没问题但在特定上下文里就是敏感词。这已经超出了关键词匹配的范畴需要结合NLP甚至机器学习模型属于更高级的内容安全方案了。在我的系统里我提供了一个可配置的TextPreprocessor接口允许使用者插入自定义的预处理逻辑比如上面提到的字符归一化。public interface TextPreprocessor { String preprocess(String text); } // 在过滤器中应用预处理 public String filter(String text) { String processedText preprocessor.preprocess(text); // ... 使用processedText进行AC自动机匹配 // ... 替换后可能需要将结果映射回原始文本的位置如果预处理改变了文本长度 }4.4 监控与日志线上系统必须有监控。我记录了几个关键指标过滤耗时每次调用filter方法的平均时间、P99时间监控性能是否劣化。匹配命中率统计被过滤的请求占比以及高频敏感词Top N用于分析内容安全态势。词库加载与切换事件记录词库更新、过滤器重建成功或失败的事件便于排查问题。日志方面对于被拦截的内容切忌记录完整的原始文本尤其是如果文本中包含用户隐私信息如手机号、地址。通常只记录匹配到的敏感词、文本的哈希值用于追溯、用户ID和时间戳即可。5. 从“过滤”到“审核”系统的扩展与实践一个健壮的敏感词系统不应该只是一个“一刀切”的过滤器。在实际业务中我们往往需要更灵活的审核流程。5.1 分级过滤与审核流程我设计了多级过滤策略Level 1: 绝对拦截词如涉政、暴恐、极端言论等一旦匹配直接拒绝内容不入库。Level 2: 可疑词如一些灰色地带的词汇、竞品名称等。匹配后内容不会直接展示而是进入“待审核”状态通知运营人员人工审核。Level 3: 替换词如不雅用语、广告引流词汇等。匹配后自动替换为*号然后正常发布。在系统实现上我为每个敏感词增加了一个level属性。AC自动机的节点数据结构需要扩展以存储这个词的级别可能一个节点是多个不同级别词的结尾。匹配时不仅返回词还返回其级别。外层业务逻辑根据级别决定执行拦截、转审还是替换。5.2 与业务系统的集成这套系统通常以JAR包的形式提供集成到Spring Boot项目中非常方便。我提供了一个EnableSensitiveWordFilter注解和自动配置类开发者只需要在配置文件中指定敏感词文件路径或数据源就可以直接注入SensitiveWordFilterBean来使用。对于更复杂的场景比如需要对数据库里存量海量文本进行批量过滤例如内容迁移或合规整改我提供了BatchTextProcessor工具类它利用多线程和流式处理能够高效地完成批量任务并生成详细的处理报告。5.3 测试确保过滤的准确性这类系统的测试至关重要尤其是边界情况。单元测试覆盖核心算法如AC自动机的构建、双缓冲切换、各种匹配策略最小/最大。集成测试模拟真实文本测试过滤和替换结果是否正确。特别注意测试重叠词如“苹果”和“苹果汁”、特殊字符、长文本下的性能。压力测试使用JMeter等工具模拟高并发请求观察GC情况、内存占用和RT响应时间确保在峰值流量下稳定运行。我习惯准备一个“测试词库”和对应的“测试文本-期望结果”的对照表在每次构建或词库更新后自动跑一遍确保核心功能没有回归。6. 源码使用指南与快速上手如果你拿到了我的Java敏感词筛选系统源码.zip可以按照以下步骤快速搭建和测试环境准备确保你的机器上有JDK 8或以上版本Maven 3.6。导入项目解压源码用IDE如IntelliJ IDEA或Eclipse导入作为一个Maven项目。核心模块项目主要模块是sensitive-word-core包含了过滤器的所有实现。sensitive-word-spring-boot-starter提供了Spring Boot的快速集成。运行测试在根目录下执行mvn test可以看到所有的单元测试和集成测试结果这是验证系统是否正常工作的第一步。简单Demopublic class QuickStart { public static void main(String[] args) { SetString words new HashSet(); words.add(苹果); words.add(香蕉); words.add(苹果汁); // 1. 构建过滤器 SensitiveWordFilter filter new ACAutomatonFilter.Builder() .wordLoader(() - words) // 使用内存词库 .replaceStrategy(new AsteriskReplaceStrategy(*)) .build(); // 2. 使用 String text 我喜欢吃苹果和香蕉也爱喝苹果汁。; System.out.println(原始文本: text); System.out.println(过滤后: filter.filter(text)); // 输出: 我喜欢吃**和**也爱喝***。 System.out.println(是否包含敏感词: filter.containsSensitiveWord(text)); // true ListMatchResult matches filter.findAll(text); matches.forEach(m - System.out.println(匹配到: m.getWord() , 位置: m.getStartIndex())); } }集成到Spring Boot引入starter依赖如果你打包发布了的话。在application.yml中配置sensitive-word: enabled: true strategy: REPLACE # 或 REJECT, REVIEW word-source: type: file location: classpath:sensitive-words.txt replace-char: *在Service中直接Autowired注入SensitiveWordFilter使用即可。这套源码的重点在于其清晰的分层设计、可扩展的接口以及生产级别的考量如性能、并发、热更新。你可以直接用它来解决内容过滤问题也可以把它当作一个学习AC自动机和多模匹配算法的绝佳范例。在实际使用中根据你的业务特点调整匹配策略、预处理规则和分级逻辑才能让它发挥最大的价值。本文还有配套的精品资源点击获取