敏感词过滤
主要内容:
前缀树
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 应用:字符串检索、词频统计、字符串排序等
敏感词过滤器
- 定义前缀树
- 获取敏感词,根据敏感词,初始化前缀树
- 编写过滤敏感词的方法
实现功能:
传入字符串,过滤其中的敏感词字段,返回过滤后的字段和当前存在的敏感词集合
前缀树示例
1、假设有2个敏感词 ace bd,画出根节点,从敏感词中分析出第一层
2、继续分析后续字母,每一层对应敏感词第几个字母
3、在敏感词底层字母处做一个标记,遍历到中途的时候不是敏感词
4、双指针检测当一个指针查询到敏感词开头,另一个指针向下移动继续查询
敏感词实现
1、敏感词过滤作为一个工具类,在util包下新建SensitiveUtil。使用@Component注解交给容器管理。定义好日志记录和敏感词替换的内容。
@Component
public class SensitiveUtil {
private static Logger logger = LoggerFactory.getLogger(SensitiveUtil.class);
//替换符
private static String REPLACEMENT = "***";
}
2、定义前缀树,树结构通常定义成节点,在SensitiveUtil里定义一个内部类TrieNode。
/**
* @Description 前缀树
* @date 2021/11/12 10:34
*/
private class TrieNode {
//关键词结束标识
private boolean isKeywordEnd = false;
//子节点(key是下级字符,value是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
//添加子节点方法
public void addSubNode(Character key, TrieNode value) {
subNodes.put(key, value);
}
//获取子节点方法
public TrieNode getSubNode(Character key) {
return subNodes.get(key);
}
}
3、添加前缀树addKeyword
将读取到的敏感词传入方法中,新建一个tempNode作为指针,然后一个字符一个字符的添加。
首先在root的子结点中找敏感词中的第一个字符,如果找到,就用subNode指向它,没有就新建subNode,并添加到子节点中。然后将指针下移到subNode,如果遍历到最后一个字符就设置结束标识。
/**
* @param keyword
* @return void
* @Description 将敏感词添加到前缀树当中
*/
private static void addKeyword(String keyword) {
TrieNode tempNode = root;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if (subNode == null) {
//初始化子节点
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
//指向子节点,进入下一轮循环
tempNode = subNode;
//设置结束标识
if (i == keyword.length() - 1) {
tempNode.setKeywordEnd(true);
}
}
}
前缀树结构实例
4、获取敏感词
注:当前敏感词存于数据库中
敏感词前缀树需要每次过滤时初始化一次,考虑到优化问题,每次敏感词字段集从redis中获取(当前储存形式为List),这里为了方便,在类的构造方法里初始化敏感词
并且需要加入前缀树中,
public class SensitiveUtil {
//替换符
private static String REPLACEMENT = "***";
//根节点
private static TrieNode root;
Map redisUtil = new HashMap();
//redis中获取敏感词集合的key
private static final String SENSITIVE_WORDS = "sensitiveWords";
public SensitiveUtil() {
//初始化敏感词
List<String> sensitiveWords = new ArrayList<>();
sensitiveWords.add("赌博");
sensitiveWords.add("试水");
redisUtil.put(SENSITIVE_WORDS, sensitiveWords);
}
/**
* @param keyword
* @return void
* @Description 将敏感词添加到前缀树当中
*/
private static void addKeyword(String keyword) {
TrieNode tempNode = root;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if (subNode == null) {
//初始化子节点
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
//指向子节点,进入下一轮循环
tempNode = subNode;
//设置结束标识
if (i == keyword.length() - 1) {
tempNode.setKeywordEnd(true);
}
}
}
5、过滤阶段
按照前面前缀树的逻辑,实现。三个指针,指针1指向前缀树,指针2、3指向需要过滤的字符串。
例如:“我要&赌*博”
指针1指向前缀树的根节点,指针2、3指向"我",判断不是特殊字符,也不是敏感词开头,指针2、3指向"要",同理,然后指向"&“,这时指针1指向root节点,说明这个特殊字符不在敏感词中间,将它拼接到结果中。然后指针2、3指向"赌”,发现是敏感词开头,指针3后移指向"*",判断是特殊字符,且此时指针1不是指向root,所以忽略掉这个特殊字符,指针3指向“博”,发现这个节点有end标记,说明从"赌"到"博"是一个敏感词。
/**
* @param text 待过滤文本
* @return map {text=[过滤后的文本***], sensitiveWords=[我是敏感词]}
* @Description 过滤敏感词
*/
private static Map<String, List<String>> getfilterResult(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
//指针1 :指向root
TrieNode tempNode = root;
//指针2 :指向敏感词开头
int begin = 0;
//指针3 :指向敏感词后续字段
int position = 0;
//过滤结果
StringBuilder sb = new StringBuilder();
//敏感词集合
List<String> sensitiveWords = new ArrayList<>();
//返回结果
Map<String, List<String>> resultMap = new HashMap<>();
while (position < text.length()) {
char c = text.charAt(position);
//判断是否为符号,进行处理、防止用户插入特殊字符躲避敏感词检测
if (isSymbol(c)) {
if (tempNode == root) {
sb.append(c);
begin++;
}
position++;
continue;
}
//检查下级节点
tempNode = tempNode.getSubNode(c);
if (tempNode == null) {
sb.append(text.charAt(begin));
position = ++begin;
tempNode = root;
} else if (tempNode.isKeywordEnd()) {
//发现敏感词
String sensitiveWord = text.substring(begin, position + 1);
sensitiveWords.add(sensitiveWord);
sb.append(REPLACEMENT);
begin = ++position;
tempNode = root;
} else {
position++;
}
}
//将最后一批字符计入结果
sb.append(text.substring(begin));
//过滤后的文本
resultMap.put("text", Arrays.asList(sb.toString()));
resultMap.put(SENSITIVE_WORDS, sensitiveWords);
return resultMap;
}
/**
* @Description 判断是否为符号
* @date 2021/11/16 9:33
*/
private static boolean isSymbol(Character c) {
/**
* <dependency>
* <groupId>org.apache.commons</groupId>
* <artifactId>commons-lang3</artifactId>
* <version>3.8.1</version>
* </dependency>
*/
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
6、测试
已知敏感词为"赌博" 测试词汇"我要&赌*博"
打印结果:
测试成功
7、完整代码
package com.kaikai.test;
import org.apache.commons.lang3.CharUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
public class SensitiveUtil {
//替换符
private static String REPLACEMENT = "***";
//根节点
private static TrieNode root;
Map redisUtil = new HashMap();
//redis中获取敏感词集合的key
private static final String SENSITIVE_WORDS = "sensitiveWords";
public static void main(String[] args) {
SensitiveUtil util = new SensitiveUtil();
List<String> filter = util.filter("来,咱们一起来赌博好不好,性感荷官在线发牌噢,快来试试水吧");
System.out.printf(filter.toString());
}
public SensitiveUtil() {
//初始化敏感词
List<String> sensitiveWords = new ArrayList<>();
sensitiveWords.add("赌博");
sensitiveWords.add("试水");
redisUtil.put(SENSITIVE_WORDS, sensitiveWords);
}
/**
* @param text 待过滤的文本
* @return java.util.List
* @Description 返回敏感词字段集合 为空则返回null
* @date 2021/11/12 10:45
*/
public List<String> filter(String text) {
try {
//敏感词集合
List<String> sensitiveWords = new ArrayList<>();
sensitiveWords = (List<String>) redisUtil.get(SENSITIVE_WORDS);
root = new TrieNode();
for (String msgSensitiveWordName : sensitiveWords) {
//将敏感词加入到前缀树中
addKeyword(msgSensitiveWordName);
}
//真正的过滤阶段:返回结果集形式为map,包括过滤后的文本,以及敏感词汇集合
Map<String, List<String>> result = getfilterResult(text);
return result.get("text");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* @param text 待过滤文本
* @return map {text=[过滤后的文本***], sensitiveWords=[我是敏感词]}
* @Description 过滤敏感词
*/
private static Map<String, List<String>> getfilterResult(String text) {
if (StringUtils.isEmpty(text)) {
return null;
}
//指针1 :指向root
TrieNode tempNode = root;
//指针2 :指向敏感词开头
int begin = 0;
//指针3 :指向敏感词后续字段
int position = 0;
//过滤结果
StringBuilder sb = new StringBuilder();
//敏感词集合
List<String> sensitiveWords = new ArrayList<>();
//返回结果
Map<String, List<String>> resultMap = new HashMap<>();
while (position < text.length()) {
char c = text.charAt(position);
//判断是否为符号,进行处理、防止用户插入特殊字符躲避敏感词检测
if (isSymbol(c)) {
if (tempNode == root) {
sb.append(c);
begin++;
}
position++;
continue;
}
//检查下级节点
tempNode = tempNode.getSubNode(c);
if (tempNode == null) {
sb.append(text.charAt(begin));
position = ++begin;
tempNode = root;
} else if (tempNode.isKeywordEnd()) {
//发现敏感词
String sensitiveWord = text.substring(begin, position + 1);
sensitiveWords.add(sensitiveWord);
sb.append(REPLACEMENT);
begin = ++position;
tempNode = root;
} else {
position++;
}
}
//将最后一批字符计入结果
sb.append(text.substring(begin));
//过滤后的文本
resultMap.put("text", Arrays.asList(sb.toString()));
resultMap.put(SENSITIVE_WORDS, sensitiveWords);
return resultMap;
}
/**
* @Description 判断是否为符号
* @date 2021/11/16 9:33
*/
private static boolean isSymbol(Character c) {
/**
* CharUtils 可引入
* <dependency>
* <groupId>org.apache.commons</groupId>
* <artifactId>commons-lang</artifactId>
* </dependency>
*/
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
/**
* @param keyword
* @return void
* @Description 将敏感词添加到前缀树当中
*/
private static void addKeyword(String keyword) {
TrieNode tempNode = root;
for (int i = 0; i < keyword.length(); i++) {
char c = keyword.charAt(i);
TrieNode subNode = tempNode.getSubNode(c);
if (subNode == null) {
//初始化子节点
subNode = new TrieNode();
tempNode.addSubNode(c, subNode);
}
//指向子节点,进入下一轮循环
tempNode = subNode;
//设置结束标识
if (i == keyword.length() - 1) {
tempNode.setKeywordEnd(true);
}
}
}
/**
* 前缀树
*/
public static class TrieNode {
private boolean isKeywordEnd = false;
private Map<Character, TrieNode> subNodes = new HashMap<>();
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
public void addSubNode(Character key, TrieNode value) {
subNodes.put(key, value);
}
public TrieNode getSubNode(Character key) {
return subNodes.get(key);
}
}
}