5.过滤敏感词 + 发布帖子 + 帖子详情
目录
1.过滤敏感词
- 前缀树:名称(Tire、字典树、查找树)、特点(查找效率高)、应用(字符串检索、词频统计、字符串排序登)
- 敏感词过滤器:定义前缀树、根据敏感词初始化前缀树、编写过滤敏感词的方法
首先定义敏感词:在 resources 资源文件中新建文件 sensitive-words.txt:
赌博
嫖娼
吸毒
开票
实现上述算法(在 util 包下新建 SensitiveFilter 类)
1.1 定义前缀树
- 添加注解 @Component
- 首先定义前缀树:对于关键词结束进行一个标识
- 提供 get 和 set 方法
- 定义子节点(key 是下级字符、value 是下级节点)
- 添加子节点的方法
- 获取子节点方法
package com.example.demo.util;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class SensitiveFilter {
//定义前缀树
private class TrieNode {
//对于关键词结束进行一个标识
private boolean isKeywordEnd = false;
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
//定义子节点(key 是下级字符、value 是下级节点)
private Map<Character, TrieNode> subNodes = new HashMap<>();
// 添加子节点
public void addSubNode(Character c, TrieNode node) {
subNodes.put(c,node);
}
// 获取子节点
public TrieNode getSubNode(Character c) {
return subNodes.get(c);
}
}
}
1.2 根据敏感词,初始化前缀树
在?SensitiveFilter 类添加:
- 定义日志
- 定义常量:当我们检测到敏感词的时候,需要把敏感词替换成这个常量
- 初始化根节点
- 根据配置文件构造树型:在初次使用工具时自动初始化,添加注解 @PostConstruct ——当容器实例化 Bean 之后,在调用构造器之后,这个方法自动被调用
- 初始化树:读取文件字符(在 class 中读取配置文件,此时是一个字节流,需要在 finally 中自动关闭,添加 try / catch,在 try 中开启,编译自动添加 finally 进行关闭,出现异常的时候添加日志;从字节流中读取字符不好,转化成字符流,再转化为缓冲流效率比较高)
- 读取敏感词,每次读取敏感词放入一个变量中,成功读取到放入前缀树中
- 接下来将一个敏感词添加到前缀树中:创建一个节点默认指向根节点,遍历字符,试图获取子节点,如果为空,初始化子节点,放在当前节点处;如果不为空直接使用
- 处理下一节点时,使指针指向子节点,进行下一轮循环
- 设置结束标识
package com.example.demo.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 final String REPLACEMENT = "***";
//初始化根节点
private TrieNode rootNode = new TrieNode();
//根据配置文件构造树型:在初次使用工具时自动初始化,添加注解 @PostConstruct
// 当容器实例化 Bean 之后,在调用构造器之后,这个方法自动被调用
@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 = rootNode;
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);
}
}
}
}
1.3 编写过滤敏感词方法
在?SensitiveFilter 类添加:
- 可以被外界调用,是一个 public 方法
- 返回的是 String 过滤敏感词后的字符串
- 首先判断传入字符串的是否为空,为空直接输出 null
- 不为空,定义三个指针(上述图中的逻辑)
- 第一个指针指向根节点;第二、三个指针指向字符串开始的地方
- 定义变量记录最终结果,需要不断追加,使用 StringBuilder
- 运行算法:指针三需要遍历到结尾
- 得到某一个字符:不着急判断,首先必须要跳过符合(会存在??敏??感??词,这种不连接的,如果识别不了是不可行的)
- 判断是否为符号:CharUtils.isAsciiAlphanumeric 判断是不是一个合法字符、0x2E80~0x9FFF 是东亚文字范围也要排除
- 跳过字符之后,若指针1处于根节点,将此符号计入结果,让指针2向下走一步,但是无论符号在开头或中间,指针3都向下走一步
- 进入下一轮循环
- 检查下级节点:首先获取下级节点
- 情况一:若没有下级节点,则以begin开头的字符串不是敏感词,进入下一个位置;指针1重新指向根节点
- 情况二:若发现敏感词,将指针二和指针三之间字符串替换掉,并且使指针三进入下一个位置,此时设置指针二和指针三位置相同,指针一指向根节点
- 情况三:检测途中没有检测完,也没有发现符号,需要继续往下执行,则指针三进入下一个位置
- 没有敏感词时将最后一批字符计入结果
- 最后返回字符串
/**
* 过滤敏感词
*
* @param text 待过滤的文本
* @return 过滤后的文本
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 指针1
TrieNode tempNode = rootNode;
// 指针2
int begin = 0;
// 指针3
int position = 0;
// 定义变量记录最终结果,需要不断追加,使用 StringBuilder
StringBuilder sb = new StringBuilder();
//指针三需要遍历到结尾
while (position < text.length()) {
//得到某一个字符
char c = text.charAt(position);
// 跳过符号(会存在??敏??感??词,这种不连接的,如果识别不了是不可行的)
if (isSymbol(c)) {
// 若指针1处于根节点,将此符号计入结果,让指针2向下走一步
if (tempNode == rootNode) {
sb.append(c);
begin++;
}
// 无论符号在开头或中间,指针3都向下走一步
position++;
continue;
}
// 检查下级节点
//获取下级节点
tempNode = tempNode.getSubNode(c);
//若没有下级节点,则以begin开头的字符串不是敏感词,进入下一个位置;指针1重新指向根节点
if (tempNode == null) {
// 以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一个位置
position = ++begin;
// 重新指向根节点
tempNode = rootNode;
} else if (tempNode.isKeywordEnd()) {
//若发现敏感词,将指针二和指针三之间字符串替换掉,并且使指针三进入下一个位置,
//此时设置指针二和指针三位置相同,指针一指向根节点
// 发现敏感词,将begin~position字符串替换掉
sb.append(REPLACEMENT);
// 进入下一个位置
begin = ++position;
// 重新指向根节点
tempNode = rootNode;
} else { //:检测途中没有检测完,也没有发现符号,需要继续往下执行,则指针三进入下一个位置
// 检查下一个字符
position++;
}
}
//没有敏感词时 将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
// 判断是否为符号
private boolean isSymbol(Character c) {
// 0x2E80~0x9FFF 是东亚文字范围
//CharUtils.isAsciiAlphanumeric 判断是不是一个合法字符
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
新建测试类:
package com.example.demo;
import com.example.demo.util.SensitiveFilter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = DemoApplication.class)
public class SensitiveTests {
@Autowired
private SensitiveFilter sensitiveFilter;
@Test
public void testSensitiveFilter() {
String text = "这里可以赌博,可以嫖娼,可以吸毒,可以开票";
text = sensitiveFilter.filter(text);
System.out.println(text);
text = "这里可以☆赌☆博☆,可以☆嫖☆娼☆,可以☆吸☆毒☆,可以☆开☆票☆,哈哈哈!";
text = sensitiveFilter.filter(text);
System.out.println(text);
}
}
2.发布帖子
- AJAX:Asynchronous JavaScript and XML
- 采用 AJAX 请求,实现发布帖子的功能
添加 AJAX 的 pom.xml:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
在 util 包下的 Community 类下添加方法:
- 添加处理字符串的 JSON 方法:给浏览器返回编号、提示信息、业务数据
- 返回 JSON 格式的字符串
- 封装参数到 JSON 中,并把值装入
- 有可能只有编号,有可能没有提示信息或者业务数据,那么添加两个重载方法便于调用
//添加处理字符串的 JSON 方法:给浏览器返回编号、提示信息、业务数据
public static String getJSONString(int code, String msg, Map<String, Object> map) {
JSONObject json = new JSONObject();
json.put("code", code);
json.put("msg", msg);
if (map != null) {
for (String key : map.keySet()) {
json.put(key, map.get(key));
}
}
return json.toJSONString();
}
//可能只有编号,有可能没有提示信息或者业务数据,那么添加两个重载方法便于调用
public static String getJSONString(int code, String msg) {
return getJSONString(code, msg, null);
}
public static String getJSONString(int code) {
return getJSONString(code, null, null);
}
2.1 数据访问层
添加一个帖子的方法返回增加的行数,在 dao 包下的 DiscussPostMapper 类下追加一个方法:
//添加一个帖子的方法
int insertDiscussPost(DiscussPost discussPost);
在 resources 资源文件下 mapper 包下的 discusspost-mapper.xml 中实现方法:
- 定义sql,将字段声明
<sql id="insertFields">
user_id, title, content, type, status, create_time, comment_count, score
</sql>
<insert id="insertDiscussPost" parameterType="DiscussPost">
insert into discuss_post(<include refid="insertFields"></include>)
values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
</insert>
2.2 业务层
业务层需要提供一个保存帖子的业务方法(并且使用敏感词的方法),在 service 包下的?DiscussPostService 类中追加一个方法:
- 对添加的数据进行敏感词过滤和标签替换:需要对 title 和 context 进行处理
//注入敏感词
@Autowired
private SensitiveFilter sensitiveFilter;
//添加一个保存帖子的业务方法,返回添加的行数
public int addDiscussPost(DiscussPost post) {
if (post == null) {
throw new IllegalArgumentException("参数不能为空");
}
//处理标签:转义 HTML
post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
post.setContent(HtmlUtils.htmlEscape(post.getContent()));
//过滤敏感词
post.setTitle(sensitiveFilter.filter(post.getTitle()));
post.setContent(sensitiveFilter.filter(post.getContent()));
return discussPostMapper.insertDiscussPost(post);//实现插入数据,调用方法
}
2.3 视图层
实现增加帖子功能,在 controller 包下新建 DiscussPostController 类(实现增加帖子功能):
- 添加注解 @Controller、@RequsetMapping 访问路径
- 实现功能,先注入 DiscussPostService 添加帖子的方法
- 处理增加帖子的请求,是一个异步请求:声明路径;增加数据,浏览器提交数据,提交的方式为 POST
- 添加 @ResponseBody 表示返回字符串
- 获取当前用户,注入 HostHolder
- 添加方法,返回字符串,传入标题(title)和内容(content)
- 发帖子前提是登陆:在?HostHolder 中取对象,如果取到的对象为空(未登录),给页面一个 403 提示(异步的,是 json 格式的数据)
- 登陆成功之后需要调用 DiscussPostService 保存帖子:构造实体:创建 post 对象,传入 UserId、title、content、处理时间
- 最后返回正确的状态(0)
- 如果报错的情况,将来统一处理,默认是正确情况
package com.example.demo.controller;
import com.example.demo.entity.DiscussPost;
import com.example.demo.entity.User;
import com.example.demo.service.DiscussPostService;
import com.example.demo.util.CommunityUtil;
import com.example.demo.util.HostHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
/**
* 实现增加帖子功能
*/
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
//实现功能,先注入 DiscussPostService 添加帖子的方法
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody //添加 @ResponseBody 表示返回字符串
//添加方法,返回字符串,传入标题(title)和内容(context)
public String addDiscussPost(String title, String content) {
//发帖子前提是登陆:在 HostHolder 中取对象,如果取到的对象为空(未登录),给页面一个 403 提示(异步的,是 json 格式的数据)
User user = hostHolder.getUser();
if (user == null) {
return CommunityUtil.getJSONString(403, "你还没有登陆");
}
//登陆成功之后需要调用 DiscussPostService 保存帖子:构造实体:创建 post 对象,传入 UserId、title、content、处理时间
DiscussPost post = new DiscussPost();
post.setUserId(user.getId());
post.setTitle(title);
post.setContent(content);
post.setCreateTime(new Date());
discussPostService.addDiscussPost(post);
// 报错的情况,将来统一处理.
return CommunityUtil.getJSONString(0, "发布成功!");
}
}
处理页面:
index.xml:
<button type="button" class="btn btn-primary btn-sm position-absolute rt-0" data-toggle="modal" data-target="#publishModal" th:if="${loginUser!=null}">我要发布</button>
js 中的 index.js:
$(function(){
$("#publishBtn").click(publish);
});
function publish() {
$("#publishModal").modal("hide");
// 获取标题和内容
var title = $("#recipient-name").val();
var content = $("#message-text").val();
// 发送异步请求(POST)
$.post(
CONTEXT_PATH + "/discuss/add",
{"title":title,"content":content},
function(data) {
data = $.parseJSON(data);
// 在提示框中显示返回消息
$("#hintBody").text(data.msg);
// 显示提示框
$("#hintModal").modal("show");
// 2秒后,自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
// 刷新页面
if(data.code == 0) {
window.location.reload();
}
}, 2000);
}
);
}
3.帖子详情
3.1 数据访问层
增加查看帖子的方法,在 dao 包下的 DiscussPostMapper 类下追加一个方法:
- 根据帖子 id,查询帖子详情,返回一个帖子
//根据帖子 id,查询帖子详情,返回一个帖子
DiscussPost selectDiscussPostById(int id);
在 resources 资源文件下 mapper 包下的 discusspost-mapper.xml 中实现方法:
- 添加 select 标签
- 根据 id 查询
<select id="selectDiscussPostById" resultType="DiscussPost">
select <include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
3.2 业务层
增加查询方法,在 service 包下的?DiscussPostService 类中追加一个方法:
- 根据 id 查询 帖子方法
//根据 id 查询 帖子方法
public DiscussPost findDiscussPostById(int id) {
return discussPostMapper.selectDiscussPostById(id);
}
3.3 视图层
查询请求,在 controller 包下的DiscussPostController 类中新增处理查询请求:
- 声明查询路径,请求方式为 GET 请求
- 使用 @PathVariable 取动态的值,需要将查询到的结果发送给模板
- 查询帖子,传入模板中
- 用户 id 需要处理,因为在页面要显示帖子的作者,不是显示 id,需要显示用户的头像,名字这样的信息
- 第一种方法:在查询的时候使用关联查询 。优点:查询快;缺点:可能存在冗余、耦合
- 第二种方法:先查出帖子数据,根据 id 调用 Userservice 查询 User,再通过 Model 将 User 发送给 模板,这样模板得到了帖子,也得到了模板。优点:查询两次,没有冗余;缺点:查询慢
- 在这里使用第二种情况,查询慢可以使用 Redis 来优化
- 查询 帖子的作者,注入 UserService
- 把作者传给模板
- 最后返回模板路径
@Autowired
private UserService userService;
//查询请求方法
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model) {
// 帖子
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
//第一种方法:在查询的时候使用关联查询 。优点:查询快;缺点:可能存在冗余、耦合
//第二种方法:先查出帖子数据,根据 id 调用 Userservice 查询 User,再通过 Model 将 User 发送给 模板,
// 这样模板得到了帖子,也得到了模板。优点:查询两次,没有冗余;缺点:查询慢
//在这里使用第二种情况,查询慢可以使用 Redis 来优化
// 作者
User user = userService.findUserById(post.getUserId());
// 把作者传给模板
model.addAttribute("user", user);
return "/site/discuss-detail";
}
index.xml:在帖子标题上增加访问详情页面的链接
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">学习 Java</a>
disucss-detail.html:处理静态资源的访问路径、复用 index.xml 的header 区域、显示标题、作者、发布时间、帖子正文等内容
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!