过滤敏感词
前缀树
- 名称:Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 应用:字符串检索、词频统计、字符串排序等
在这里插入图片描述
敏感词过滤器的步骤
- 根节点不包含任何字符;
- 其余每个节点只有一个字符;
- 连接起来一条路就是字符串,每条路的字符串都不同;
(怎么感觉有点像KMP算法)
- 在resources文件夹下创建敏感词txt:
cpp
赌博
嫖娼
吸毒
开票
定义前缀树
- 在utils下创建工具类SensitiveFilter,创建内部类定义前缀树的结构:
cpp
@Component
public class SensitiveFilter {
private class TrieNode {
private boolean isKeywordEnd = false;//是否是敏感词的结尾
private Map<Character, TrieNode> subNodes = new HashMap<>();//key是下级字符,value是下级节点
public void addSubNode(Character c, TrieNode node) {
subNodes.put(c, node);
}
public TrieNode getSubNode(Character c) {
return subNodes.get(c);
}
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
}
}
根据敏感词,初始化前缀树
cpp
//注解的意思是在在Spring创建Bean的实例,设置完所有属性,解析并完成所有的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());
}
}
- 这里使用PostConstruct注解,方便在Spring创建依赖Bean的时候就创建好前缀树;
- InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"),这个的意思是从classpath下读取txt文件,为什么不用相对路径?
在Web应用中,你通常不能控制当前工作目录。Web服务器可能在任何位置启动你的应用,这使得使用相对路径来访问资源文件变得不可靠。使用上面的路径也就是访问构建后classes底下的文件(如果构建后没有出现,需要maven clean之后重新构建):
- addKeyword方法,将String添加到前缀树:
cpp
private void addKeyword(String keyword){
TrieNode tempNode = rootNode;//相当于指针从root开始
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);
}
}
}
编写过滤敏感词的方法
cpp
/*
过滤敏感词
参数:待过滤的文本
返回:过滤后的文本
*/
public String filter(String text){
if(text == null){
return null;
}
//指针1:指向树
TrieNode tempNode = rootNode;
//指针2:指向文本开始
int begin = 0;
//指针3:指向文本末尾
int position = 0;
//结果
StringBuilder sb = new StringBuilder();
//利用指针3遍历文本(整个文本都要遍历)
while(position < text.length()){
char c = text.charAt(position);
//跳过符号(有的敏感词中间有符号以规避)
if(isSymbol(c)){
//若指针1处于根节点,将此符号计入结果(直接跳过特殊符号),让指针2向下走一步
if(tempNode == rootNode){
sb.append(c);
begin++;
}
//无论符号在开头还是中间,指针3都向下走一步
position++;//指针3是整体遍历的,不管都要走
continue;//进入下一轮循环
}
//检查下级节点
tempNode = tempNode.getSubNode(c);
if(tempNode == null) {
//以begin开头的字符串不是敏感词
sb.append(text.charAt(begin));
//进入下一个位置
position = ++begin;//先加后赋值
//重新指向根节点
tempNode = rootNode;//指针3重新到跟节点
}else if (tempNode.isKeywordEnd()) {
//发现敏感词,将begin-position字符串替换掉
sb.append(REPLACEMENT);
//进入下一个位置(end的下一个位置),两者重合
begin = ++position;
//重新指向根节点
tempNode = rootNode;
}else {
//检查下一个字符
position++;
}
}
//将最后一批字符计入结果
sb.append(text.substring(begin));
return sb.toString();
}
- 防止有人用特殊符号隔开敏感词。判断是否是符号。
cpp
private boolean isSymbol(char c) {
// 0x2E80-0x9FFF 东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
测试
cpp
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class FilterTests {
@Autowired
private SensitiveFilter sensitiveFilter;
@Test
public void testSensitiveFilter() {
String text = "这里可以*赌*博*,可以|嫖|娼|,可以|吸|毒|,可以*开*票*,哈哈哈";
String text1 = sensitiveFilter.filter(text);
System.out.println(text1);
}
}
输出:
cpp
这里可以*****,可以|***|,可以|***|,可以*****,哈哈哈
发布帖子
AJAX
- Asynchronous JavaScript and XML
- 异步的JavaScript与XML,不是一门新技术,只是一个新的术语。
- 使用AJAX,网页能够将增量更新呈现在页面上,而不需要刷新整个页面(异步的意思)。
- 虽然X代表XML,但目前JSON的使用比XML更加普遍。
- https://developer.mozilla.org/zh-CN/docs/Web/Guide/AJAX
例子:使用jQuery发送AJAX请求
- 编写生成json字符串的工具类(CommunityUtils中重载三个方法):
cpp
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(Map.Entry<String, Object> entry : map.entrySet()) {
// json.put(entry.getKey(), entry.getValue());
// }
json.putAll(map);
}
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);
}
- 编写controller接受异步请求:(AlphaController中)
cpp
@RequestMapping(path = "/ajax", method = RequestMethod.POST)
@ResponseBody
public String testAjax(String name, int age) {
System.out.println(name);
System.out.println(age);
return CommunityUtil.getJsonString(0, "操作成功");
}
- 因为返回的是json字符串而不是页面 ,所以使用@ResponseBody注解;
- 测试,编写一个静态html,在其中点击按钮就提交相应的json数据;
cpp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ajax</title>
</head>
<body>
<p>
<input type="button" value="发送" onclick="send();">
</p>
<script
src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
crossorigin="anonymous">
</script>
<script>
function send() {
$.ajax({
url: "/community/alpha/ajax",
type: "post",
data: {
name: "zhangsan",
age: 18
},
success: function (data) {
console.log(typeof (data))
console.log(data);
// 将json字符串转换为json对象
data = $.parseJSON(data);
console.log(typeof (data));
console.log(data.code)
}
});
}
</script>
</body>
</html>
开发发布帖子功能
- DAO层添加insert接口:
cpp
@Mapper
public interface DiscussPostMapper {
//userId为0时,表示查询所有用户的帖子,如果不为0,表示查询指定用户的帖子
//offset表示起始行号,limit表示每页最多显示的行数
List<DiscussPost> selectDiscussPosts(int userId, int offset, int limit);
//查询帖子的行数
//userId为0时,表示查询所有用户的帖子
int selectDiscussPostRows(@Param("userId") int userId);
//@param注解用于给参数取别名,拼到sql语句中,如果只有一个参数,并且在<if>标签里,则必须加别名
int insertDiscussPost(DiscussPost discussPost);
}
修改Mapper-xml:
cpp
<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>
- Service层对内容进行敏感词过滤等:DiscussPostService
cpp
public int addDiscussPost(DiscussPost discussPost) {
if(discussPost == null) {
throw new IllegalArgumentException("参数不能为空");
}
//转义HTML标记
discussPost.setTitle(HtmlUtils.htmlEscape(discussPost.getTitle()));
discussPost.setContent(HtmlUtils.htmlEscape(discussPost.getContent()));
//过滤敏感词
discussPost.setTitle(sensitiveFilter.filter(discussPost.getTitle()));
discussPost.setContent(sensitiveFilter.filter(discussPost.getContent()));
return discussPostMapper.insertDiscussPost(discussPost);
}
- Controller层addpost:
cpp
@Controller
@RequestMapping("/discuss")
public class DiscussPostController {
@Autowired
private DiscussPostService discussPostService;
@Autowired
private HostHolder hostHolder;
@RequestMapping(path = "/add", method = RequestMethod.POST)
@ResponseBody
public String addDiscussPost(String title, String content) {
User user = hostHolder.getUser();
if(user == null) {
return CommunityUtil.getJsonString(403, "你还没有登录!");
}
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发现报错:
cpp
com.mysql.cj.exceptions.UnableToConnectException: Public Key Retrieval is not allowed at java.bas
解决方法,修改properties文件:
cpp
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/community?characterEncoding=utf-8&useSSL=false&serverTimezone=Hongkong&allowPublicKeyRetrieval=true
添加allowPublicKeyRetrieval=true
- 修改index.html
这里修改成不登录发布按钮不显示
- 修改index.js
cpp
$(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);//将字符串转换为json对象
//在提示框中显示返回的消息
$("#hintBody").text(data.msg);
//显示提示框
$("#hintModal").modal("show");
//2秒后自动隐藏提示框
setTimeout(function(){
$("#hintModal").modal("hide");
//刷新页面
if(data.code == 0){//发布成功
window.location.reload();//刷新页面
}
}, 2000);
}
);
$("#hintModal").modal("show");
setTimeout(function(){
$("#hintModal").modal("hide");
}, 2000);
}
这个就是上面那个id。
- 测试
bug解决
- 这里终于解决了bug,疑似是js文件版本不对的问题,导致点击下拉框和发布框的时候都弹不出来啊,改成了原来的index.html的js,就恢复正常了:
cpp
<script src="https://code.jquery.com/jquery-3.3.1.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" crossorigin="anonymous"></script>
开发帖子详情
Dao层:DiscussPostMapper
- 添加一个方法:
cpp
DiscussPost selectDiscussPostById(int id);
- 修改Mapper.xml
cpp
<select id="selectDiscussPostById" resultType="DiscussPost">
select
<include refid="selectFields"></include>
from discuss_post
where id = #{id}
</select>
Service层:DiscussPostService
cpp
public DiscussPost findDiscussPostById(int id) {
return discussPostMapper.selectDiscussPostById(id);
}
Controller层:DiscussPostController
cpp
@RequestMapping(path = "/detail/{discussPostId}", method = RequestMethod.GET)
public String getDiscussPost(@PathVariable(name="discussPostId") int discussPostId, Model model) {
DiscussPost post = discussPostService.findDiscussPostById(discussPostId);
model.addAttribute("post", post);
//帖子的作者
User user = userService.findUserById(post.getUserId());
model.addAttribute("user", user);
return "/site/discuss-detail";//返回模版路径
}
- 需要接收帖子id,一般都使用url中带着,所以用@PathVariable从注解中取;
- 需要通过帖子id查帖子,再用帖子差用户id,最后查到用户名;
修改index.html
cpp
<!-- 帖子列表 -->
<ul class="list-unstyled">
<li class="media pb-3 pt-3 mb-3 border-bottom" th:each="map:${discussPosts}">
<a href="site/profile.html">
<img th:src="${map.user.headerUrl}" class="mr-4 rounded-circle" alt="用户头像" style="width:50px;height:50px;">
</a>
<div class="media-body">
<h6 class="mt-0 mb-3">
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</a>
<span class="badge badge-secondary bg-primary" th:if="${map.post.type==1}">置顶</span>
<span class="badge badge-secondary bg-danger" th:if="${map.post.status==1}">精华</span>
</h6>
<div class="text-muted font-size-12">
<u class="mr-3" th:utext="${map.user.username}">寒江雪</u> 发布于 <b th:text="${#dates.format(map.post.createTime,'yyyy-MM-dd HH:mm:ss')}">2019-04-15 15:32:18</b>
<ul class="d-inline float-right">
<li class="d-inline ml-2">赞 11</li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2">回帖 7</li>
</ul>
</div>
</div>
</li>
</ul>
- 这里把原来静态的都改成动态,注意变量要用${}括起来。
- 这里我突然想起来一个问题,在实体类中,这些属性都是private为什么能直接用.运算符获取?
在你的代码中,post.title就是一个表达式,它表示post对象的title属性。虽然title属性在post类中被声明为private,但是Thymeleaf可以通过post类的getTitle方法来获取title属性的值。这是Java的标准Bean规范,即对于一个名为foo的属性,应该有一个名为getFoo的方法来获取它的值,有一个名为setFoo的方法来设置它的值。
注意这就是java的bean,不是受Spring托管的@Bean注解:
在Java中,一个类并不需要使用@Bean注解或其他任何注解就能成为一个Java Bean。Java Bean是遵循特定命名规则的Java类,主要包含私有属性和对应的公有getter和setter方法。 在你的DiscussPost类中,所有的属性都是私有的,并且每个属性都有对应的公有getter和setter方法,所以它就是一个Java Bean。 @Bean注解通常用在Spring框架中,用于声明一个方法返回的对象应该被Spring管理。但是,并不是所有的Java Bean都需要被Spring管理,所以并不是所有的Java Bean都需要使用@Bean注解。
修改discuss-detail.html
cpp
<div class="main">
<!-- 帖子详情 -->
<div class="container">
<!-- 标题 -->
<h6 class="mb-4">
<img src="http://static.nowcoder.com/images/img/icons/ico-discuss.png"/>
<span th:utext="${post.title}">备战春招,面试刷题跟他复习,一个月全搞定!</span>
<div class="float-right">
<button type="button" class="btn btn-danger btn-sm">置顶</button>
<button type="button" class="btn btn-danger btn-sm">加精</button>
<button type="button" class="btn btn-danger btn-sm">删除</button>
</div>
</h6>
<!-- 作者 -->
<div class="media pb-3 border-bottom">
<a href="profile.html">
<img th:src="${user.headerUrl}" class="align-self-start mr-4 rounded-circle user-header" alt="用户头像" >
</a>
<div class="media-body">
<div class="mt-0 text-warning" th:utext="${user.username}">寒江雪</div>
<div class="text-muted mt-3">
发布于 <b th:text="${#dates.format(post.createTime,'yyyy-mm-dd-hh-mm-ss')}">2019-04-15 15:32:18</b>
<ul class="d-inline float-right">
<li class="d-inline ml-2"><a href="#" class="text-primary">赞 11</a></li>
<li class="d-inline ml-2">|</li>
<li class="d-inline ml-2"><a href="#replyform" class="text-primary">回帖 7</a></li>
</ul>
</div>
</div>
</div>
<!-- 正文 -->
<div class="mt-4 mb-3 content" th:utext="${post.content}">
金三银四的金三已经到了,你还沉浸在过年的喜悦中吗?
如果是,那我要让你清醒一下了:目前大部分公司已经开启了内推,正式网申也将在3月份陆续开始,金三银四,春招的求职黄金时期已经来啦!!!
再不准备,作为19应届生的你可能就找不到工作了。。。作为20届实习生的你可能就找不到实习了。。。
现阶段时间紧,任务重,能做到短时间内快速提升的也就只有算法了,
那么算法要怎么复习?重点在哪里?常见笔试面试算法题型和解题思路以及最优代码是怎样的?
跟左程云老师学算法,不仅能解决以上所有问题,还能在短时间内得到最大程度的提升!!!
</div>
</div>
最终效果: