简介:这是一套面向Java中高级开发者与文本处理系统学习者的敏感词过滤实战源码,解决Web应用中常见的内容安全审核、论坛/评论区违禁词拦截等实际问题。资源包共1454个文件,涵盖306个核心Java类(含Trie树构建、AC自动机实现、分词集成与多线程过滤模块)、458个前端交互文件(JS+HTML+CSS,支持敏感词高亮、批量导入导出及可视化配置界面),以及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 Map<Character, 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 List<MatchResult> filter(String text) { List<MatchResult> 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); /** * 查找文本中所有敏感词及其位置 */ List<MatchResult> findAll(String text); }ACAutomatonFilter类在初始化时,会加载敏感词库并构建AC自动机。这里有一个关键点:敏感词库的热更新。我们不可能每次更新词库都重启服务。因此,我采用了“双缓冲”机制。维护两个AC自动机实例:一个正在提供服务的current实例,一个用于构建新版本的preparing实例。当管理员通过后台新增或删除敏感词后,系统在preparing实例上重新构建完整的AC自动机,构建完成后,通过一个原子操作(比如AtomicReference)将current指向新的实例。这样,过滤服务的切换是无感知、无锁的,对性能影响极小。
3.2 敏感词库的设计与加载
敏感词库的格式很重要。我选择了最简单的每行一个词的文本格式,便于运营人员维护。同时,支持从数据库、远程配置中心(如Nacos、Apollo)或本地文件加载。加载器(WordLoader)被设计成可插拔的,通过策略模式可以轻松切换来源。
public interface SensitiveWordLoader { /** * 加载敏感词集合 */ Set<String> load() throws LoadException; } // 文件加载器示例 public class FileWordLoader implements SensitiveWordLoader { private String filePath; @Override public Set<String> load() { Set<String> 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使用HashMap<Character, TrieNode>存储子节点,在节点数巨大时,HashMap的Entry对象会带来不小的内存开销。对于子节点数量通常不会特别多的情况,可以改用数组(如果字符集有限,如纯英文)或者更紧凑的结构如Trove库的TCharObjectHashMap。 - 延迟构建与缓存:服务启动时,如果词库很大,构建AC自动机可能耗时数秒,导致服务启动慢或首次请求超时。可以采用异步构建,或者将构建好的AC自动机状态(经过序列化优化)缓存起来,下次启动直接加载缓存,极大加快启动速度。
4.2 高并发下的线程安全
我们的ACAutomatonFilter在构建好后,内部状态(主要是root节点及其整个树结构)是只读的,因此filter、containsSensitiveWord等方法本身是线程安全的,可以放心被多线程调用。
但是,双缓冲切换这个动作需要保证原子性和可见性。我使用了AtomicReference<SensitiveWordFilter>来持有当前的过滤器实例。构建新实例在后台线程完成,完成后通过atomicRef.compareAndSet(old, new)进行原子替换。确保所有工作线程都能立即看到新的过滤器实例。
public class DynamicFilterManager { private final AtomicReference<SensitiveWordFilter> currentFilterRef = new AtomicReference<>(); public void refreshFilter(Set<String> 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,可以看到所有的单元测试和集成测试结果,这是验证系统是否正常工作的第一步。 - 简单Demo:
public class QuickStart { public static void main(String[] args) { Set<String> 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 List<MatchResult> 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自动机和多模匹配算法的绝佳范例。在实际使用中,根据你的业务特点,调整匹配策略、预处理规则和分级逻辑,才能让它发挥最大的价值。
本文还有配套的精品资源,点击获取