【任务7】注册与登录:进阶:动态维护敏感词库

上一个任务,大部分人应该是把 敏感词 的维护写死在代码里了,但其实,在正常的线上系统中,肯定是不能写死的,必须得可以动态删除+增加,不然写死的话每次都要更改代码,重新发布系统太麻烦了。

所以呢,我们可以用一个简单的数据库表来动态维护我们的敏感词库,这样你要新增或者删除时,只需要直接操作下数据库就可以了(当然,真实的线上也是不能直接链接数据库写sql的,也是得弄一个可视化界面来弄,不过我们这里就不考虑了)。

所以呢,我们这里维护一个 disallow_word 表,字段有 id, value, type,type 是一个隐藏字段,表示敏感词的类型,但是我们暂时用不到,可以定义为 Int(默认值为0),以后或许会用到,这就是设计表需要关注的点,因为我们得为未来考虑。

之后我们就可以直接从数据库拉敏感词列表来过滤了。

要求:

1、请把你的 敏感词 过滤的算法代码写在下面
2、建表语句也写下面。

本问题对应的项目链接:从0设计一个web项目

PS:每一个打卡,都要给出你的GitHub链接哈,并且代码提交到GitHub

共以下 9 个回答

  • 星河揽我入梦 永久会员 2024年6月14日 下午4:33

    github仓库:https://github.com/CurryisCoat/community/tree/dev

    @Component
    public class SensitiveFilter {
    
        private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
    
        private static final String REPLACEMENT = "***";
    
        // 根节点
        private TrieNode rootNode = new TrieNode();
    
        @Autowired
        private DisallowService disallowService;
    
        @PostConstruct
        public void init()
        {
    //        InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-filter.txt");
    //        BufferedReader read = new BufferedReader(new InputStreamReader(is));
    
    //        try {
    //            String keyWord;
    //            while((keyWord=read.readLine())!=null)
    //            {
    //                this.addKeyword(keyWord);
    //            }
    //        } catch (IOException e) {
    //            logger.error("加载敏感词文件失败: " + e.getMessage());
    //        }
    
            List<DisallowWord> list = disallowService.wordList();
            for(DisallowWord t:list){
                String temp = t.getValue();
                this.addKeyword(temp);
            }
        }
    
        //将一个敏感词添加到前缀树中
        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);
                }
            }
        }
        //过滤文本
        public String filter(String text)
        {
            if(StringUtils.isBlank(text)){
                return null;
            }
    
            TrieNode p1 = rootNode;
            int p2 = 0;
            int p3 = 0;
            StringBuilder sb = new StringBuilder();
            while(p3 < text.length())
            {
                char c = text.charAt(p3);
                if(isSymbol(c)){
    
                    if(p1 == rootNode){
                        sb.append(c);
                        p2++;
                    }
                    p3++;
                    continue;
                }
    
                p1 = p1.getSubNode(c);
                if(p1==null){
                    sb.append(text.charAt(p2));
                    p3 = ++p2;
                    p1 = rootNode;
                }else if(p1.isKeywordEnd()==true){
                    sb.append(REPLACEMENT);
                    p2 = ++p3;
                    p1 = rootNode;
                }else{
                    p3++;
                }
            }
            sb.append(text.substring(p2));
            return sb.toString();
        }
    
    
        // 判断是否为符号
        private boolean isSymbol(Character c) {
            // 0x2E80~0x9FFF 是东亚文字范围
            return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
        }
        //树结构
        private class TrieNode{
    
            private boolean iskeywordEnd = false;
    
            private HashMap<Character,TrieNode> subNodes = new HashMap<>();
    
            public boolean isKeywordEnd() {
                return iskeywordEnd;
            }
    
            public void setKeywordEnd(boolean keywordEnd) {
                iskeywordEnd = keywordEnd;
            }
    
            public void addSubNode(Character c,TrieNode node){
                subNodes.put(c,node);
            }
    
            public TrieNode getSubNode(Character c){
                return subNodes.get(c);
            }
        }
    }
    
    

    2.建表语句
    CREATE TABLE disallow_word (
    id int NOT NULL AUTO_INCREMENT,
    value varchar(255) NOT NULL,
    type int NOT NULL DEFAULT ‘0’,
    PRIMARY KEY (id)
    ) ENGINE=InnoDB

    0 赞同 0 条回复
  • 忆霁昕 永久会员 2022年10月25日 下午3:06

    github仓库:https://github.com/yijixin66/FromZerotoExpert
    服务器:http://8.142.128.17/FromZerotoExpert/register.html
    1、ac自动机

    @Component
    public class AcTrie {
        class AcNode {
            public char data;
            //key-子节点的data value-子节点
            public HashMap<Character, AcNode> children = new HashMap();
            public boolean end = false;
            public AcNode fail;
    
            public AcNode() {
    
            }
            public AcNode(char data) {
                this.data = data;
            }
        }
    
        private AcNode root = new AcNode();
        //根据字符串数组构建ac自动机
        public void build(List<String> list) {
            for (String s : list) {
                insert(s);
            }
            buildFail();
        }
        //插入一个字符串
        public void insert(String text) {
            AcNode p = root;
            for (int i = 0; i < text.length(); i++) {
                char key = text.charAt(i);
                if (!p.children.containsKey(key)) {
                    AcNode node = new AcNode(key);
                    p.children.put(key, node);
                }
                p = p.children.get(key);
            }
            p.end = true;
        }
    
    
        public void buildFail() {
            //层序遍历
            Queue<AcNode> queue = new LinkedList<>();
            root.fail = null;
            queue.add(root);
            while (!queue.isEmpty()) {
                AcNode p = queue.remove();
                for (AcNode pc : p.children.values()) {
                    if (p == root) {
                        pc.fail = root;
                    } else {
                        AcNode q = p.fail;
                        while (q != null) {
                            AcNode qc = q.children.get(pc.data);
                            if (qc != null) {
                                pc.fail = qc;
                                break;
                            }
                            q = q.fail;
                        }
                        if (q == null) {
                            pc.fail = root;
                        }
                    }
                    queue.add(pc);
                }
            }
        }
        //查找该字符的子串是否存在
        public boolean find(String text) {
            AcNode p = root;
            int i = 0;
            int j = 0;
            while (j < text.length()) {
                char key = text.charAt(j);
                if (p.children.containsKey(key)) {
                    j++;
                    p = p.children.get(key);
                    if (p.end) {
                        return true;
                    }
                } else {
                    //没有匹配上 回退
                    i++;
                    j = i;
                    p = root;
                }
    
            }
            return false;
        }
        public boolean match (String text) {
            AcNode p = root;
            for (int i = 0; i < text.length(); i++) {
                char key = text.charAt(i);
                while (!p.children.containsKey(key) && p != root) {
                    p = p.fail;
                }
                p = p.children.get(key);
                if (p == null) {
                    p = root;
                }
                AcNode tmp = p;
                while (tmp != null) {
                    if (tmp.end) {
                        return true;
                    }
                    tmp = tmp.fail;
                }
            }
            return false;
        }
    
    }
    

    2、sql语句

    drop table if exists fzte_disallow_word;
    create table fzte_disallow_word(
        id int(10) primary key auto_increment,
        value VARCHAR(255) not null,
        type int(10) default 0
    );
    
    0 赞同 0 条回复
  • 豆子彩虹 普通 2022年10月24日 上午11:15
    1. @Component
      public class MyUtils {
          @Autowired
          DisallowWordService disallowWordService;
          private static Trie trie = new Trie();
          @PostConstruct
          public void init() {
              List<DisallowWord> disallowWords = disallowWordService.getAll();
              System.out.println("get");
              for (DisallowWord disallowWord : disallowWords) {
                  trie.put(disallowWord.getValue());
              }
          }
          /**
           * 判断用户名是否合法
           * @param s 用户名
           * @return true表示不合法,false表示合法
           */
          public static boolean isIllegal(String s) {
              return trie.ifContain(s);
          }
      }
      
    CREATE TABLE disallow_word(
    id INT PRIMARY KEY AUTO_INCREMENT,
    `value` VARCHAR(8) NOT NULL,
    type INT NOT NULL DEFAULT 0
    );
    

    GitHub地址:https://github.com/lei342345795/FromZerotoExpert

    0 赞同 0 条回复
  • So 普通 2022年10月21日 下午4:30

    1、

    package com.yang.util;
    
    import java.util.*;
    
    public class SensitiveWordUtil {
        public static Map<String, Object> dictionaryMap = new HashMap<>();
    
        public static void initMap(Collection<String> words) {
            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) {
                        wordMap = new HashMap<>(2);
                        wordMap.put("isEnd", "0");
                        curMap.put(key, wordMap);
                    }
                    curMap = wordMap;
                    if (i == len - 1) {
                        curMap.put("isEnd", "1");
                    }
                }
            }
            dictionaryMap = map;
        }
    
        private static int checkWord(String text, int beginIndex) {
            boolean isEnd = false;
            int wordLength = 0;
            Map<String, Object> curMap = dictionaryMap;
            int len = text.length();
            for (int i = beginIndex; i < len; i++) {
                String key = String.valueOf(text.charAt(i));
                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;
        }
    
        public static boolean mathWords(String text) {
            int len = text.length();
            for (int i = 0; i < len; i++) {
                int wordLength = checkWord(text, i);
                if (wordLength > 0) {
                    return true;
                }
            }
            return false;
        }
    }
    

    2、
    create table disallow_word(
    id int primary key auto_increment,
    value varchar(255) not null,
    type int default 0
    );
    github地址:https://github.com/Egofir/ferry

    0 赞同 0 条回复
  • lucca 普通 2022年10月8日 下午11:42

    1、请把你的 敏感词 过滤的算法代码写在下面
    Trie树:

    /**
     * Trie树 实现:
     * 1.关键字查找
     * 2.词频统计
     *
     * @author suen
     * @date 2022/10/8 13:39
     */
    public class TrieTree {
    
        private final TrieNode root = new TrieNode();
    
        /**
         * 插入关键词
         *
         * @param word 关键词
         */
        public void insert(String word) {
            // 如果是字符串不做处理
            if (StrUtil.isBlank(word)) return;
    
            char[] cs = word.toCharArray();
            TrieNode t = this.root;
            for (char c : cs) {
                if (!t.child.containsKey(c)) {
                    t.child.put(c, new TrieNode());
                }
                t = t.child.get(c);
                t.cnt++;
            }
            t.end++;
        }
    
        /**
         * 删除字符串,回收空闲节点
         *
         * @param word 字符串
         */
        public void delete(String word) {
            if (search(word) == 0) return;
    
            TrieNode t = root;
            char[] cs = word.toCharArray();
            for (char c : cs) {
                if (--t.child.get(c).cnt == 0) {
                    t.child.remove(c);
                    return;
                }
                t = t.child.get(c);
            }
            t.end--;
        }
    
        /**
         * 返回单词频率
         *
         * @param word 单词
         * @return 频率
         */
        public int search(String word) {
            if (StrUtil.isBlank(word)) return 0;
    
            TrieNode t = root;
            char[] cs = word.toCharArray();
            for (char c : cs) {
                if (!t.child.containsKey(c)) return 0;
                t = t.child.get(c);
            }
            return t.end;
        }
    
        public boolean exist(String word) {
            if (StrUtil.isBlank(word)) return false;
    
            TrieNode t;
            char[] cs = word.toCharArray();
            for (int i = 0; i < cs.length; i++) {
                t = root;
                int p = i;
                while (p < cs.length && t.child.containsKey(cs[p])) {
                    t = t.child.get(cs[p++]);
                    if (t.end > 0) return true;
                }
            }
            return false;
        }
    
        // 节点
        private static final class TrieNode {
            int cnt;    // 前缀频率
            int end;    // 该字符串出现次数
            HashMap<Character, TrieNode> child;
    
            public TrieNode() {
                cnt = 0;
                end = 0;
                child = new HashMap<>();
            }
        }
    }
    

    校验器

    /**
     * 敏感词验证器
     *
     * @author suen
     * @date 2022/10/8 19:15
     */
    public class SensitiveWordValidator {
    
        private static final TrieTree tree = new TrieTree();
    
        static {
            SensitiveWordDao sensitiveWordDao = SpringContextHolder.getBean(SensitiveWordDao.class);
            // 总数量
            long count = sensitiveWordDao.count();
            // 查询次数
            long cnt = count / 100;
            for (long i = 0; i <= cnt; i++) {
                List<String> words = sensitiveWordDao.findByPage(i * 100, 100);
                for (String word : words) {
                    tree.insert(word);
                }
            }
        }
    
        /**
         * 验证是否包含敏感词
         *
         * @param str 语句
         * @return 是否包含敏感词
         */
        public static boolean isValid(String str) {
            return !tree.exist(str);
        }
    }
    

    2、建表语句也写下面。

    create table `sensitive_words` (
      `id` int(11) not null auto_increment comment '主键',
      `word` varchar(255) not null comment '敏感词',
      `type` int(4) not null default 0 comment '类型',
      primary key (`id`)
    ) engine = innodb comment = '敏感词表';
    

    GitHub链接: https://github.com/lucca-suen/FromZerotoExpert/tree/v2.2

    0 赞同 0 条回复
  • 月诉长安 永久会员 2022年9月24日 下午5:36

    1.敏感词的util类代码:

    package com.shk.utils;
    
    import com.shk.mapper.SensitiveMapper;
    import com.shk.pojo.DisallowWord;
    import org.apache.ibatis.session.SqlSession;
    
    import java.util.*;
    
    /**
     * @author: sunhengkang
     * @date:2022/9/22
     */
    public class SensitivewordFilter {
        //敏感词树
        private static HashMap sensitiveWordMap = null;
        //匹配规则
        private static final int MinMatchTYpe = 1;      //最小匹配规则,如:敏感词库["中国","中国人"],语句:"我是中国人",匹配结果:我是[中国]人
        private static final int MaxMatchType = 2;      //最大匹配规则,如:敏感词库["中国","中国人"],语句:"我是中国人",匹配结果:我是[中国人]
        //敏感词组的集合
        private static Set wordSet = null;
    
    
        /**
         * 初始化敏感词库,构建DFA算法模型
         *
         * @param sensitiveWordSet 敏感词库
         */
        public static synchronized void init(Set sensitiveWordSet) {
            addSensitiveWordToHashMap(sensitiveWordSet);
        }
            /**
             * 初始化数据库中的敏感词,将数据库中的敏感次构建成
             * 数,树状结构便于之后查找,注意采用了hashmap来存储
             * @param keyWordSet 敏感词字符串合集
             */
        @SuppressWarnings({ "rawtypes", "unchecked" })
        private static void addSensitiveWordToHashMap(Set keyWordSet) {
            sensitiveWordMap = new HashMap(keyWordSet.size());//初始化敏感词容器
            String key = null;
            Map nowMap = null;
            Map newWorMap = null;
            //迭代keyWordSet
            Iterator iterator = keyWordSet.iterator();
            while (iterator.hasNext()) {
                key = iterator.next();    //关键字
                nowMap = sensitiveWordMap;
                for (int i = 0; i < key.length(); i++) {
                    char keyChar = key.charAt(i);       //转换成char型
                    Object wordMap = nowMap.get(keyChar);       //获取
    
                    if (wordMap != null) {        //如果存在该key,直接赋值
                        nowMap = (Map) wordMap;
                    } else {     //不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个
                        newWorMap = new HashMap();
                        newWorMap.put("isEnd", "0");
                        nowMap.put(keyChar, newWorMap);
                        nowMap = newWorMap;
                    }
    
                    if (i == key.length() - 1) {
                        nowMap.put("isEnd", "1");    //最后一个                }
                    }
                }
            }
        }
    
        @SuppressWarnings({ "rawtypes"})
        private static int checkSensitiveWord(String txt,int beginIndex,int matchType){
            boolean  flag = false;    //敏感词结束标识位:用于敏感词只有1位的情况
            int matchFlag = 0;     //匹配标识数默认为0
            char word = 0;
            Map nowMap = sensitiveWordMap;
            for(int i = beginIndex; i < txt.length() ; i++){
                word = txt.charAt(i);
                nowMap = (Map) nowMap.get(word);     //获取指定key
                if(nowMap != null){     //存在,则判断是否为最后一个
                    matchFlag++;     //找到相应key,匹配标识+1
                    if("1".equals(nowMap.get("isEnd"))){       //如果为最后一个匹配规则,结束循环,返回匹配标识数
                        flag = true;       //结束标志位为true
                        if(SensitivewordFilter.MinMatchTYpe == matchType){    //最小规则,直接返回,最大规则还需继续查找
                            break;
                        }
                    }
                }
                else{     //不存在,直接返回
                    break;
                }
            }
            if(matchFlag < 2 && !flag){
                matchFlag = 0;
            }
            return matchFlag;
        }
    
    
    
    
        /**
         * 判断文字是否包含敏感字符
         *
         * @param txt       文字
         * @return 若包含返回true,否则返回false
         */
        public static boolean contains(String txt) {
            boolean flag = false;
            for (int i = 0; i < txt.length(); i++) {
                int matchFlag = checkSensitiveWord(txt, i, MinMatchTYpe); //判断是否包含敏感字符
                if (matchFlag > 0) {    //大于0存在,返回true
                    flag = true;
                }
            }
            return flag;
        }
    
        /**
         * 获取敏感词Set集合
         * @return
         */
      public static   Set getSensitiveWordSet(){
          wordSet = new HashSet<>();
          SqlSession sqlSession = SqlSessionUtil.getSqlSession();
          SensitiveMapper mapper = sqlSession.getMapper(SensitiveMapper.class);
          List disallowWords = mapper.getdisallowWord();
          for(int i = 0; i < disallowWords.size(); i++){
              wordSet.add(disallowWords.get(i).getSensitiveword());
          }
    
          return wordSet;
      }
    
    }
    
    
    

    其中使用Util实现昵名审查的用法为:

    //用户名检测
            Set wordSet = SensitivewordFilter.getSensitiveWordSet();
            SensitivewordFilter.init(wordSet);
            boolean contains = SensitivewordFilter.contains(nickname);
            if (contains){
                writer.println("昵名不合规");
            }
    

    2.敏感词的建表语句

    CREATE TABLE disallow_word (
    id int AUTO_INCREMENT PRIMARY KEY COMMENT ‘敏感词id’,
    sensitiveword varchar(15) not null UNIQUE COMMENT ‘敏感词’,
    type TINYINT NOT NULL DEFAULT(0) COMMENT ‘敏感词类型’
    )ENGINE=INNODB default charset=utf8mb4 ;

    3.github地址

    github:https://github.com/Haiyang-coder/FromZerotoExpert/tree/dev

    0 赞同 0 条回复
  • xiaoxi 永久会员 2022年9月23日 下午5:29

    1.敏感词过滤代码。
    我是在一个工具类VaildUtil中声明了一个前缀树的根节点的静态变量

    private staic Trie trie = new Trie();
    

    然后在类中的isVaildName()方法中调用trie.startIndex来检查昵称是否包括敏感词

    /** 判断用户昵称是否符合要求 */
        public static boolean isVaildName(String name) {
            // 昵称为空,抛出异常
            if (name == null) {
                throw new UserException(UserExceptionEnum.NAME_IS_NULL);
            }
            // 昵称长度不符合,返回false
            if (name.length() < 2 || name.length() > 16) {
                return false;
            }
            return true && !trie.searchWord(name);
        }
    

    另外,为了后期能更好地维护已建立的前缀树,可以新建一个Controller层的类对disallow_word表进行CRUD操作(我没有实现),我还添加了一个updateTrie()方法决定是否根据新表中的敏感词重构新的字典树,这个方法有点不行,万一你已经构建了几百万单词的前缀树,因为添加了一个,难道是再构建几百万+1的前缀树,所以我们可以新增一个addTrie方法,只向前缀树中新增单词而不重构,也可以新增一个deleteTrie方法,只向前缀树中删除单词。DisallowWordController层代码如下:

    @RestController
    @RequestMapping("/disallowword")
    public class DisallowWordController {
        @Autowired
        private DisallowWordService disallowWordService;
    
        /** 根据新的敏感词表重构前缀树 */
        @GetMapping("/reconstructTrie")
        public ResultVO updateTrie() throws Exception {
            List words = disallowWordService.getALlWordValue();
            if (words == null) {
                throw new Exception("敏感词表数据为空");
            }
            ValidUtil.setTrie(Trie.constructTrie(words.toArray(new String[words.size()])));
            return ResultVOUtil.success(null);
        }
    
        /** 向前缀树中插入新单词 */
        @PostMapping("/addTrie")
        public ResultVO addTrie(@Param("disallowword") String disallowword) throws Exception {
            if (disallowword == null) {
                throw new Exception("敏感词为空");
            }
            ValidUtil.getTrie().insert(disallowword);
            return ResultVOUtil.success(null);
        }
        @PostMapping("/deleteTrie")
        public ResultVO deleteTrie(@Param("disallowword") String disallowword) throws Exception {
            if (disallowword == null) {
                throw new Exception("敏感词为空");
            }
            ValidUtil.getTrie().delete(disallowword);
            return ResultVOUtil.success(null);
        }
    }
    

    或者在向数据库中的disallow_word表进行添加和删除操作的同时也对前缀树进行跟新。
    Trie树结构代码如下:

    public class Trie {
        static class TrieNode {
            // 字符为键,树节点为值
            private HashMap children = new HashMap<>();
            // 判断是否是树的叶子节点
            private boolean isEnd;
        }
        private TrieNode root;
        public Trie() {
            root = new TrieNode();
        }
        /** 插入一个字符串 */
        public void insert(String word) {
            TrieNode cur = root;
            for (int i = 0; i < word.length(); i ++) {
                char c = word.charAt(i);
                if (!cur.children.containsKey(c)) {
                    cur.children.put(c, new TrieNode());
                }
                cur = cur.children.get(c);
            }
            cur.isEnd = true;
        }
        /** 删除一个字符串 */
        public void delete(String word) {
            if (searchWord(word)) {
                TrieNode cur = root;
                for (int i = 0; i < word.length() - 1; i ++) {
                    char c = word.charAt(i);
                    cur = cur.children.get(c);
                }
                char c = word.charAt(word.length() - 1);
                if (cur.children.get(c).children.size() == 0) {
                    cur.children.remove(c);
                } else {
                    cur.children.get(c).isEnd = false;
                }
            }
    
        }
        /** 得到前缀搜索得到的树节点 */
        private TrieNode searchPrefix(String prefix) {
            TrieNode cur = root;
            for (int i = 0; i < prefix.length(); i ++) {
                char c = prefix.charAt(i);
                if (cur.children.containsKey(c)) {
                    cur = cur.children.get(c);
                } else {
                    return null;
                }
            }
            return cur;
        }
        /** 搜索前缀树是否存在字符串word */
        public boolean searchWord(String word) {
            TrieNode cur = searchPrefix(word);
            return cur != null && cur.isEnd == true;
        }
        /** 搜索前缀树中是否存在前缀prefix */
        public boolean startPrefix(String prefix) {
            TrieNode cur = searchPrefix(prefix);
            return cur != null;
        }
        public static Trie constructTrie(String[] words) {
            Trie trie = new Trie();
            for (int i = 0; i < words.length; i ++) {
                trie.insert(words[i]);
            }
            return trie;
        }
    
        public static void main(String[] args) {
            Trie trie = new Trie();
            trie.insert("ab");
            trie.insert("abd");
            trie.insert("abe");
            trie.insert("ac");
            trie.insert("acf");
            trie.insert("ca");
            System.out.println(trie.searchWord("ab"));
            trie.delete("ab");
            System.out.println(trie.searchWord("ab"));
            System.out.println(trie.searchWord("abd"));
            System.out.println(trie.searchWord("abe"));
        }
    }
    

    2.建表语句:

    DROP TABLE IF EXISTS `disallow_word`;
    CREATE TABLE `disallow_word` (
      `word_id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
      `word_value` varchar(32) NOT NULL COMMENT '敏感词值',
      `word_type` tinyint NOT NULL DEFAULT (0) COMMENT '敏感词类型',
      PRIMARY KEY (`word_id`),
      KEY `word_value_key` (`word_value`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='敏感词表';
    insert  into `disallow_word`(`word_id`,`word_value`,`word_type`) values (1,'尼玛',0),(2,'sb',0),(3,'草泥马',0);
    

    github地址:https://github.com/shuaishuaixi03/FromZerotoExpert/tree/dev

    0 赞同 7 条回复
    • shuaidi 永久会员 @ xiaoxi 2022年9月23日 下午10:35

      这考虑的很周到啊

    • xiaoxi 永久会员 @ shuaidi 2022年9月24日 上午10:57

      应该还是有点不足的,做时思考了部分时间

    • 秊 永久会员 @ xiaoxi 2022年9月24日 上午10:29

      微信没找到你。。关于”在前缀树中删除一个字符串”我有一点疑问,在你的算法中:若要删除的字符串在Trie中的尾节点没有子节点,则只删除这个尾结点,同时这个尾结点对应的isEnd标志也删除了,但是要删除的字符串除了尾结点其它还在Trie中,也就是没删干净,即使这样在后续的逻辑判断中是没问题的,但也造成了空间浪费;且在极端情况下,假如删除了一个很长的敏感词,而给出的字符串只与这个敏感词差一个字,即使最后的结果是没有这个敏感词,相比于完全删除这个字符串,搜索的时间长了。

    • xiaoxi 永久会员 @ 秊 2022年9月24日 上午11:06

      是有点问题,我当初考虑到前缀树中有ab,abc,acf,abe,abd,若是只删除ab,却删除了a、b两个节点,那么后面的abc,acf,abe,abd都查不到。可以改进,找到第一个子节点中只包含要删除的目标字符串中的字符节点,删除这个节点及它的子节点,比如一颗前缀树包含abcdefg,ac,bc,此时要删除abcdefg,找到的节点应该为c,应该删除c这个节点及之后的节点d,e,f,g

    • 秊 永久会员 @ xiaoxi 2022年9月24日 上午11:08

      我已经实现出来了?嘿嘿

    • xiaoxi 永久会员 @ 秊 2022年9月24日 上午11:11

      但是这样是提高了查询的效率,却降低的插入的效率,我没删除的节点可以留下来为下次插入的字符串的做准备,比如我插入了abcd,后面删除了abcd,但只释放了d节点,后面我在插入abcf,省下了申请a、b、c的三个节点空间的时间,只需申请f节点的空间,并把加入到c节点的子节点中,把f节点的isEnd设为true.而且根据场景,敏感词的长度一般有限制,不会太长,就是说树的高度不会太高,这颗前缀树应该是矮胖型的。很长的敏感词应该很少或者不存在

    • 秊 永久会员 @ xiaoxi 2022年9月24日 上午11:17

      对,那就是鱼和熊掌的问题了?

  • 秊 永久会员 2022年9月21日 下午4:48
    1. 敏感词算法:
    
    // UserService 
    @PostConstruct
        public void init() {
            List impolitePhrases = disallow_wordService. selectValue();
            root = new Trie();
            for (String s : impolitePhrases) {
                root.insert(s);
            }
        }
    
    // Disallow_wordService 
    public List selectValue() {
            List disallow_wordList = disallow_wordMapper.selectByExample(null);
            List impolitePhrases = new ArrayList<>();
            for (Disallow_word disallow_word : disallow_wordList) {
                impolitePhrases.add(disallow_word.getValue());
            }
            return  impolitePhrases;
        }
    
    // UserController
    public static int userNamePolite(String userName, Trie root) {
    
            if (root.check(userName, root) != null) {  // 传入敏感词Trie
                return 1;
            }
            return 0;
        }
    
    
    // Trie
    public class Trie {
    
        private HashMap children;
        private boolean isEnd;
    
        // 每个Trie的数据内容为上一个Trie的children,自身是否为end是当前自己的 isEnd标志
        public Trie() {
            children = new HashMap();
            isEnd = false;
        }
    
        public void insert(String singleWord) {   // 先插入后匹配
            Trie node = this;
            for (int i = 0; i < singleWord.length(); ++i) {
                char ch = singleWord.charAt(i);
                if (!node.children.containsKey(ch)) {
                    node.children.put(ch, new Trie());
                }
                node = node.children.get(ch);
                if (i == singleWord.length() - 1) {
                    node.isEnd = true;
                }
            }
        }
    
        /**
         * 若 singleWord 存在 Trie 中, 则删除它
         * @param singleWord
         * @param root
         */
        public void delete(String singleWord, Trie root) {
            Trie node = check(singleWord, root);
            if (node == null) return;
            if (node.children.isEmpty() != true) {  // 这个分支下还有其它敏感词的尾节点
                node.isEnd = false;
            } else {  // 这个分支下面没有其它敏感词的尾节点, 但这个分支上面可能有
                Trie preWaitDel = root;   // 最后要开始往后删除字符的前一个节点
                char preWaitDelKey = singleWord.charAt(0);  // 最后要开始往后删除字符的值
                node = root;
                for (int i = 0; i < singleWord.length() - 1; ++i) {  // 最后一个字符上面if已经判断过了
                    char c = singleWord.charAt(i);
                    node = node.children.get(c);
                    // 如果当前节点子节点不止一个, 即除了当前敏感词的下一个节点之外还有其它节点, 或者当且节点就是其它敏感词的末节点, 更新两个待删除所需值
                    if (node.children.size() != 1 || node.isEnd == true) {
                        preWaitDel = node;
                        preWaitDelKey = singleWord.charAt(i + 1);
                    }
                }
                preWaitDel.children.remove(preWaitDelKey);  // 最后删除
            }
        }
    
        /**
         *  检测整串昵称是否含有敏感词汇
         * @param target 目标字符串
         * @param root 敏感词汇Trie
         * @return 若找到则返回敏感词在Trie中的尾结点, 否则null
         */
        public Trie check(String target, Trie root) {
            for (int p1 = 0; p1 < target.length(); ++p1) {
                char ch = target.charAt(p1);
                Trie node = root;
                if (root.children.containsKey(ch)) {
                    node = root.children.get(ch);
                    if (node.isEnd) {
                        return node;
                    } else {
                        for (int p2 = p1 + 1; p2 < target.length(); ++p2) {
                            char cch = target.charAt(p2);
                            if (node.children.containsKey(cch)) {
                                node = node.children.get(cch);
                                if (node.isEnd) {
                                    return node;
                                }
                            } else {
                                break;   // 匹配中断后中返回p1继续往后匹配
                            }
                        }
                    }
                }
            }
            return null;
        }
    
        public static void main(String[] args) {
            Trie root = new Trie();
            root.insert("abcde");
            root.insert("abcf");
            if (root.check("abcde", root) != null) {
                System.out.println("找到了");
            }
            root.delete("abcde", root);
            if (root.check("abcf", root) != null) {  // 在这里查看内存, 发现没有了"de"两个节点
                System.out.println("找到了");
            }
        }
    }
    
    1. 建表语句:
      drop table if exists disallow_word;
      create table disallow_word (
      id bigint auto_increment not null,
      value varchar(50) not null comment '敏感字',
      type int default(0) comment '类型',
      primary key (id)
      ) engine=innodb default charset=utf8mb4 comment='敏感词库';

    insert into disallow_word(value) values ('尼玛');
    insert into disallow_word(value) values ('站长');
    insert into disallow_word(value) values ('国家领导人');
    insert into disallow_word(value) values ('操');
    insert into disallow_word(value) values ('lala');

    1. 更新: 项目运行过程中,访问接口动态添加/删除敏感词, 以“添加”为例:
    // Controller
    @PostMapping("/add")
        @ResponseBody
        public CommonResp add(Disallow_word disallow_word) {
            CommonResp commonResp = new CommonResp<>();
            disallow_wordService.add(disallow_word.getValue(), commonResp);
            return commonResp;
        }
    
    // Service
    public void add(String value, CommonResp commonResp) {
            if (Objects.equals(value, "")) {
                commonResp.setSuccess(false);
                commonResp.setMessage(commonResp.getMessage() + "填入敏感词为空");
            }
            Disallow_wordService.root.insert(value, Disallow_wordService.root);
        }
    

    服务器链接:http://124.223.184.187:8088/FromZerotoExpert
    Github仓库链接:https://github.com/jiangyklala/FromZerotoExpert/tree/dev

    0 赞同 0 条回复
  • Second(S) 普通 2022年9月16日 下午9:20

    1.敏感词过滤代码

        // Contoller层构建字典树对象
        @PostConstruct
        public void init(){
            Trie root = new Trie();
            List disallowWords = disallowWordService.selectAll();
            for(int i = 0; i < disallowWords.size(); i++){
                root.insert(disallowWords.get(i).getValue());
            }
            words = root;
        }
    
    
        /**
         * 敏感词检测
         * @param username 用户名
         * @param root 已经构建好的字典树
         * @return
         */
        public static RegisterResult checkDisalowWord(String username, Trie root){
            int i = 0;
            while(i < username.length()){
                int j = i;
                // 若username子串前缀不为敏感词前缀,一定不是敏感词
                // 若子串前缀为敏感词前缀,则继续检测是否为敏感词,若是,则返回false和信息否则继续检测,直至跳出循环。
                while(j < username.length() && root.startsWith(username.substring(i, j + 1))){
                    if(root.search(username.substring(i, j + 1))){
                        return new RegisterResult(false, "用户名含有敏感词汇!");
                    }
                    j++;
                }
                i++;
            }
            return new RegisterResult(true);
        }
    

    在这里用到了@PostConstruct注解,目的是防止每次敏感词检测时都要访问数据库表建立字典树,使用@PostConstruct注解之后,只会在Controller加载完毕时访问一次敏感词数据库表,并完成字典树的建立,当需要进行敏感词检测时,将已经创建好的字典树对象传入敏感词过滤算法中,即可完成检测。
    2.建表语句
    DROP TABLE IF EXISTS disallow_word;

    CREATE TABLE disallow_word(
    id INT PRIMARY KEY AUTO_INCREMENT,
    value varchar(255) NOT NULL,
    type INT DEFAULT(0)
    )ENGINE=INNODB;

    INSERT INTO disallow_word(value) VALUES("尼玛");
    INSERT INTO disallow_word(value) VALUES("站长");
    INSERT INTO disallow_word(value) VALUES("国家领导人");
    INSERT INTO disallow_word(value) VALUES("操");

    注:在项目文件夹中含有from_zeroto_expert.sql文件,里面记录了所有建库的sql代码。

    gitHub路径:https://github.com/Second6s9/FromZerotoExpert/tree/dev
    gitHub防覆盖路径:https://github.com/Second6s9/FromZerotoExpert/tree/day07
    服务器地址:http://39.101.66.57:8080/FromZerotoExpert/register

    0 赞同 1 条回复

# 回答此问题

后才能回答