5.过滤敏感词 + 发布帖子 + 帖子详情

2023-12-13 09:20:59

目录

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:

赌博
嫖娼
吸毒
开票

实现上述算法(在 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 区域、显示标题、作者、发布时间、帖子正文等内容

文章来源:https://blog.csdn.net/m0_72161237/article/details/134555771
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。