avatar

目录
牛客网后端项目实战(十五):过滤敏感词

本节内容:

  • 前缀树
    • 名称:Trie、字典树、查找树
    • 特点:查找效率高,消耗内存大
    • 应用:字符串检索、词频统计、字符串排序等
  • 敏感词过滤器
    • 定义前缀树
    • 根据敏感词,初始化前缀树
    • 编写过滤敏感词的方法

前缀树示例

  1. 假设3个敏感词,abc、bf、be,画出根节点,从敏感词中分析出第一层

  2. 继续分析后续字母,每一层对应敏感词第几个字母

  3. 遍历到中途的时候不是敏感词,到最底层才是,比如ab不是,abc才是,在最底层做一个标记

  4. 双指针检测,第一个指针判断是否是敏感词开头,如果是,移动第二个指针往下查。

敏感词实现

敏感词我们存在文件里,在resource目录下新建sensitive-words.txt,随意写几个敏感词。

敏感词过滤作为一个工具类,在util包下新建SensitiveFilter。使用@Component注解交给容器管理。定义好日志记录和敏感词替换的内容。

java
1
2
3
4
5
6
7
@Component
public class SensitiveFilter {

private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//替换符
private static String REPLACEMENT = "***";
}

前缀树,树结构通常定义成节点,在SensitiveFilter里定义一个内部类TrieNode。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//前缀树
private class TrieNode {
//关键词结束标识
private boolean isKeywordEnd = false;

//子节点(key是下级字符,value是下级节点)
private Map 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);
}
}

敏感词前缀树我们需要且只需要初始化一次,定义一个init方法,使用classLoader获取输入流,然后使用缓存字符流。一行一行的读取敏感词,将其添加到前缀树,添加到前缀树的方法定义为addKeyWord。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//根节点
private TrieNode root = new TrieNode();

@PostConstruct
public void init() {

try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword=reader.readLine())!=null){
//添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败:"+e.getMessage());
}

}

将读取到的敏感词传入方法中,新建一个tempNode作为指针,然后一个字符一个字符的添加。

首先在root的子结点中找敏感词中的第一个字符,如果找到,就用subNode指向它,没有就新建subNode,并添加到子节点中。然后将指针下移到subNode,如果遍历到最后一个字符就设置结束标识。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//将敏感词添加到前缀树当中
private void addKeyword(String keyword){
TrieNode tempNode=root;
for (int i=0;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);
}
}
}

然后写过滤敏感词的方法,这里有一个问题,有些用户会在敏感词中插入特殊字符来逃避检查。如果特殊字符是在检查的开头,我们不做处理,如果特殊字符在疑似敏感词中间,我们就要忽略掉这个特殊字符。

java
1
2
3
4
5
//判断是否为符号
private boolean isSymbol(Character c){
// 0x2E80-0x9FFF是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c<0x2E80||c>0x9FFF);
}

按照前面前缀树的逻辑,实现。三个指针,指针1指向前缀树,指针2、3指向需要过滤的字符串。

例如:“我要&开*票”

​ 指针1指向前缀树的根节点,指针2、3指向“我”,判断不是特殊字符,也不是敏感词开头,指针2、3指向要,同理,然后指向“&”,这时指针1指向root节点,说明这个特殊字符不在敏感词中间,将它拼接到结果中。然后指针2、3指向“开”,发现是敏感词开头,指针3后移指向“*”,判断是特殊字符,且此时指针1不是指向root,所以忽略掉这个特殊字符,指针3指向“票”,发现这个节点有end标记,说明从开到票是一个敏感词。

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 过滤敏感词
* @param text 待过滤文本
* @return 过滤后的文本
*/
public String filter(String text){
if (StringUtils.isBlank(text)){
return null;
}

//指针1
TrieNode tempNode=root;

//指针2
int begin=0;
//指针3
int position=0;
//结果
StringBuilder sb=new StringBuilder();

while (position
char c=text.charAt(position);

//跳过符号
if (isSymbol(c)){
//若指针1处于根节点,将此符号计入结果,让指针2向下走一步
if (tempNode==root){
sb.append(c);
begin++;
}
//无论符号在开头或中间,指针3都向下走一步
position++;
continue;

}

//检查下级节点
tempNode=tempNode.getSubNode(c);
if (tempNode==null){
//以begin开头的字符不是敏感词
sb.append(text.charAt(begin));
//进入下一个位置
position=++begin;
//重新指向根节点
tempNode=root;
}else if (tempNode.isKeywordEnd()){
//发现敏感词,将begin-position字符串替换掉
sb.append(REPLACEMENT);
begin=++position;
//重新指向根节点
tempNode=root;
}else {
//检查下一个字符
position++;
}
}
//将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}

完整代码:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package com.neu.langsam.community.util;

import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

@Component
public class SensitiveFilter {

private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//替换符
private static String REPLACEMENT = "***";

//根节点
private TrieNode root = new TrieNode();

@PostConstruct
public void init() {

try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword=reader.readLine())!=null){
//添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败:"+e.getMessage());
}

}


//将敏感词添加到前缀树当中
private void addKeyword(String keyword){
TrieNode tempNode=root;
for (int i=0;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);
}
}
}


/**
* 过滤敏感词
* @param text 待过滤文本
* @return 过滤后的文本
*/
public String filter(String text){
if (StringUtils.isBlank(text)){
return null;
}

//指针1
TrieNode tempNode=root;

//指针2
int begin=0;
//指针3
int position=0;
//结果
StringBuilder sb=new StringBuilder();

while (position
char c=text.charAt(position);

//跳过符号
if (isSymbol(c)){
//若指针1处于根节点,将此符号计入结果,让指针2向下走一步
if (tempNode==root){
sb.append(c);
begin++;
}
//无论符号在开头或中间,指针3都向下走一步
position++;
continue;

}

//检查下级节点
tempNode=tempNode.getSubNode(c);
if (tempNode==null){
//以begin开头的字符不是敏感词
sb.append(text.charAt(begin));
//进入下一个位置
position=++begin;
//重新指向根节点
tempNode=root;
}else if (tempNode.isKeywordEnd()){
//发现敏感词,将begin-position字符串替换掉
sb.append(REPLACEMENT);
begin=++position;
//重新指向根节点
tempNode=root;
}else {
//检查下一个字符
position++;
}
}
//将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}


//判断是否为符号
private boolean isSymbol(Character c){
// 0x2E80-0x9FFF是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c<0x2E80||c>0x9FFF);
}

//前缀树
private class TrieNode {
//关键词结束标识
private boolean isKeywordEnd = false;

//子节点(key是下级字符,value是下级节点)
private Map 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);
}
}

}

新建一个test看一下结果,发现结果正确。

文章作者: langsam
文章链接: https://langsam1998.github.io/2020/04/20/20200328-%E7%89%9B%E5%AE%A2%E7%BD%91%E5%90%8E%E7%AB%AF%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%EF%BC%88%E5%8D%81%E4%BA%94%EF%BC%89%EF%BC%9A%E8%BF%87%E6%BB%A4%E6%95%8F%E6%84%9F%E8%AF%8D/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 平儿的博客
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论