个人简介:
前言:
如果你想要一个可以系统学习的网站,那么我推荐的是牛客网,个人感觉用着还是不错的,页面很整洁,而且内容也很全面,语法练习,算法题练习,面试知识汇总等等都有,论坛也很活跃,传送门链接:
一:需求新增
1.需求分析
前面我们的想法是调用腾讯云的内容安全对文章的文本信息及图片进行安全检测,通过则将文章发布。但是这时候有个缺点,就是审核不能过滤一些敏感词,比如针孔摄像头、高额贷款、论文代写等,我们这时候就需要自己维护一套敏感词系统用来对文章进行检测。对于这套敏感词系统,我们可以在管理端进行删除增添的操作。
除了要对文本进行敏感词过滤之外,由于图片中可能还会存在敏感词,所以我们也需要对图片中的文字进行检测,使用到的技术是OCR。
2.技术选型
(1)文本
方案 说明 数据库模糊查询 效率太低 String.indexOf("")查找 数据库量大的话也是比较慢 全文检索 分词再匹配 DFA算法 确定有穷自动机(一种数据结构)
在文字过滤系统中,为了能够应付较高的并发,需要尽量减少计算。采用DFA算法基本没有什么计算,基本是状态转移。所以选用最后一种方案。
(2)图片
方案 说明 百度OCR 收费 Tesseract-OCR Google维护的开源OCR引擎,支持Java,Python等语言调用 Tess4J 封装了Tesseract-OCR ,支持Java调用
Tess4J封装了Tesseract-OCR,Java调用起来方便,因此选用此方案。
3.DFA
(1)原理分析
DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。
其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。
简单点说就是,它是是通过event和当前的state得到下一个state,即event+state=nextstate。理解为系统中有多个节点,通过传递进入的event,来确定走哪个路由至另一个节点,而节点是有限的。
存储:一次性的把所有的敏感词存储到了多个map中,就是下图表示这种结构
敏感词:冰毒、大麻、大坏蛋
其处理过程为先检索文章中每一个字符,然后查看该字符是否包含在敏感词库中,假如包含在敏感词库中则查看该字符在敏感词库中isEnd是否为0,假如不为0则继续检索下一个字符,然后继续查看该字符是否存在于敏感词库中,假如存在则查看isEnd是否为1,假如为1则说明是敏感词。举个例子,假如有这样一句话:“我不卖冰毒”,这时候就会先检索“我”字,发现不在敏感词库,然后继续检索下一个,直至到“冰”,这时候“冰”在敏感词库中,但是isEnd为0,继续检索,“毒”在敏感词库且isEnd为1,这时候检测到敏感词“冰毒”。
(2)代码实现
①创建敏感词表:
实体类:
package com.my.model.wemedia.pojos;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 敏感词信息表
* </p>
*
* @author itheima
*/
@Data
@TableName("wm_sensitive")
public class WmSensitive implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Integer id;
/**
* 敏感词
*/
@TableField("sensitives")
private String sensitives;
/**
* 创建时间
*/
@TableField("created_time")
private Date createdTime;
}
②创建对应Mapper接口
③导入工具类
package com.my.utils.common;
import java.util.*;
public class SensitiveWordUtil {
public static Map<String, Object> dictionaryMap = new HashMap<>();
/**
* 生成关键词字典库
* @param words
* @return
*/
public static void initMap(Collection<String> words) {
if (words == null) {
System.out.println("敏感词列表不能为空");
return ;
}
// map初始长度words.size(),整个字典库的入口字数(小于words.size(),因为不同的词可能会有相同的首字)
Map<String, Object> map = new HashMap<>(words.size());
// 遍历过程中当前层次的数据
Map<String, Object> curMap = null;
Iterator<String> iterator = words.iterator();
while (iterator.hasNext()) {
String word = iterator.next();
curMap = map;
int len = word.length();
for (int i =0; i < len; i++) {
// 遍历每个词的字
String key = String.valueOf(word.charAt(i));
// 当前字在当前层是否存在, 不存在则新建, 当前层数据指向下一个节点, 继续判断是否存在数据
Map<String, Object> wordMap = (Map<String, Object>) curMap.get(key);
if (wordMap == null) {
// 每个节点存在两个数据: 下一个节点和isEnd(是否结束标志)
wordMap = new HashMap<>(2);
wordMap.put("isEnd", "0");
curMap.put(key, wordMap);
}
curMap = wordMap;
// 如果当前字是词的最后一个字,则将isEnd标志置1
if (i == len -1) {
curMap.put("isEnd", "1");
}
}
}
dictionaryMap = map;
}
/**
* 搜索文本中某个文字是否匹配关键词
* @param text
* @param beginIndex
* @return
*/
private static int checkWord(String text, int beginIndex) {
if (dictionaryMap == null) {
throw new RuntimeException("字典不能为空");
}
boolean isEnd = false;
int wordLength = 0;
Map<String, Object> curMap = dictionaryMap;
int len = text.length();
// 从文本的第beginIndex开始匹配
for (int i = beginIndex; i < len; i++) {
String key = String.valueOf(text.charAt(i));
// 获取当前key的下一个节点
curMap = (Map<String, Object>) curMap.get(key);
if (curMap == null) {
break;
} else {
wordLength ++;
if ("1".equals(curMap.get("isEnd"))) {
isEnd = true;
}
}
}
if (!isEnd) {
wordLength = 0;
}
return wordLength;
}
/**
* 获取匹配的关键词和命中次数
* @param text
* @return
*/
public static Map<String, Integer> matchWords(String text) {
Map<String, Integer> wordMap = new HashMap<>();
int len = text.length();
for (int i = 0; i < len; i++) {
int wordLength = checkWord(text, i);
if (wordLength > 0) {
String word = text.substring(i, i + wordLength);
// 添加关键词匹配次数
if (wordMap.containsKey(word)) {
wordMap.put(word, wordMap.get(word) + 1);
} else {
wordMap.put(word, 1);
}
i += wordLength - 1;
}
}
return wordMap;
}
/* public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("法轮");
list.add("法轮功");
list.add("冰毒");
initMap(list);
String content="我是一个好人,并不会卖冰毒,也不操练法轮功,我真的不卖冰毒";
Map<String, Integer> map = matchWords(content);
System.out.println(map);
} */
}
4.OCR
(1)原理分析
OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程。
(2)代码实现
①在tbug-headlines-common中导入如下依赖
<dependency>
<groupId>net.sourceforge.tess4j</groupId>
<artifactId>tess4j</artifactId>
<version>4.1.1</version>
</dependency>
②封装成工具类
package com.my.common.tess4j;
import lombok.Getter;
import lombok.Setter;
import net.sourceforge.tess4j.ITesseract;
import net.sourceforge.tess4j.Tesseract;
import net.sourceforge.tess4j.TesseractException;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.awt.image.BufferedImage;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "tess4j")
public class Tess4jClient {
private String dataPath;
private String language;
public String doOCR(BufferedImage image) throws TesseractException {
//创建Tesseract对象
ITesseract tesseract = new Tesseract();
//设置字体库路径
tesseract.setDatapath(dataPath);
//中文识别
tesseract.setLanguage(language);
//执行ocr识别
String result = tesseract.doOCR(image);
//替换回车和tal键 使结果为一行
result = result.replaceAll("/r|/n", "-").replaceAll(" ", "");
return result;
}
}
③在spring.factories配置中添加该类,完整如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.my.common.exception.ExceptionCatch, com.my.common.swagger.SwaggerConfiguration, com.my.common.swagger.Swagger2Configuration, com.my.common.tencentcloud.TextDetection, com.my.common.tencentcloud.ImageDetection, com.my.common.tess4j.Tess4jClient,
④在tbug-headlines-wemedia中添加如下配置
tess4j: data-path: D:headlinesPro essdata language: chi_sim
注意文件路径要填自己的路径
二:完整审核流程
1.审核类
2.功能调用
在文章提交实现类中添加对自动审核方法的调用(见第五步)
/**
* 提交文章
* @param dto
* @return
*/
@Override
public ResponseResult submitNews(WmNewsDto dto) {
//1.参数校验
if(dto == null || dto.getContent().length() == 0) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
//2.保存或修改文章
//2.1属性拷贝
WmNews wmNews = new WmNews();
BeanUtils.copyProperties(dto,wmNews);
//2.2设置封面图片
if(dto.getImages() != null && dto.getImages().size() != 0) {
String images = StringUtils.join(dto.getImages(), ",");
wmNews.setImages(images);
}
//2.3封面类型为自动
if(dto.getType().equals(WemediaConstants.WM_NEWS_TYPE_AUTO)) {
wmNews.setType(null);
}
saveOrUpdateWmNews(wmNews);
//3.判断是否为草稿
if(dto.getStatus().equals(WmNews.Status.NORMAL.getCode())) {
//直接保存结束
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
//4.不是草稿
//4.1保存文章图片素材与文章关系
//4.1.1提取图片素材列表
List<String> imagesList = getImagesList(dto);
//4.1.2保存
saveRelatedImages(imagesList,wmNews.getId(),WemediaConstants.WM_CONTENT_REFERENCE);
//4.2保存封面图片和文章关系
saveRelatedCover(dto,imagesList,wmNews);
//5.审核文章(异步调用)
wmAutoScanService.AutoScanTextAndImage(wmNews.getId());
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/290827.html