目录树
🌕开发社区核心功能
1.过滤敏感词
比如我们平时浏览网站的时候,网站会对内容进行过滤 —— 暴力、色情等进行过滤,进行打码或者隐藏;这些就是过滤敏感词!
技术角度
:对字符串进行过滤,输入的内容就是一个大的字符串;调用某些API去判断字符串里有没敏感词
可以用JDK自带的方法string的replace()方法;
在网站中,敏感词很多(几十上百个),替换太麻烦,所以实际过程中,
我们会采用前缀树,这一种数据结构自己来实现一个敏感词过滤的算法!
前缀树过滤敏感词的算法逻辑
特点:
- 除了根节点外的每一个节点,只包含一个字符,根节点为空;
- 从根节点开始,到某一个节点c,它会途径a和b,那么途径的字符按顺序连起来就是过滤的字符
- 每个节点的所有子节点包含的字符不相同
工作流程图:
1.1 定义前缀树
- 先定义好敏感词,敏感词可以写在数据库里,也可以写在文件里;
- 然后定义一个工具类用来描述定义前缀树,因为后续其他功能点也会用到敏感词过滤,比如评论,帖子等等;
1.1.1 SensitiveFilter
@Component
public class SensitiveFilter {
//定义一个内部类,不允许外界访问
//这就是前缀树的算法逻辑
private class TireNode{
//1.关键词结束标识 —— 也就是图解中打了x的底部元素
private boolean isKeywordEnd = false;
//2.子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//get、setter方法
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
//3.添加子节点的方法
public void addSubNode(Character c,TireNode node){
subNodes.put(c, node);
}
//4.获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
}
}
1.2 根据敏感词,初始化前缀树
其实就是在上述代码中的内部上面添加了代码,具体如下;
SensitiveFilter
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//敏感词的替换符
private static final String REPLACEMENT = "***";
//根节点初始化
private TireNode rootNode = new TireNode();
@PostConstruct //表示这是一个初始化方法;
public void init(){
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
//这是一个字节流,从字节流读文字不太方便,所以转为字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));//缓冲流
) {
String keywords; //每次读取的词都存到变量keywords里
while ((keywords = reader.readLine()) != null){
//添加到前缀树
this.addKeyword(keywords);
}
}catch (IOException e){
logger.error("加载敏感词文件失败!" + e.getMessage());
}
}
//方法定义:将一个敏感词添加到前缀树当中去
private void addKeyword(String keyword){
TireNode tempNode = rootNode;
//tempNode临时节点,相当于一个指针,默认先等于根节点,然后根据单词拆解每个字符不断地指向下一个字符,不断构造树的下一级;
for (int i=0;i<keyword.length();i++){
char c = keyword.charAt(i); //得到一个字符
TireNode subNode = tempNode.getSubNode(c); //
if (subNode == null){
//初始化子节点
subNode = new TireNode();
tempNode.addSubNode(c,subNode);
}
//指针指向子节点,进入下一轮循环
tempNode = subNode;
//设置结束标识
if (i == keyword.length()-1){
tempNode.setKeywordEnd(true);
}
}
}
//定义一个内部类,不允许外界访问
//这就是前缀树的算法逻辑
private class TireNode{
//1.关键词结束标识 —— 也就是图解中打了x的底部元素
private boolean isKeywordEnd = false;
//2.子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//get、setter方法
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
//3.添加子节点的方法
public void addSubNode(Character c,TireNode node){
subNodes.put(c, node);
}
//4.获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
}
}
1.3 编写过滤敏感词的方法
SensitiveFilter
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
//敏感词的替换符
private static final String REPLACEMENT = "***";
//根节点初始化
private TireNode rootNode = new TireNode();
//2.
@PostConstruct //表示这是一个初始化方法;
public void init(){
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
//这是一个字节流,从字节流读文字不太方便,所以转为字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(is));//缓冲流
) {
String keywords; //每次读取的词都存到变量keywords里
while ((keywords = reader.readLine()) != null){
//添加到前缀树
this.addKeyword(keywords);
}
}catch (IOException e){
logger.error("加载敏感词文件失败!" + e.getMessage());
}
}
//2.1 方法定义:将一个敏感词添加到前缀树当中去
private void addKeyword(String keyword){
TireNode tempNode = rootNode;
//tempNode临时节点,相当于一个指针,默认先等于根节点,然后根据单词拆解每个字符不断地指向下一个字符,不断构造树的下一级;
for (int i=0;i<keyword.length();i++){
char c = keyword.charAt(i); //得到一个字符
TireNode subNode = tempNode.getSubNode(c); //
if (subNode == null){
//初始化子节点
subNode = new TireNode();
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(图解中的指向前缀树结构的指针)
TireNode tempNode = rootNode;
//指针2(图解中最上面开始的指向指针)
int begin = 0;
//指针3(图解中最下面用于扫描徘徊的指针)
int position = 0;
//结果封装(对扫描字符进行逐个封装,最后成为合法的字符串)
StringBuilder stringBuilder = new StringBuilder();
while (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.getSubNode(c);
if (tempNode == null){
//tempNode == null 节点为空,表示当前节点没有下级节点
//即以begin开头的字符串不是敏感词
stringBuilder.append(text.charAt(begin));
//进入下一个位置
position = ++begin;
//指针1重新指向根节点,进行下一轮判断
tempNode = rootNode;
}else if (tempNode.isKeywordEnd){
//发现了敏感词,将begin到position的字符串替换掉
stringBuilder.append(REPLACEMENT);
//让指针进3入下一个位置
begin = ++ position;
//指针1重新指向根节点,进行下一轮判断
tempNode = rootNode;
}else {
//检查下一个字符
position++;
}
}
//将最后一批字符计入结果
stringBuilder.append(text.substring(begin));
return stringBuilder.toString();
}
//方法:判断是否为符号
private boolean isSymbol(Character c){
//0x2E80 ~ 0x9FFF 是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c<0x2E80 || c>0x9FFF);
}
//1.定义一个内部类,不允许外界访问
//这就是前缀树的算法逻辑
private class TireNode{
//1.1 关键词结束标识 —— 也就是图解中打了x的底部元素
private boolean isKeywordEnd = false;
//1.2 子节点(key是下级字符,value是下级节点)
private Map<Character,TireNode> subNodes = new HashMap<>();
//get、setter方法
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
//1.3 添加子节点的方法
public void addSubNode(Character c,TireNode node){
subNodes.put(c, node);
}
//1.4 获取子节点
public TireNode getSubNode(Character c){
return subNodes.get(c);
}
}
}
1.4 SensitiveTests测试类测试
SensitiveTests
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class SensitiveTests {
@Autowired
private SensitiveFilter sensitiveFilter;
@Test
public void testSensitiveFilter(){
String text = "加入来吃饭啊!加入来睡觉啊!加入也能放假哦!";
text = sensitiveFilter.filter(text);
System.out.println(text);
String text1 = "加入来❤吃❤饭啊!加入来睡❤觉啊!加入也能❤放❤假❤哦!";
text = sensitiveFilter.filter(text);
System.out.println(text);
}
}
结果: