zoukankan      html  css  js  c++  java
  • 【敏感词检测】用DFA构建字典树完成敏感词检测任务

    任务概述

    敏感词检测是各类平台对用户发布内容(UGC)进行审核的必做任务。
    对于文本内容做敏感词检测,最简单直接的方法就是规则匹配。构建一个敏感词词表,然后与文本内容进行匹配,如发现有敏感词,则提交报告给人工审核或者直接加以屏蔽。
    当然也可以用机器学习的方法来做,不过需要收集及标注大量数据,有条件的话也可以加以实现。

    任务难点及解决策略

    1)对抗检测的场景:比如同音替换、字形替换、隐喻暗指、词中间插入特殊字符等。

    解决策略:特殊字符可以使用特殊字符词表过滤,其他几种不好解决,主要通过扩大敏感词表规模来。

    2)断章取义的问题:从语义上理解没有问题,但按窗口大小切出几个字来看,却属于敏感词,造成误报。

    解决策略:这个问题主要是分词错误导致的,应当考虑分词规则,而不是无脑遍历,或者用正则匹配。

    3)检测效率问题:随着词表的增大,循环查找词表的速度会变得很慢。

    解决策略:使用DFA算法构建字典树。

    4)词的歧义问题:一个词某个义项是违规的,但其他义项或许是正常的。

    解决策略:这个要结合上下文考虑,机器学习类的方法比较容易解决这一问题。

    5)词表质量问题:从网络上获取得到的敏感词词表,要么包含词汇较少,不能满足检测需求,要么包含词汇过多,检测出了很多当前业务场景下不需要屏蔽的词。

    解决策略:需要定期人工整理。

    敏感词字典树的构建

    构建字典树使用的是确定有限自动机(DFA)。DFA算法的核心是建立了以敏感词为基础的许多敏感词树。
    它的基本思想是基于状态转移来检索敏感词,只需要扫描一次待检测文本(长度n),就能对所有敏感词进行检测。
    且DFA算法的时间复杂度O(n)基本上是与敏感词的个数无关的,只与文本长度有关。

    如下图所示,比如abcd,abd,bcd,efg,hij这几个词在树中表示如下。
    中文的常用字只有四五千个,但由这些字构成的词却难以计数,如果循环遍历,时间消耗极大,而通过字典树,沿着根节点向下,每走一步就可以极大地缩小搜索空间。

    代码实现

    import jieba
    
    min_match = 1  # 最小匹配原则
    max_match = 2  # 最大匹配原则
    
    class SensitiveWordDetect:
        def __init__(self, sensitive_words_path, stopWords_path):
            #============把敏感词库加载到列表中====================  
            temp_line_list = []
            with open(sensitive_words_path, 'r', encoding='utf-8') as file:
                temp_line_list = file.readlines()
                
            self.sensitive_word_list = sorted([i.split('
    ')[0] for i in temp_line_list])
            # print(self.sensitive_word_list[-10:])
            
            #============把停用词加载到列表中======================
            temp_line_list_2 = []
            with open(stopWords_path, 'r', encoding='utf-8') as file:
                temp_line_list_2 = file.readlines()
                
            self.stop_word_list = [i.split('
    ')[0] for i in temp_line_list_2]
            
            #==============得到sensitive字典=======================
            self.sensitive_word_map = self.init_sensitive_word_map(self.sensitive_word_list)
            #print(self.sensitive_word_map)
            #print(len(self.sensitive_word_map)) 
            
        # 构建敏感词库
        def init_sensitive_word_map(self, sensitive_word_list):
            sensitive_word_map = {}
            # 读取每一行,每一个word都是一个敏感词
            for word in sensitive_word_list:
                now_map = sensitive_word_map
                # 遍历该敏感词的每一个特定字符
                for i in range(len(word)):
                    keychar = word[i]
                    word_map = now_map.get(keychar)
                    if word_map != None:
                        # now_map更新为下一层
                        now_map = word_map
                    else:
                        # 不存在则构建一个map, isEnd设置为0,因为不是最后一个
                        new_next_map = {}
                        new_next_map["isEnd"] = 0
                        now_map[keychar] = new_next_map
                        now_map = new_next_map
                    # 到这个词末尾字符
                    if i == len(word)-1:
                        now_map["isEnd"] = 1
            # print(sensitive_word_map)
            return sensitive_word_map
            
        def check_sensitive_word(self, txt, begin_index=0, match_mode=min_match):
            '''
            :param txt: 输入待检测的文本
            :param begin_index:输入文本开始的下标
            :return:返回敏感词字符的长度
            '''
            now_map = self.sensitive_word_map
            sensitive_word_len = 0 # 敏感词的长度
            contain_char_sensitive_word_len = 0 # 包括特殊字符敏感词的长度
            end_flag = False #结束标记位
    
            for i in range(begin_index, len(txt)):
                char = txt[i]
                if char in self.stop_word_list:
                    contain_char_sensitive_word_len += 1
                    continue
    
                now_map = now_map.get(char)
                if now_map != None:
                    sensitive_word_len += 1
                    contain_char_sensitive_word_len += 1
                    # 结束位置为True
                    if now_map.get("isEnd") == 1:
                        end_flag = True
                        # 最短匹配原则
                        if match_mode == min_match:
                            break
                else:
                    break
            if end_flag == False:
                contain_char_sensitive_word_len = 0
            #print(sensitive_word_len)
            return contain_char_sensitive_word_len
            
        def get_sensitive_word_list(self, txt):
            # 去除停止词
            new_txt = ''
            for char in txt:
                if char not in self.stop_word_list:
                    new_txt += char
            # 然后分词
            seg_list = list(jieba.cut(new_txt, cut_all=False))
            
            cur_txt_sensitive_list = []
            # 注意,并不是一个个char查找的,找到敏感词会增强敏感词的长度
            for i in range(len(txt)):
                length = self.check_sensitive_word(txt, i, match_mode=max_match)
                if length > 0:
                    word = txt[i:i+length]
                    cur_txt_sensitive_list.append(word)
                    i = i+length-1  # 出了循环还要+1 i+length是没有检测到的,下次直接从i+length开始
            
            # 对得到的结果和分词结果进行匹配,不匹配的不要
            rst_list = []
            for line in cur_txt_sensitive_list:
                new_line = ''
                for char in line:
                    if char not in self.stop_word_list:
                        new_line += char
                if new_line in seg_list:
                    rst_list.append(line)
            return rst_list
            
        def replace_sensitive_word(self, txt, replace_char='*'):
            lst = self.get_sensitive_word_list(txt)
            #print(lst)
            # 如果需要加入的关键词,已经在关键词列表存在了,就不需要继续添加
            def judge(lst, word):
                if len(lst) == 0:
                    return True
                for str in lst:
                    if str.count(word) != 0:
                        return False
                return True
                
            # 最严格的打码,选取最长打码长度
            for word in lst:
                replace_str = len(word) * replace_char
                txt = txt.replace(word, replace_str)
                
            new_lst = []
            for word in lst:
                new_word = ""
                # newWord是除去停用词、最精炼版本的敏感词
                for char in word:
                    if char in self.stop_word_list:
                        continue
                    new_word += char
                length = self.check_sensitive_word(new_word, 0, match_mode=min_match)
                if judge(new_lst, new_word[:length]):
                    new_lst.append(new_word[:length])
                else:
                    continue
    
            return txt, new_lst # 最终返回的结果是屏蔽敏感词后的文本,以及检测出的敏感词
    
  • 相关阅读:
    前端环境搭建
    mybatis
    An internal error occurred during: "Launching on Tomcat 7.x"
    mybatis批量添加、批量删除
    linux系统下怎么关闭一个端口
    httpclient传参类型与响应参数接收
    scheduled定时任务cron表达式知识地址
    ClassNotFoundException: http.nio.NHttpClientEventHandle
    发送请求工具—Advanced REST Client的安装使用
    linux下Tomcat配置提示权限不够解决办法
  • 原文地址:https://www.cnblogs.com/yanqiang/p/13530815.html
Copyright © 2011-2022 走看看