🛡️ 敏感词过滤:文字界的守门员大作战

34 阅读13分钟

知识点编号:267
难度系数:⭐⭐⭐⭐
实用指数:💯💯💯💯💯


📖 开篇:一场聊天室的"翻车"事故

想象你开发了一个社交App,上线第一天就收到举报:

用户A: "这个产品真 TM 垃圾!"
用户B: "买 P i a o,QQ: 123456"
用户C: "习 近 平" (政治敏感)
用户D: "六 合 彩 开 奖" (赌博)

产品经理冲到你面前:

"小王啊,这些敏感词怎么没过滤?公司要被约谈了!😱"

你:

"啊?什么敏感词?我不知道啊!😭"

这就是我们今天要解决的问题:如何优雅地实现敏感词过滤系统!🎯


🎯 敏感词过滤的应用场景

场景敏感词类型处理方式
评论系统 💬脏话、广告替换为***
直播弹幕 📺政治、暴力直接屏蔽
用户昵称 👤低俗、违规禁止注册
文章发布 📝违法、涉黄人工审核
搜索引擎 🔍敏感词汇阻止搜索

🎨 敏感词检测的四种实现方式

┌──────────────────────────────────────────────────────────┐
│              敏感词过滤算法对比                            │
└──────────────────────────────────────────────────────────┘

方案1: 暴力匹配(String.contains)
  优点: 简单易懂
  缺点: 性能极差 O(n*m*k)
  
方案2: 正则表达式(Pattern.compile)
  优点: 功能强大,支持复杂规则
  缺点: 词库大时性能差

方案3: 前缀树 Trie(推荐⭐⭐⭐⭐)
  优点: 性能高 O(n)
  缺点: 内存占用较大
  
方案4: DFA算法(推荐⭐⭐⭐⭐⭐)
  优点: 性能最高,扫描一遍
  缺点: 实现稍复杂

🐌 方案一:暴力匹配(新手村)

代码实现

public class SimpleSensitiveFilter {
    
    private Set<String> sensitiveWords = new HashSet<>();
    
    public SimpleSensitiveFilter() {
        // 初始化敏感词库
        sensitiveWords.add("傻逼");
        sensitiveWords.add("妈的");
        sensitiveWords.add("垃圾");
        // ... 假设有10000个敏感词
    }
    
    /**
     * 检测是否包含敏感词
     */
    public boolean containsSensitive(String text) {
        for (String word : sensitiveWords) {
            if (text.contains(word)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * 替换敏感词
     */
    public String filter(String text) {
        for (String word : sensitiveWords) {
            text = text.replace(word, "***");
        }
        return text;
    }
}

// 测试
public static void main(String[] args) {
    SimpleSensitiveFilter filter = new SimpleSensitiveFilter();
    String text = "这个产品真垃圾,傻逼设计!";
    
    System.out.println(filter.filter(text));
    // 输出: 这个产品真***,***设计!
}

性能测试 🐌

// 假设:
// - 敏感词库:10,000个词
// - 待检测文本:1000字
// - 平均每个词长度:4字符

// 时间复杂度:O(词库数量 × 文本长度 × 词长度)
//           = O(10,000 × 1,000 × 4)
//           = O(40,000,000) 次操作

// 实测:单次检测耗时约 200ms 😱

优缺点

维度评价说明
实现难度5分钟搞定
性能❌❌❌太慢,不可用
内存占用很小
适用场景📚只适合学习

🔍 方案二:正则表达式(进阶版)

代码实现

public class RegexSensitiveFilter {
    
    private Pattern pattern;
    
    public RegexSensitiveFilter(Set<String> words) {
        // 构建正则表达式
        // (word1|word2|word3|...)
        String regex = words.stream()
            .map(Pattern::quote)  // 转义特殊字符
            .collect(Collectors.joining("|", "(", ")"));
        
        this.pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
    }
    
    public boolean containsSensitive(String text) {
        return pattern.matcher(text).find();
    }
    
    public String filter(String text) {
        Matcher matcher = pattern.matcher(text);
        return matcher.replaceAll("***");
    }
}

// 测试
public static void main(String[] args) {
    Set<String> words = new HashSet<>(Arrays.asList("傻逼", "垃圾", "妈的"));
    RegexSensitiveFilter filter = new RegexSensitiveFilter(words);
    
    String text = "这个产品真垃圾!";
    System.out.println(filter.filter(text));
    // 输出: 这个产品真***!
}

支持变形检测 🎭

public class AdvancedRegexFilter {
    
    /**
     * 支持空格、特殊字符插入的变形
     */
    private Pattern buildPattern(String word) {
        // 将 "傻逼" 转换为 "傻[\s\W]*逼"
        StringBuilder sb = new StringBuilder();
        for (char c : word.toCharArray()) {
            sb.append(c);
            sb.append("[\s\W]*");  // 匹配空格和特殊字符
        }
        return Pattern.compile(sb.toString());
    }
    
    public String filter(String text) {
        // 可以检测:傻 逼、傻-逼、傻*逼 等变形
        text = text.replaceAll("傻[\s\W]*逼", "***");
        return text;
    }
}

// 测试
String text1 = "傻逼";      // 匹配 ✅
String text2 = "傻 逼";     // 匹配 ✅
String text3 = "傻-逼";     // 匹配 ✅
String text4 = "傻*&^逼";   // 匹配 ✅

优缺点

维度评价说明
实现难度⭐⭐需要懂正则
性能⚠️词库小时尚可
灵活性✅✅支持复杂规则
适用场景🎯词库<1000,需要变形匹配

🌲 方案三:前缀树Trie(推荐方案)

什么是前缀树?

想象你在玩"成语接龙"游戏 🎮:

                根节点
                 │
        ┌────────┼────────┐
        │        │        │
       傻       垃       妈
        │        │        │
       逼       圾       的
     (结束)   (结束)   (结束)

前缀树的特点

  • 共享相同前缀
  • 从根到叶子的路径=一个词
  • 查找快如闪电 ⚡

生活比喻 📚

就像字典的目录:

  • 要查"苹果",先翻到"A"(苹的拼音),再找"果"
  • 不需要从"阿"开始一个个查

数据结构设计

public class TrieNode {
    
    // 子节点
    private Map<Character, TrieNode> children = new HashMap<>();
    
    // 是否为词的结尾
    private boolean isEnd = false;
    
    public void addChild(Character c, TrieNode node) {
        children.put(c, node);
    }
    
    public TrieNode getChild(Character c) {
        return children.get(c);
    }
    
    public boolean hasChild(Character c) {
        return children.containsKey(c);
    }
    
    public void setEnd(boolean end) {
        this.isEnd = end;
    }
    
    public boolean isEnd() {
        return isEnd;
    }
}

前缀树实现

public class TrieSensitiveFilter {
    
    private TrieNode root = new TrieNode();
    
    /**
     * 构建前缀树
     */
    public void addWord(String word) {
        TrieNode node = root;
        
        for (char c : word.toCharArray()) {
            if (!node.hasChild(c)) {
                node.addChild(c, new TrieNode());
            }
            node = node.getChild(c);
        }
        
        node.setEnd(true);  // 标记结束
    }
    
    /**
     * 检测是否包含敏感词
     */
    public boolean containsSensitive(String text) {
        for (int i = 0; i < text.length(); i++) {
            int len = checkWord(text, i);
            if (len > 0) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * 从位置begin开始检测
     * @return 敏感词长度,0表示不匹配
     */
    private int checkWord(String text, int begin) {
        TrieNode node = root;
        int len = 0;
        
        for (int i = begin; i < text.length(); i++) {
            char c = text.charAt(i);
            
            // 跳过空格等干扰字符
            if (isSkipChar(c)) {
                continue;
            }
            
            node = node.getChild(c);
            if (node == null) {
                return 0;  // 不匹配
            }
            
            len++;
            
            if (node.isEnd()) {
                return len;  // 匹配到完整词
            }
        }
        
        return 0;
    }
    
    /**
     * 过滤敏感词
     */
    public String filter(String text) {
        StringBuilder result = new StringBuilder();
        int i = 0;
        
        while (i < text.length()) {
            int len = checkWord(text, i);
            
            if (len > 0) {
                // 替换为***
                result.append("***");
                i += len;
            } else {
                // 保留原字符
                result.append(text.charAt(i));
                i++;
            }
        }
        
        return result.toString();
    }
    
    /**
     * 判断是否为干扰字符
     */
    private boolean isSkipChar(char c) {
        // 空格、标点符号等
        return c == ' ' || c == '*' || c == '-' || c == '_';
    }
}

// 测试
public static void main(String[] args) {
    TrieSensitiveFilter filter = new TrieSensitiveFilter();
    
    // 构建敏感词库
    filter.addWord("傻逼");
    filter.addWord("垃圾");
    filter.addWord("妈的");
    
    // 测试1: 普通匹配
    String text1 = "这个产品真垃圾!";
    System.out.println(filter.filter(text1));
    // 输出: 这个产品真***!
    
    // 测试2: 变形匹配(有干扰字符)
    String text2 = "你是个傻-逼";
    System.out.println(filter.filter(text2));
    // 输出: 你是个***
}

性能分析 ⚡

// 时间复杂度:O(文本长度)
// - 假设文本1000字
// - 检测每个字符最多遍历一次
// - 总操作:1000次

// 实测:单次检测耗时约 0.5ms 
// 比暴力匹配快了 400倍!🚀

优缺点

维度评价说明
实现难度⭐⭐⭐需要理解树结构
性能✅✅✅非常快 O(n)
内存占用⚠️较大(树结构)
变形支持可跳过干扰字符
适用场景🎯生产环境推荐

🚀 方案四:DFA算法(终极版)

什么是DFA?

DFA(Deterministic Finite Automaton) = 确定有限状态机

用人话说:一次扫描,找出所有敏感词 🎯

DFA vs Trie的区别

Trie树:
  - 每个字符都要检查一次
  - 需要多次回溯
  
DFA状态机:
  - 一遍扫描到底
  - 不需要回溯
  - 性能更优!

DFA实现

public class DFASensitiveFilter {
    
    // 状态机的根节点
    private Map<Character, Object> rootMap = new HashMap<>();
    
    // 结束标记
    private static final String END_FLAG = "END";
    
    /**
     * 构建DFA状态机
     */
    public void addWord(String word) {
        Map<Character, Object> currentMap = rootMap;
        
        for (int i = 0; i < word.length(); i++) {
            char c = word.charAt(i);
            
            // 获取或创建子节点
            Object node = currentMap.get(c);
            if (node == null) {
                Map<Character, Object> newNode = new HashMap<>();
                currentMap.put(c, newNode);
                currentMap = newNode;
            } else {
                currentMap = (Map<Character, Object>) node;
            }
            
            // 最后一个字符,标记结束
            if (i == word.length() - 1) {
                currentMap.put(END_FLAG, true);
            }
        }
    }
    
    /**
     * 检测并替换敏感词
     */
    public String filter(String text) {
        StringBuilder result = new StringBuilder();
        Map<Character, Object> currentMap = rootMap;
        
        int begin = 0;  // 敏感词起始位置
        int position = 0;  // 当前检测位置
        
        while (position < text.length()) {
            char c = text.charAt(position);
            
            // 跳过干扰字符
            if (isSkipChar(c)) {
                if (currentMap == rootMap) {
                    result.append(c);
                    begin++;
                }
                position++;
                continue;
            }
            
            // 状态转移
            Object nextNode = currentMap.get(c);
            
            if (nextNode == null) {
                // 不匹配,输出begin位置的字符
                result.append(text.charAt(begin));
                position = begin + 1;
                begin = position;
                currentMap = rootMap;
                
            } else {
                Map<Character, Object> nextMap = (Map<Character, Object>) nextNode;
                
                // 检查是否到达终点
                if (nextMap.containsKey(END_FLAG)) {
                    // 找到敏感词
                    result.append("***");
                    position++;
                    begin = position;
                    currentMap = rootMap;
                } else {
                    // 继续匹配
                    currentMap = nextMap;
                    position++;
                }
            }
        }
        
        // 处理剩余字符
        while (begin < text.length()) {
            result.append(text.charAt(begin));
            begin++;
        }
        
        return result.toString();
    }
    
    /**
     * 获取所有敏感词及其位置
     */
    public List<SensitiveWord> findAll(String text) {
        List<SensitiveWord> result = new ArrayList<>();
        Map<Character, Object> currentMap = rootMap;
        
        int begin = 0;
        int position = 0;
        
        while (position < text.length()) {
            char c = text.charAt(position);
            
            if (isSkipChar(c)) {
                if (currentMap == rootMap) {
                    begin++;
                }
                position++;
                continue;
            }
            
            Object nextNode = currentMap.get(c);
            
            if (nextNode == null) {
                position = begin + 1;
                begin = position;
                currentMap = rootMap;
                
            } else {
                Map<Character, Object> nextMap = (Map<Character, Object>) nextNode;
                
                if (nextMap.containsKey(END_FLAG)) {
                    // 记录敏感词
                    String word = text.substring(begin, position + 1);
                    result.add(new SensitiveWord(word, begin, position));
                    
                    position++;
                    begin = position;
                    currentMap = rootMap;
                } else {
                    currentMap = nextMap;
                    position++;
                }
            }
        }
        
        return result;
    }
    
    private boolean isSkipChar(char c) {
        return c == ' ' || c == '*' || c == '-' || c == '_';
    }
}

// 敏感词实体
@Data
@AllArgsConstructor
public class SensitiveWord {
    private String word;      // 敏感词
    private int startIndex;   // 起始位置
    private int endIndex;     // 结束位置
}

测试示例

public static void main(String[] args) {
    DFASensitiveFilter filter = new DFASensitiveFilter();
    
    // 构建词库
    filter.addWord("傻逼");
    filter.addWord("垃圾");
    filter.addWord("法轮功");
    filter.addWord("买票");
    
    // 测试1: 基本过滤
    String text1 = "你就是个大傻逼,垃圾东西!";
    System.out.println(filter.filter(text1));
    // 输出: 你就是个大***,***东西!
    
    // 测试2: 变形检测
    String text2 = "买 票 QQ:123456";
    System.out.println(filter.filter(text2));
    // 输出: *** QQ:123456
    
    // 测试3: 查找所有敏感词
    List<SensitiveWord> words = filter.findAll(text1);
    words.forEach(w -> 
        System.out.printf("敏感词:%s,位置:[%d, %d]\n", 
            w.getWord(), w.getStartIndex(), w.getEndIndex())
    );
    // 输出:
    // 敏感词:傻逼,位置:[5, 6]
    // 敏感词:垃圾,位置:[8, 9]
}

性能测试 🚀

@Test
public void performanceTest() {
    DFASensitiveFilter filter = new DFASensitiveFilter();
    
    // 加载10000个敏感词
    loadWords(filter, 10000);
    
    // 生成1000字的测试文本
    String text = generateText(1000);
    
    // 测试100次
    long start = System.currentTimeMillis();
    for (int i = 0; i < 100; i++) {
        filter.filter(text);
    }
    long end = System.currentTimeMillis();
    
    System.out.println("100次过滤耗时:" + (end - start) + "ms");
    System.out.println("平均每次:" + (end - start) / 100.0 + "ms");
    
    // 实测结果:
    // 100次过滤耗时:50ms
    // 平均每次:0.5ms ⚡
}

优缺点

维度评价说明
实现难度⭐⭐⭐⭐状态机复杂
性能✅✅✅✅✅最快!O(n)
内存占用⚠️较大
变形支持✅✅完美支持
适用场景🚀高并发场景

🎨 生产环境完整方案

架构设计

┌──────────────────────────────────────────────────────────┐
│            敏感词过滤系统架构                              │
└──────────────────────────────────────────────────────────┘

                   用户请求
                      ↓
              ┌──────────────┐
              │  业务系统     │
              └──────┬───────┘
                     │
                     ↓
         ┌─────────────────────┐
         │ 敏感词过滤Service    │
         │  - DFA算法          │
         │  - 本地缓存          │
         └─────────┬───────────┘
                   │
         ┌─────────┴──────────┐
         │                    │
         ↓                    ↓
    ┌─────────┐        ┌──────────┐
    │ Redis   │        │ MySQL    │
    │ (热词)  │        │ (全量)   │
    └─────────┘        └──────────┘
         ↑                    ↑
         │                    │
         └────────┬───────────┘
                  │
           ┌──────────────┐
           │ 词库管理后台  │
           │  - 增删改查   │
           │  - 版本管理   │
           └──────────────┘

完整代码实现

1. 数据库表设计

-- 敏感词表
CREATE TABLE sensitive_word (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    word VARCHAR(100) NOT NULL COMMENT '敏感词',
    category VARCHAR(50) COMMENT '分类:政治/暴力/色情/广告',
    level INT DEFAULT 1 COMMENT '级别:1-低危 2-中危 3-高危',
    action VARCHAR(20) DEFAULT 'REPLACE' COMMENT '动作:REPLACE/BLOCK/REVIEW',
    status INT DEFAULT 1 COMMENT '状态:1-启用 0-禁用',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_word (word),
    KEY idx_category (category),
    KEY idx_status (status)
) COMMENT '敏感词表';

-- 过滤日志表
CREATE TABLE sensitive_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT COMMENT '用户ID',
    content TEXT COMMENT '原始内容',
    filtered_content TEXT COMMENT '过滤后内容',
    hit_words VARCHAR(500) COMMENT '命中的敏感词',
    action VARCHAR(20) COMMENT '处理动作',
    ip VARCHAR(50) COMMENT 'IP地址',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_user (user_id),
    KEY idx_created (created_at)
) COMMENT '敏感词过滤日志';

2. 敏感词Service

@Service
public class SensitiveWordService {
    
    @Autowired
    private SensitiveWordMapper wordMapper;
    
    @Autowired
    private SensitiveLogMapper logMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // DFA过滤器(单例)
    private DFASensitiveFilter filter = new DFASensitiveFilter();
    
    // 词库版本号
    private volatile long version = 0L;
    
    /**
     * 初始化敏感词库
     */
    @PostConstruct
    public void init() {
        reloadWords();
    }
    
    /**
     * 重新加载敏感词库
     */
    public void reloadWords() {
        // 1. 从数据库加载
        List<String> words = wordMapper.selectAllActive();
        
        // 2. 重建DFA
        DFASensitiveFilter newFilter = new DFASensitiveFilter();
        words.forEach(newFilter::addWord);
        
        // 3. 替换旧filter
        this.filter = newFilter;
        this.version = System.currentTimeMillis();
        
        // 4. 同步到Redis
        redisTemplate.opsForValue().set("sensitive:version", version);
        redisTemplate.opsForValue().set("sensitive:words", words);
        
        log.info("✅ 敏感词库加载完成,共{}个词", words.size());
    }
    
    /**
     * 过滤敏感词
     */
    public FilterResult filter(String content, Long userId) {
        // 1. 查找所有敏感词
        List<SensitiveWord> hitWords = filter.findAll(content);
        
        if (hitWords.isEmpty()) {
            return FilterResult.ok(content);
        }
        
        // 2. 根据敏感词级别决定动作
        SensitiveAction action = determineAction(hitWords);
        
        // 3. 执行过滤
        String filtered = filter.filter(content);
        
        // 4. 记录日志(异步)
        logAsync(userId, content, filtered, hitWords, action);
        
        // 5. 返回结果
        return FilterResult.builder()
            .originalContent(content)
            .filteredContent(filtered)
            .hitWords(hitWords)
            .action(action)
            .build();
    }
    
    /**
     * 判断处理动作
     */
    private SensitiveAction determineAction(List<SensitiveWord> hitWords) {
        // 高危词直接拦截
        boolean hasHighRisk = hitWords.stream()
            .anyMatch(w -> getWordLevel(w.getWord()) >= 3);
        
        if (hasHighRisk) {
            return SensitiveAction.BLOCK;
        }
        
        // 中危词人工审核
        boolean hasMediumRisk = hitWords.stream()
            .anyMatch(w -> getWordLevel(w.getWord()) == 2);
        
        if (hasMediumRisk) {
            return SensitiveAction.REVIEW;
        }
        
        // 低危词替换
        return SensitiveAction.REPLACE;
    }
    
    /**
     * 异步记录日志
     */
    @Async
    public void logAsync(Long userId, String original, String filtered, 
                        List<SensitiveWord> hitWords, SensitiveAction action) {
        SensitiveLog log = new SensitiveLog();
        log.setUserId(userId);
        log.setContent(original);
        log.setFilteredContent(filtered);
        log.setHitWords(hitWords.stream()
            .map(SensitiveWord::getWord)
            .collect(Collectors.joining(",")));
        log.setAction(action.name());
        
        logMapper.insert(log);
    }
    
    /**
     * 添加敏感词
     */
    public void addWord(String word, String category, int level) {
        // 1. 保存到数据库
        SensitiveWord entity = new SensitiveWord();
        entity.setWord(word);
        entity.setCategory(category);
        entity.setLevel(level);
        wordMapper.insert(entity);
        
        // 2. 实时添加到DFA
        filter.addWord(word);
        
        // 3. 通知其他节点刷新(通过Redis发布订阅)
        redisTemplate.convertAndSend("sensitive:reload", version);
    }
}

3. 过滤结果

@Data
@Builder
public class FilterResult {
    private String originalContent;     // 原始内容
    private String filteredContent;     // 过滤后内容
    private List<SensitiveWord> hitWords;  // 命中的敏感词
    private SensitiveAction action;     // 处理动作
    private boolean hasSensitive;       // 是否有敏感词
    
    public static FilterResult ok(String content) {
        return FilterResult.builder()
            .originalContent(content)
            .filteredContent(content)
            .hitWords(Collections.emptyList())
            .action(SensitiveAction.PASS)
            .hasSensitive(false)
            .build();
    }
}

public enum SensitiveAction {
    PASS,      // 通过
    REPLACE,   // 替换
    REVIEW,    // 人工审核
    BLOCK      // 直接拦截
}

4. 控制器使用

@RestController
@RequestMapping("/comment")
public class CommentController {
    
    @Autowired
    private SensitiveWordService sensitiveService;
    
    @PostMapping("/add")
    public Result addComment(@RequestBody CommentDTO dto) {
        // 1. 过滤敏感词
        FilterResult filterResult = sensitiveService.filter(
            dto.getContent(), 
            UserContext.getUserId()
        );
        
        // 2. 根据动作处理
        switch (filterResult.getAction()) {
            case BLOCK:
                return Result.fail("内容包含违规信息,无法发布");
                
            case REVIEW:
                // 保存为待审核状态
                saveComment(filterResult.getFilteredContent(), 
                           CommentStatus.PENDING);
                return Result.success("评论已提交,等待审核");
                
            case REPLACE:
                // 直接发布(已替换敏感词)
                saveComment(filterResult.getFilteredContent(), 
                           CommentStatus.PUBLISHED);
                return Result.success("评论发布成功");
                
            default:
                // 无敏感词,直接发布
                saveComment(filterResult.getOriginalContent(), 
                           CommentStatus.PUBLISHED);
                return Result.success("评论发布成功");
        }
    }
}

🎭 高级技巧:对抗变形

常见变形手段

变形类型示例说明
插入空格"傻 逼"最常见
插入符号"傻@逼"用特殊字符
拼音代替"sha bi"拼音混淆
谐音代替"沙比"同音字
繁简混用"傻逼" (繁体)繁体字
火星文"傻♂逼"奇怪符号

对抗策略

1. 字符标准化

public class TextNormalizer {
    
    /**
     * 文本标准化
     */
    public static String normalize(String text) {
        // 1. 转小写
        text = text.toLowerCase();
        
        // 2. 繁体转简体
        text = convertToSimplified(text);
        
        // 3. 全角转半角
        text = convertToHalfWidth(text);
        
        // 4. 移除特殊字符
        text = removeSpecialChars(text);
        
        return text;
    }
    
    /**
     * 繁体转简体
     */
    private static String convertToSimplified(String text) {
        // 使用opencc4j库
        return ZhConverterUtil.toSimple(text);
    }
    
    /**
     * 全角转半角
     */
    private static String convertToHalfWidth(String text) {
        StringBuilder sb = new StringBuilder();
        for (char c : text.toCharArray()) {
            if (c >= 65281 && c <= 65374) {
                // 全角转半角
                sb.append((char) (c - 65248));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }
}

2. 拼音检测

public class PinyinSensitiveFilter {
    
    private DFASensitiveFilter filter = new DFASensitiveFilter();
    
    /**
     * 构建拼音敏感词库
     */
    public void addWord(String word) {
        // 1. 添加原词
        filter.addWord(word);
        
        // 2. 添加拼音形式
        String pinyin = PinyinHelper.convertToPinyin(word, "");
        filter.addWord(pinyin);
        
        // 3. 添加拼音首字母
        String initial = PinyinHelper.getShortPinyin(word);
        filter.addWord(initial);
    }
    
    public String filter(String text) {
        // 检测拼音
        String pinyin = PinyinHelper.convertToPinyin(text, "");
        if (filter.containsSensitive(pinyin)) {
            return "***";
        }
        
        // 检测原文
        return filter.filter(text);
    }
}

// 示例
filter.addWord("傻逼");
// 自动生成:
// - 傻逼
// - shabi
// - sb

3. 相似度检测

public class SimilarityChecker {
    
    /**
     * 检测是否为谐音词
     */
    public boolean isSimilar(String word1, String word2) {
        // 1. 拼音相似度
        String pinyin1 = PinyinHelper.convertToPinyin(word1, "");
        String pinyin2 = PinyinHelper.convertToPinyin(word2, "");
        
        double pinyinSim = calculateSimilarity(pinyin1, pinyin2);
        
        // 2. 字形相似度
        double shapeSim = calculateShapeSimilarity(word1, word2);
        
        // 3. 综合判断
        return pinyinSim > 0.8 || shapeSim > 0.7;
    }
    
    /**
     * 计算编辑距离
     */
    private double calculateSimilarity(String s1, String s2) {
        int distance = levenshteinDistance(s1, s2);
        int maxLen = Math.max(s1.length(), s2.length());
        return 1.0 - (double) distance / maxLen;
    }
}

📊 性能优化

1️⃣ 多级缓存

@Component
public class CachedSensitiveFilter {
    
    @Autowired
    private DFASensitiveFilter filter;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 本地缓存(Caffeine)
    private LoadingCache<String, String> localCache = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(Duration.ofMinutes(10))
        .build(this::doFilter);
    
    /**
     * 过滤(带缓存)
     */
    public String filter(String text) {
        // 1. 本地缓存
        String cached = localCache.get(text);
        if (cached != null) {
            return cached;
        }
        
        // 2. Redis缓存
        String key = "sensitive:filter:" + MD5Util.md5(text);
        String redisResult = redisTemplate.opsForValue().get(key);
        if (redisResult != null) {
            return redisResult;
        }
        
        // 3. 实际过滤
        String result = doFilter(text);
        
        // 4. 写入缓存
        redisTemplate.opsForValue().set(key, result, Duration.ofHours(1));
        
        return result;
    }
    
    private String doFilter(String text) {
        return filter.filter(text);
    }
}

2️⃣ 异步处理

@Service
public class AsyncSensitiveService {
    
    @Autowired
    private SensitiveWordService sensitiveService;
    
    /**
     * 异步批量过滤
     */
    @Async("sensitiveExecutor")
    public CompletableFuture<List<String>> batchFilter(List<String> texts) {
        List<String> results = texts.stream()
            .map(text -> sensitiveService.filter(text, null).getFilteredContent())
            .collect(Collectors.toList());
        
        return CompletableFuture.completedFuture(results);
    }
}

@Configuration
public class ExecutorConfig {
    
    @Bean(name = "sensitiveExecutor")
    public Executor sensitiveExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("sensitive-");
        executor.initialize();
        return executor;
    }
}

🎓 面试题解析

Q1:如何应对敏感词变形?

  1. 字符标准化:繁简转换、全半角转换
  2. 跳过干扰字符:空格、符号、表情
  3. 拼音检测:检测拼音和首字母
  4. 相似度算法:编辑距离、音形相似
  5. AI模型:使用NLP模型识别变形

Q2:10万个敏感词,如何保证性能?

  1. DFA算法:时间复杂度O(n),与词库数量无关
  2. 分类加载:按场景加载不同词库
  3. 多级缓存:本地缓存 + Redis
  4. 异步处理:非关键场景异步过滤
  5. 预热:启动时预热DFA树

Q3:如何更新敏感词库不影响线上服务?

// 1. 双Buffer机制
private volatile DFASensitiveFilter activeFilter;
private DFASensitiveFilter standbyFilter;

public void reload() {
    // 在备用filter上重建
    standbyFilter = new DFASensitiveFilter();
    loadWords(standbyFilter);
    
    // 原子切换
    activeFilter = standbyFilter;
}

// 2. 增量更新
public void addWord(String word) {
    // 直接添加到active filter
    activeFilter.addWord(word);
}

📝 总结

方案选型

┌─────────────────────────────────────────────┐
│         敏感词过滤方案选型指南               │
└─────────────────────────────────────────────┘

场景1: 词库<100,低频调用
  → 正则表达式

场景2: 词库<10000,中等并发
  → Trie前缀树

场景3: 词库>10000,高并发
  → DFA算法(推荐)

场景4: 需要AI识别变形
  → DFA + NLP模型

关键要点 🎯

  1. 算法选择:DFA > Trie > 正则 > 暴力
  2. 性能优化:缓存 + 异步 + 预热
  3. 变形对抗:标准化 + 拼音 + 相似度
  4. 动态更新:热加载 + 双Buffer
  5. 日志审计:记录命中词,便于优化

祝你的系统永远"干净"! 🎉🎉🎉