掘金 后端 ( ) • 2024-04-15 13:31

前言

在社交化的电商项目中,很多用户提交的信息涉及敏感性,比如:用户评论、用户发表的动态等。目前平台已经介入第三方平台的Saas服务,在线过滤用户提交的敏感词信息。然后,在实际使用过程中存在一些敏感词漏处理的情况,从而导致平台声誉受损。基于此,我们准备在平台增加一个自定义敏感词过滤的功能模块,对于第三方敏感词服务漏处理的敏感词,我们将通过后台配置在敏感词库中,并进行补充处理。

需求详细分析

基于上述需求背景描述,可以分析得出这个需求的几个关键点:

  1. 平台自定义的敏感词库数据量不大。平台自定义的敏感词库只需要补充第三方敏感词服务漏处理的敏感词,所以该库数据量应该不会很大;
  2. 自定义敏感词处理逻辑在第三方敏感词服务校验之后执行;
  3. 自定义敏感词需要尽快生效。一旦发现漏处理的敏感词,立即通过后台配置到自定义敏感词库,快速生效,避免下次再在平台出现;

敏感词处理方案分析

设计高效的敏感词匹配算法可以帮助您快速准确地过滤用户提交的信息。以下是一些常用的高效敏感词匹配算法:

  1. Trie 树算法:Trie 树(字典树)是一种常见的敏感词匹配算法。它将敏感词库构建成一个树形结构,每个节点表示一个字符。通过遍历输入文本并在 Trie 树中匹配字符,可以快速找到是否存在敏感词。Trie 树算法的时间复杂度为 O(n),其中 n 是待匹配文本的长度。

  2. Aho-Corasick 算法:Aho-Corasick 算法是一种改进的多模式匹配算法,可以同时匹配多个模式(敏感词)。它将敏感词库构建成一个状态转移图,并利用自动机的方式进行匹配。Aho-Corasick 算法的时间复杂度为 O(m + n + z),其中 m 是敏感词库的总长度,n 是待匹配文本的长度,z 是匹配到的敏感词数量。

  3. 双数组 Trie 算法:双数组 Trie(Double-Array Trie)是 Trie 树的一种空间优化版本。它使用两个数组来表示 Trie 树的节点,减少了空间占用。双数组 Trie 算法在构建 Trie 树时需要较长的预处理时间,但在匹配过程中具有较高的效率。

  4. Bloom Filter 算法:Bloom Filter 是一种概率型数据结构,可以用于快速判断一个元素是否存在于集合中。您可以将敏感词库构建成一个 Bloom Filter,然后在用户提交的信息中进行匹配。Bloom Filter 算法的特点是空间效率高,但存在一定的误判率。

  5. 字符串匹配:将敏感词存入数组或集合中,然后循环遍历敏感词,判断待处理文本是否contains敏感词,一旦匹配则终止循环,这种方式适用于敏感词很少并且对性能要求不高的情况。

根据具体的需求和场景,您可以选择适合的敏感词匹配算法。一般来说,Trie 树和 Aho-Corasick 算法在敏感词匹配方面具有较高的效率和准确性。如果敏感词库较大,可以考虑使用双数组 Trie 算法来降低空间占用。如果对准确性要求不是特别高,可以考虑使用 Bloom Filter 算法来加快匹配速度。

方案选择

根据场景需求,笔者在实际项目采用了第5中方法:自定义敏感词通过字典功能进行配置。执行过滤时先从数据库加载自定义敏感词库,并进行缓存5min,这样可以避免每次校验都从数据库加载自定义敏感词库,同时又能在一定时间(5min)内获得最新的敏感词库,让最新添加的敏感词过滤生效。

/**
 * 从数据库或缓存加载敏感词库,默认缓存5min
 * @return
 */
private Set<String> getSensitiveWords() {
	if(redisTemplate.hasKey(SENSITIVE_WORDS_KEY)) {
		return redisTemplate.opsForSet().members(SENSITIVE_WORDS_KEY);
	}

	Set<String> sensitiveWords = new HashSet<>();

	try {
		R<List<SysDictItem>> r = dictService.getDictByType("sensitive_words");
		if(CommonConstants.SUCCESS.equals(r.getCode())) {
			sensitiveWords = r.getData().stream().map(SysDictItem::getItemValue).collect(Collectors.toSet());
			redisTemplate.opsForSet().add(SENSITIVE_WORDS_KEY, sensitiveWords);
			redisTemplate.expire(SENSITIVE_WORDS_KEY, 5, TimeUnit.MINUTES);
		}
	} catch (Exception e) {
		log.warn("远程获取敏感词信息失败!", e);
	}

	return sensitiveWords;
}

// 具体过滤逻辑代码
Set<String> sensitiveWords = getSensitiveWords();
// 自定义敏感词作为第三方敏感词服务的补充,词库数量应该不会很大,因此检验方式比较简单粗暴
if(sensitiveWords.stream().anyMatch(word -> text.contains(word))) return Boolean.FALSE;

当然,如果自定义敏感词库数据量比较大,上述方案则存在性能问题,建议采用Trie 树算法,也叫DFA(确定有穷自动机)算法,可以参考Hutool提供的算法实现工具类来实现敏感词过滤,参考《基于Hutool-DFA实现内容敏感词过滤》

补充

文章发布之后,后续开发联调过程中发现上述代码存在错误,具体问题请查看《RedisTemplate批量往集合Set添加元素出现的转换异常问题分析》