当前位置:   article > 正文

5.过滤敏感词 + 发布帖子 + 帖子详情_消息敏感字过滤

消息敏感字过滤

目录

1.过滤敏感词

1.1 定义前缀树

1.2 根据敏感词,初始化前缀树

1.3 编写过滤敏感词方法

2.发布帖子

2.1 数据访问层

2.2 业务层

2.3 视图层

3.帖子详情

3.1 数据访问层

3.2 业务层

3.3 视图层


1.过滤敏感词

  • 前缀树:名称(Tire、字典树、查找树)、特点(查找效率高)、应用(字符串检索、词频统计、字符串排序登)
  • 敏感词过滤器:定义前缀树、根据敏感词初始化前缀树、编写过滤敏感词的方法

首先定义敏感词:在 resources 资源文件中新建文件 sensitive-words.txt:

  1. 赌博
  2. 嫖娼
  3. 吸毒
  4. 开票

实现上述算法(在 util 包下新建 SensitiveFilter 类

1.1 定义前缀树

  • 添加注解 @Component
  • 首先定义前缀树:对于关键词结束进行一个标识
  • 提供 get 和 set 方法
  • 定义子节点(key 是下级字符、value 是下级节点)
  • 添加子节点的方法
  • 获取子节点方法
  1. package com.example.demo.util;
  2. import org.springframework.stereotype.Component;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. @Component
  6. public class SensitiveFilter {
  7. //定义前缀树
  8. private class TrieNode {
  9. //对于关键词结束进行一个标识
  10. private boolean isKeywordEnd = false;
  11. public boolean isKeywordEnd() {
  12. return isKeywordEnd;
  13. }
  14. public void setKeywordEnd(boolean keywordEnd) {
  15. isKeywordEnd = keywordEnd;
  16. }
  17. //定义子节点(key 是下级字符、value 是下级节点)
  18. private Map<Character, TrieNode> subNodes = new HashMap<>();
  19. // 添加子节点
  20. public void addSubNode(Character c, TrieNode node) {
  21. subNodes.put(c,node);
  22. }
  23. // 获取子节点
  24. public TrieNode getSubNode(Character c) {
  25. return subNodes.get(c);
  26. }
  27. }
  28. }

1.2 根据敏感词,初始化前缀树

在 SensitiveFilter 类添加:

  • 定义日志
  • 定义常量:当我们检测到敏感词的时候,需要把敏感词替换成这个常量
  • 初始化根节点
  • 根据配置文件构造树型:在初次使用工具时自动初始化,添加注解 @PostConstruct ——当容器实例化 Bean 之后,在调用构造器之后,这个方法自动被调用
  • 初始化树:读取文件字符(在 class 中读取配置文件,此时是一个字节流,需要在 finally 中自动关闭,添加 try / catch,在 try 中开启,编译自动添加 finally 进行关闭,出现异常的时候添加日志;从字节流中读取字符不好,转化成字符流,再转化为缓冲流效率比较高)
  • 读取敏感词,每次读取敏感词放入一个变量中,成功读取到放入前缀树中
  • 接下来将一个敏感词添加到前缀树中:创建一个节点默认指向根节点,遍历字符,试图获取子节点,如果为空,初始化子节点,放在当前节点处;如果不为空直接使用
  • 处理下一节点时,使指针指向子节点,进行下一轮循环
  • 设置结束标识
  1. package com.example.demo.util;
  2. import org.apache.commons.lang3.CharUtils;
  3. import org.apache.commons.lang3.StringUtils;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.stereotype.Component;
  7. import javax.annotation.PostConstruct;
  8. import java.io.BufferedReader;
  9. import java.io.IOException;
  10. import java.io.InputStream;
  11. import java.io.InputStreamReader;
  12. import java.util.HashMap;
  13. import java.util.Map;
  14. @Component
  15. public class SensitiveFilter {
  16. //定义日志
  17. private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
  18. //定义常量:当我们检测到敏感词的时候,需要把敏感词替换成这个常量
  19. private static final String REPLACEMENT = "***";
  20. //初始化根节点
  21. private TrieNode rootNode = new TrieNode();
  22. //根据配置文件构造树型:在初次使用工具时自动初始化,添加注解 @PostConstruct
  23. // 当容器实例化 Bean 之后,在调用构造器之后,这个方法自动被调用
  24. @PostConstruct
  25. public void init() {
  26. try (
  27. InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");//字符流
  28. BufferedReader reader = new BufferedReader(new InputStreamReader(is));//字节流-》缓冲流
  29. ) {
  30. String keyword;
  31. while ((keyword = reader.readLine()) != null) {
  32. // 添加到前缀树
  33. this.addKeyword(keyword);
  34. }
  35. } catch (IOException e) {
  36. logger.error("加载敏感词文件失败: " + e.getMessage());
  37. }
  38. }
  39. // 将一个敏感词添加到前缀树中
  40. private void addKeyword(String keyword) {
  41. //创建一个节点默认指向根节点,后续会移动
  42. TrieNode tempNode = rootNode;
  43. for (int i = 0; i < keyword.length(); i++) {
  44. char c = keyword.charAt(i);
  45. //获取子节点
  46. TrieNode subNode = tempNode.getSubNode(c);
  47. //如果为空,初始化子节点,放在当前节点处
  48. if (subNode == null) {
  49. // 初始化子节点
  50. subNode = new TrieNode();
  51. tempNode.addSubNode(c, subNode);
  52. }
  53. // 完成之后,那个创建节点指向子节点,进入下一轮循环
  54. tempNode = subNode;
  55. // 设置结束标识
  56. if (i == keyword.length() - 1) {
  57. tempNode.setKeywordEnd(true);
  58. }
  59. }
  60. }
  61. }

1.3 编写过滤敏感词方法

在 SensitiveFilter 类添加:

  • 可以被外界调用,是一个 public 方法
  • 返回的是 String 过滤敏感词后的字符串
  • 首先判断传入字符串的是否为空,为空直接输出 null
  • 不为空,定义三个指针(上述图中的逻辑)
  • 第一个指针指向根节点;第二、三个指针指向字符串开始的地方
  • 定义变量记录最终结果,需要不断追加,使用 StringBuilder
  • 运行算法:指针三需要遍历到结尾
  • 得到某一个字符:不着急判断,首先必须要跳过符合(会存在❤️敏❤️感❤️词,这种不连接的,如果识别不了是不可行的)
  • 判断是否为符号:CharUtils.isAsciiAlphanumeric 判断是不是一个合法字符、0x2E80~0x9FFF 是东亚文字范围也要排除
  • 跳过字符之后,若指针1处于根节点,将此符号计入结果,让指针2向下走一步,但是无论符号在开头或中间,指针3都向下走一步
  • 进入下一轮循环
  • 检查下级节点:首先获取下级节点
  • 情况一:若没有下级节点,则以begin开头的字符串不是敏感词,进入下一个位置;指针1重新指向根节点
  • 情况二:若发现敏感词,将指针二和指针三之间字符串替换掉,并且使指针三进入下一个位置,此时设置指针二和指针三位置相同,指针一指向根节点
  • 情况三:检测途中没有检测完,也没有发现符号,需要继续往下执行,则指针三进入下一个位置
  • 没有敏感词时将最后一批字符计入结果
  • 最后返回字符串
  1. /**
  2. * 过滤敏感词
  3. *
  4. * @param text 待过滤的文本
  5. * @return 过滤后的文本
  6. */
  7. public String filter(String text) {
  8. if (StringUtils.isBlank(text)) {
  9. return null;
  10. }
  11. // 指针1
  12. TrieNode tempNode = rootNode;
  13. // 指针2
  14. int begin = 0;
  15. // 指针3
  16. int position = 0;
  17. // 定义变量记录最终结果,需要不断追加,使用 StringBuilder
  18. StringBuilder sb = new StringBuilder();
  19. //指针三需要遍历到结尾
  20. while (position < text.length()) {
  21. //得到某一个字符
  22. char c = text.charAt(position);
  23. // 跳过符号(会存在❤️敏❤️感❤️词,这种不连接的,如果识别不了是不可行的)
  24. if (isSymbol(c)) {
  25. // 若指针1处于根节点,将此符号计入结果,让指针2向下走一步
  26. if (tempNode == rootNode) {
  27. sb.append(c);
  28. begin++;
  29. }
  30. // 无论符号在开头或中间,指针3都向下走一步
  31. position++;
  32. continue;
  33. }
  34. // 检查下级节点
  35. //获取下级节点
  36. tempNode = tempNode.getSubNode(c);
  37. //若没有下级节点,则以begin开头的字符串不是敏感词,进入下一个位置;指针1重新指向根节点
  38. if (tempNode == null) {
  39. // 以begin开头的字符串不是敏感词
  40. sb.append(text.charAt(begin));
  41. // 进入下一个位置
  42. position = ++begin;
  43. // 重新指向根节点
  44. tempNode = rootNode;
  45. } else if (tempNode.isKeywordEnd()) {
  46. //若发现敏感词,将指针二和指针三之间字符串替换掉,并且使指针三进入下一个位置,
  47. //此时设置指针二和指针三位置相同,指针一指向根节点
  48. // 发现敏感词,将begin~position字符串替换掉
  49. sb.append(REPLACEMENT);
  50. // 进入下一个位置
  51. begin = ++position;
  52. // 重新指向根节点
  53. tempNode = rootNode;
  54. } else { //:检测途中没有检测完,也没有发现符号,需要继续往下执行,则指针三进入下一个位置
  55. // 检查下一个字符
  56. position++;
  57. }
  58. }
  59. //没有敏感词时 将最后一批字符计入结果
  60. sb.append(text.substring(begin));
  61. return sb.toString();
  62. }
  63. // 判断是否为符号
  64. private boolean isSymbol(Character c) {
  65. // 0x2E80~0x9FFF 是东亚文字范围
  66. //CharUtils.isAsciiAlphanumeric 判断是不是一个合法字符
  67. return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
  68. }

新建测试类:

  1. package com.example.demo;
  2. import com.example.demo.util.SensitiveFilter;
  3. import org.junit.Test;
  4. import org.junit.runner.RunWith;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.test.context.SpringBootTest;
  7. import org.springframework.test.context.ContextConfiguration;
  8. import org.springframework.test.context.junit4.SpringRunner;
  9. @RunWith(SpringRunner.class)
  10. @SpringBootTest
  11. @ContextConfiguration(classes = DemoApplication.class)
  12. public class SensitiveTests {
  13. @Autowired
  14. private SensitiveFilter sensitiveFilter;
  15. @Test
  16. public void testSensitiveFilter() {
  17. String text = "这里可以赌博,可以嫖娼,可以吸毒,可以开票";
  18. text = sensitiveFilter.filter(text);
  19. System.out.println(text);
  20. text = "这里可以☆赌☆博☆,可以☆嫖☆娼☆,可以☆吸☆毒☆,可以☆开☆票☆,哈哈哈!";
  21. text = sensitiveFilter.filter(text);
  22. System.out.println(text);
  23. }
  24. }

2.发布帖子

  • AJAX:Asynchronous JavaScript and XML
  • 采用 AJAX 请求,实现发布帖子的功能

添加 AJAX 的 pom.xml:

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>fastjson</artifactId>
  4. <version>1.2.58</version>
  5. </dependency>

在 util 包下的 Community 类下添加方法:

  • 添加处理字符串的 JSON 方法:给浏览器返回编号、提示信息、业务数据
  • 返回 JSON 格式的字符串
  • 封装参数到 JSON 中,并把值装入
  • 有可能只有编号,有可能没有提示信息或者业务数据,那么添加两个重载方法便于调用
  1. //添加处理字符串的 JSON 方法:给浏览器返回编号、提示信息、业务数据
  2. public static String getJSONString(int code, String msg, Map<String, Object> map) {
  3. JSONObject json = new JSONObject();
  4. json.put("code", code);
  5. json.put("msg", msg);
  6. if (map != null) {
  7. for (String key : map.keySet()) {
  8. json.put(key, map.get(key));
  9. }
  10. }
  11. return json.toJSONString();
  12. }
  13. //可能只有编号,有可能没有提示信息或者业务数据,那么添加两个重载方法便于调用
  14. public static String getJSONString(int code, String msg) {
  15. return getJSONString(code, msg, null);
  16. }
  17. public static String getJSONString(int code) {
  18. return getJSONString(code, null, null);
  19. }

2.1 数据访问层

添加一个帖子的方法返回增加的行数,在 dao 包下的 DiscussPostMapper 类下追加一个方法

  1. //添加一个帖子的方法
  2. int insertDiscussPost(DiscussPost discussPost);

在 resources 资源文件下 mapper 包下的 discusspost-mapper.xml 中实现方法:

  • 定义sql,将字段声明
  1. <sql id="insertFields">
  2. user_id, title, content, type, status, create_time, comment_count, score
  3. </sql>
  4. <insert id="insertDiscussPost" parameterType="DiscussPost">
  5. insert into discuss_post(<include refid="insertFields"></include>)
  6. values(#{userId},#{title},#{content},#{type},#{status},#{createTime},#{commentCount},#{score})
  7. </insert>

2.2 业务层

业务层需要提供一个保存帖子的业务方法(并且使用敏感词的方法),在 service 包下的 DiscussPostService 类中追加一个方法

  • 对添加的数据进行敏感词过滤和标签替换:需要对 title 和 context 进行处理
  1. //注入敏感词
  2. @Autowired
  3. private SensitiveFilter sensitiveFilter;
  4. //添加一个保存帖子的业务方法,返回添加的行数
  5. public int addDiscussPost(DiscussPost post) {
  6. if (post == null) {
  7. throw new IllegalArgumentException("参数不能为空");
  8. }
  9. //处理标签:转义 HTML
  10. post.setTitle(HtmlUtils.htmlEscape(post.getTitle()));
  11. post.setContent(HtmlUtils.htmlEscape(post.getContent()));
  12. //过滤敏感词
  13. post.setTitle(sensitiveFilter.filter(post.getTitle()));
  14. post.setContent(sensitiveFilter.filter(post.getContent()));
  15. return discussPostMapper.insertDiscussPost(post);//实现插入数据,调用方法
  16. }

2.3 视图层

实现增加帖子功能,在 controller 包下新建 DiscussPostController 类(实现增加帖子功能):

  • 添加注解 @Controller、@RequsetMapping 访问路径
  • 实现功能,先注入 DiscussPostService 添加帖子的方法
  • 处理增加帖子的请求,是一个异步请求:声明路径;增加数据,浏览器提交数据,提交的方式为 POST
  • 添加 @ResponseBody 表示返回字符串
  • 获取当前用户,注入 HostHolder
  • 添加方法,返回字符串,传入标题(title)和内容(content)
  • 发帖子前提是登陆:在 HostHolder 中取对象,如果取到的对象为空(未登录),给页面一个 403 提示(异步的,是 json 格式的数据)
  • 登陆成功之后需要调用 DiscussPostService 保存帖子:构造实体:创建 post 对象,传入 UserId、title、content、处理时间

  • 最后返回正确的状态(0)
  • 如果报错的情况,将来统一处理,默认是正确情况
  1. package com.example.demo.controller;
  2. import com.example.demo.entity.DiscussPost;
  3. import com.example.demo.entity.User;
  4. import com.example.demo.service.DiscussPostService;
  5. import com.example.demo.util.CommunityUtil;
  6. import com.example.demo.util.HostHolder;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Controller;
  9. import org.springframework.web.bind.annotation.RequestMapping;
  10. import org.springframework.web.bind.annotation.RequestMethod;
  11. import org.springframework.web.bind.annotation.ResponseBody;
  12. import java.util.Date;
  13. /**
  14. * 实现增加帖子功能
  15. */
  16. @Controller
  17. @RequestMapping("/discuss")
  18. public class DiscussPostController {
  19. //实现功能,先注入 DiscussPostService 添加帖子的方法
  20. @Autowired
  21. private DiscussPostService discussPostService;
  22. @Autowired
  23. private HostHolder hostHolder;
  24. @RequestMapping(path = "/add", method = RequestMethod.POST)
  25. @ResponseBody //添加 @ResponseBody 表示返回字符串
  26. //添加方法,返回字符串,传入标题(title)和内容(context)
  27. public String addDiscussPost(String title, String content) {
  28. //发帖子前提是登陆:在 HostHolder 中取对象,如果取到的对象为空(未登录),给页面一个 403 提示(异步的,是 json 格式的数据)
  29. User user = hostHolder.getUser();
  30. if (user == null) {
  31. return CommunityUtil.getJSONString(403, "你还没有登陆");
  32. }
  33. //登陆成功之后需要调用 DiscussPostService 保存帖子:构造实体:创建 post 对象,传入 UserId、title、content、处理时间
  34. DiscussPost post = new DiscussPost();
  35. post.setUserId(user.getId());
  36. post.setTitle(title);
  37. post.setContent(content);
  38. post.setCreateTime(new Date());
  39. discussPostService.addDiscussPost(post);
  40. // 报错的情况,将来统一处理.
  41. return CommunityUtil.getJSONString(0, "发布成功!");
  42. }
  43. }

处理页面:

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:

  1. $(function(){
  2. $("#publishBtn").click(publish);
  3. });
  4. function publish() {
  5. $("#publishModal").modal("hide");
  6. // 获取标题和内容
  7. var title = $("#recipient-name").val();
  8. var content = $("#message-text").val();
  9. // 发送异步请求(POST)
  10. $.post(
  11. CONTEXT_PATH + "/discuss/add",
  12. {"title":title,"content":content},
  13. function(data) {
  14. data = $.parseJSON(data);
  15. // 在提示框中显示返回消息
  16. $("#hintBody").text(data.msg);
  17. // 显示提示框
  18. $("#hintModal").modal("show");
  19. // 2秒后,自动隐藏提示框
  20. setTimeout(function(){
  21. $("#hintModal").modal("hide");
  22. // 刷新页面
  23. if(data.code == 0) {
  24. window.location.reload();
  25. }
  26. }, 2000);
  27. }
  28. );
  29. }

3.帖子详情

3.1 数据访问层

增加查看帖子的方法,在 dao 包下的 DiscussPostMapper 类下追加一个方法

  • 根据帖子 id,查询帖子详情,返回一个帖子
  1. //根据帖子 id,查询帖子详情,返回一个帖子
  2. DiscussPost selectDiscussPostById(int id);

在 resources 资源文件下 mapper 包下的 discusspost-mapper.xml 中实现方法:

  • 添加 select 标签
  • 根据 id 查询
  1. <select id="selectDiscussPostById" resultType="DiscussPost">
  2. select <include refid="selectFields"></include>
  3. from discuss_post
  4. where id = #{id}
  5. </select>

3.2 业务层

增加查询方法,在 service 包下的 DiscussPostService 类中追加一个方法

  • 根据 id 查询 帖子方法
  1. //根据 id 查询 帖子方法
  2. public DiscussPost findDiscussPostById(int id) {
  3. return discussPostMapper.selectDiscussPostById(id);
  4. }

3.3 视图层

查询请求,在 controller 包下的DiscussPostController 类中新增处理查询请求:

  • 声明查询路径,请求方式为 GET 请求
  • 使用 @PathVariable 取动态的值,需要将查询到的结果发送给模板
  • 查询帖子,传入模板中

  • 用户 id 需要处理,因为在页面要显示帖子的作者,不是显示 id,需要显示用户的头像,名字这样的信息
  • 第一种方法:在查询的时候使用关联查询 。优点:查询快;缺点:可能存在冗余、耦合
  • 第二种方法:先查出帖子数据,根据 id 调用 Userservice 查询 User,再通过 Model 将 User 发送给 模板,这样模板得到了帖子,也得到了模板。优点:查询两次,没有冗余;缺点:查询慢
  • 在这里使用第二种情况,查询慢可以使用 Redis 来优化
  • 查询 帖子的作者,注入 UserService
  • 把作者传给模板
  • 最后返回模板路径
  1. @Autowired
  2. private UserService userService;
  3. //查询请求方法
  4. @RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
  5. public String getDiscussPost(@PathVariable("discussPostId") int discussPostId, Model model) {
  6. // 帖子
  7. DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
  8. model.addAttribute("post", post);
  9. //第一种方法:在查询的时候使用关联查询 。优点:查询快;缺点:可能存在冗余、耦合
  10. //第二种方法:先查出帖子数据,根据 id 调用 Userservice 查询 User,再通过 Model 将 User 发送给 模板,
  11. // 这样模板得到了帖子,也得到了模板。优点:查询两次,没有冗余;缺点:查询慢
  12. //在这里使用第二种情况,查询慢可以使用 Redis 来优化
  13. // 作者
  14. User user = userService.findUserById(post.getUserId());
  15. // 把作者传给模板
  16. model.addAttribute("user", user);
  17. return "/site/discuss-detail";
  18. }

index.xml:在帖子标题上增加访问详情页面的链接

<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">学习 Java</a>

disucss-detail.html:处理静态资源的访问路径、复用 index.xml 的header 区域、显示标题、作者、发布时间、帖子正文等内容

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/572961
推荐阅读
相关标签
  

闽ICP备14008679号