主要通过前缀树实现,敏感词过滤。
前缀树通过,类TreeNode,中的属性HashMap实现,hashmap中的value值又是个TreeNode。这样就形成了一个类似树的结构。往树上正确添加结点很重要
然后字符串与前缀树的单词匹配 是通过双指针实现的。具体看代码吧
1.当没有匹配到敏感词时,begin指针++,position指向begin。并且把这个字符加上res结果集。
2.当匹配到第一个敏感词时,就继续往后匹配,通过position++,往后begin暂时不动,直到匹配到最后一个叶子结点。把结果集变成***,然后position++,begin跟上position,从刚匹配到的敏感词后开始。
如果最后不是敏感词,begin++,position = begin。
第三个难点,特殊字符,比如❥。这种不能干扰敏感词的判断
1.如果特殊字符位于字符首,只要begin++就行 position = begin。然后把特殊字符加入就行。
2.特殊字符位于敏感词中间,只要position++就行。然后continue
/**
* @Description:
* @Author: cjh
* @data: Version 1.0
*/
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
private static final String REPLACE = "***";
//root
private TreeNode rootNode = new TreeNode();
/**
*这个注解表示这是个初始化方法,当容器实例化这个bean之后,这个方法会被自动调用。在服务器启动时就会调用
* 启动玩服务器就能对敏感词筛选了
*/
@PostConstruct()
public void init(){
try (
//获取类加载器,默认从类路径下加载资源。就是编译后的classes下
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
//读文字用字符流,但是字符缓冲流更快
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(resourceAsStream));
){
String keyWord;
//这个表示一次读一行
while ((keyWord = bufferedReader.readLine()) != null){
//添加到前缀树
this.addKeyWord(keyWord);
}
}catch (IOException e){
logger.error("加载敏感词文件失败:"+ e.getMessage());
}
}
//添加一个词在前缀树上
private void addKeyWord(String keyWord) {
TreeNode root = rootNode;
for (int i = 0; i < keyWord.length(); i++) {
char c = keyWord.charAt(i);
//先看下有没有创建
TreeNode next = root.getNextNode(c);
if (next == null){
next = new TreeNode();
root.addNextNode(c,next);
}
//指向子节点,进行下次循环
root = next;
//设置结束标志
if (i == keyWord.length() - 1){
root.setKeyWordEnd(true);
}
}
}
/**
* 过滤开始
*/
public String filter(String text){
if (StringUtils.isBlank(text)){
return null;
}
//指针1,指向树的用于遍历
TreeNode tempNode = rootNode;
//指针2,3用于字符串的遍历比较
int begin = 0;
int position = 0;
//结果
StringBuilder stringBuilder = new StringBuilder();
while (begin < text.length()){
if (position < text.length()){
char c = text.charAt(position);
//跳过特殊字符
if (isSymbol(c)){
//若指针1处于根节点,将此符号计入结果,让指针2向下走一步
if (tempNode == rootNode){
stringBuilder.append(c);
begin++;
}
//无论符号在哪,前面还是中间指针3都往下走
position++;
continue;
}
//检查下级节点
tempNode = tempNode.getNextNode(c);
if(tempNode == null){
//没有检查到敏感词
stringBuilder.append(text.charAt(begin));
//下个位置
position = ++begin;
//重新指向root节点
tempNode = rootNode;
}else if (tempNode.isKeyWordEnd()){
//发现敏感词
stringBuilder.append(REPLACE);
begin = ++position;
tempNode = rootNode;
}else{
//检查到敏感词了,但是还在检查的途中。当字符串到了结尾,循环结束但是,没有添加结尾
position++;
}
}
// position遍历越界仍未匹配到敏感词.此时begin未越界,可能begin后面后面还有没匹配的敏感词
//所以不能退出循环和
else {
//没有检查到敏感词
stringBuilder.append(text.charAt(begin));
//下个位置
position = ++begin;
//重新指向root节点
tempNode = rootNode;
}
}
//所以我们要在循坏外加
stringBuilder.append(text.substring(begin));
return stringBuilder.toString();
}
//判断是否为符号,防止敏感词干扰。比如五角星啥的
private boolean isSymbol(Character c){
//是特殊符号,返回false.这个范围是东亚文字。特殊字符返回true。数字字母都不是特殊字符
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
private class TreeNode{
//关键词结束标识
private boolean isKeyWordEnd = false;
//子节点。key下一个节点的字,value下一个节点
private Map<Character,TreeNode> nextNode = new HashMap<>();
public boolean isKeyWordEnd() {
return isKeyWordEnd;
}
public void setKeyWordEnd(boolean keyWordEnd) {
isKeyWordEnd = keyWordEnd;
}
//添加子节点
public void addNextNode(Character c,TreeNode node){
nextNode.put(c,node);
}
//获取子节点
public TreeNode getNextNode(Character c){
return nextNode.get(c);
}
}
}