python-DFA算法敏感词检索

敏感词检索功能

根据 DFA 算法思想进行实现,主要包括两方面的内容:

  1. 将收集好的敏感词库生成 Trie 树
  2. 按照项目需求,对文本中的敏感词进行检索或者处理

算法复杂度:

  1. Trie树: 构建-O(n)
  2. 敏感词:检索-O(n)

说明:

​ 构建 Trie 树的过程,需要扫描所有的字符串,时间复杂度是 O(n)n 表示所有字符串的长度和)。每次查询时,如果要查询的字符串长度是 n,那只需要比对大约 n 个节点,就能完成查询操作,时间复杂度为 O(n)n 表示要查找的字符串的长度)。

DFA 算法简介

DFA 简介

在实现文字过滤的算法中,DFA 是比较好的实现算法。DFADeterministic Finite Automaton,也就是确定有穷自动机。

这类系统具有一系列离散的输入输出信息和有穷数目的内部状态(状态:概括了对过去输入信息处理的状况)。

系统只需要根据当前所处的状态和当前面临的输入信息就可以决定系统的后继行为。每当系统处理了当前的输入后,系统的内部状态也将发生改变。

算法核心是建立了以敏感词为基础的多个敏感词树,只需要扫描一次待检测文本,就能对所有敏感词进行检测。

Trie 树简介

Trie 树又称字典树、单词查找树、前缀树,是一种能够高效存储和快速检索字符串集合的多叉树结构的数据结构。

Trie 树性质

  1. 根节点不包含字符,其他节点包含一个字符
  2. 从根节点到某一节点经过的字符连接起来构成一个字符串
  3. 一个字符串与 Trie 树中的一条路径对应
  4. 在实现过程中,会在叶节点中设置一个标志,用来表示该节点是否是一个字符串的结尾
  5. 插入查找的复杂度为 Ο(n)n 为被检索字符串长度

DFA (确定有穷自动机)

简介

DFA 利用了 Trie 树来实现文本检索。

​ 算法核心是建立了以敏感词为基础的多个敏感词树,只需要扫描一次待检测文本,就能对所有敏感词进行检测。

Trie 树特性: 把敏感词组成成树形结构最大的好处就是可以减少检索次数,我们只需要遍历一次待检测文本,然后在敏感词库中检索有没有该字符对应的子树,如果没有相应的子树,说明当前检测的字符不在敏感词库中,则直接跳过继续检测下一个字符;如果有相应的子树,则接着检查下一个字符是不是前一个字符对应的子树的子节点,这样迭代下去,就能找出待检测文本中是否包含敏感词了。而且对于被检测文本中不存在的敏感词,完全不会扫描到。

​ 什么是 **NFA **(不确定的有穷自动机) 和 **DFA **(确定的有穷自动机) ,参考:http://t.zoukankan.com/HIT-ryp-p-12874153.html

字典树示例

1. 树结构示例

​ 假设我们有以下5个敏感词需要检测:****。那么我们可以先把敏感词中有相同前缀的词组合成一个树形结构,不同前缀的词分

属不同树形分支,以上述5个敏感词为例,可以初始化成如下2棵树:

在这里插入图片描述

在这里插入图片描述

总结
Trie 树的本质就是利用字符串之间的公共前缀(节省存储空间),将重复的前缀合并在一起存储为树形结构。
Trie 树的特点是利用字符串的公共前缀来减少查询时间,最大限度减少字符串匹配的时间。

2. python 数据结构示例

python 中用字典类型来存储上述的树形结构,以上述敏感词为例,把每个敏感词字符串拆散成字符,再存储到 dict 中:

{
    '傻': {
        '逼': {
            '\x00': 0
        }, 
        '子': {
            '\x00': 0
        }, 
        '大': {
            '个': {
                '\x00': 0
            }
        }
    }, 
    '坏': {
        '蛋': {
            '\x00': 0
        }, 
        '人': {
            '\x00': 0}
    }
}

3. python 代码实现

    """
    首先将每个词的第一个字符作为 key,value 则是另一个 dict,value 对应的 dict 的 key 为第二个字符,
    如果还有第三个字符,则存储到以第二个字符为 key 的value 中,当然这个 value 还是一个 dict,以此类推下去,直到最后一个字符,
    当然最后一个字符对应的 value 也是 dict,只不过这个 dict 只需要存储一个结束标志,
    如上述的例子中,存了一个 {'\x00': 0} 的 dict,来表示这个 value 对应的 key 是敏感词的最后一个字符。
    简言之,就是字典套字典结构。
    dict 在理想情况下可以以 O(1) 的时间复杂度进行查询,所以在遍历待检测字符串的过程中,可以以 O(1) 的时间复杂度检索出当前字符是否在敏感词库中
    """
    
    def __init__(self):
        """
        算法初始化
        """
        # 敏感词链表
        self.root = dict()
        # 敏感词限定
        self.delimit = '\x00'
        # 无意义词库,在检测中需要跳过的(这种无意义的词最后有个专门的地方维护,保存到数据库或者其他存储介质中)
        self.skip_root = [' ', '&', '!', '!', '@', '#', '$', '¥', '*', '^', '%', '?', '?', '<', '>', "《", '》', ",", "。",
                          ",", ".", "\\", "|", "/", "\'", "\"", "“", "‘", ";", ";", ":", ":", "~", "`", "-", "+", "=",
                          "_"]
        # 初始化敏感词库
        for sensitive_word in self.read_sensitive_words():
            self.add_word(sensitive_word)

    @staticmethod
    def read_sensitive_words():
        """
        读取敏感词库
        :return: 返回敏感词列表
        """
        sensitive_words_list = []

        with open('sensitive_words_conf/sensitive_words.conf', mode='r', encoding='utf-8') as fp:
            sensitive_words = fp.readlines()
            for sensitive_word in sensitive_words:
                # 敏感词英文变为小写、去除首尾空格和换行
                sensitive_word = sensitive_word.lower().strip()
                # 如果敏感词为空直接跳过
                if not sensitive_word:
                    continue
                # 将敏感词添加到敏感词列表
                sensitive_words_list.append(sensitive_word)

        return sensitive_words_list

    def add_word(self, sensitive_word):
        """
        敏感词入库
        :param sensitive_word: 敏感词
        """
        now_node = self.root
        # 遍历敏感词的每个字
        for i in range(len(sensitive_word)):
            # 如果这个字已经存在字符链的key中就进入其子字典
            if sensitive_word[i] in now_node:
                now_node = now_node[sensitive_word[i]]
            else:
                if not isinstance(now_node, dict):
                    break
                # 遍历字符生成此敏感词的字典树    
                for j in range(i, len(sensitive_word)):
                    now_node[sensitive_word[j]] = {}
                    last_level, last_char = now_node, sensitive_word[j]
                    now_node = now_node[sensitive_word[j]]
                last_level[last_char] = {self.delimit: 0}
                break
        # 遍历完字符串,字典树添加结束标志
        if i == len(sensitive_word) - 1:
            now_node[self.delimit] = 0

敏感词检索

1. 检索示例

{
    '傻': {
        '逼': {
            '\x00': 0
        }, 
        '子': {
            '\x00': 0
        }, 
        '大': {
            '个': {
                '\x00': 0
            }
        }
    }, 
    '坏': {
        '蛋': {
            '\x00': 0
        }, 
        '人': {
            '\x00': 0}
    }
}

以文本 “你是不是”** 为例,依次检测每个字符,因为前4个字符都不在敏感词库里,找不到相应的子树,直接跳过。当检测到 “傻” 字时,发现敏感词库中有相应的子树,接着再搜索下一个字符 “*” 是不是子树的子节点,发现是,接下来再判断 “*” 这个字符是不是叶子节点,如果是,则说明匹配到了一个敏感词了,在这里 “*” 这个字符刚好是的子树的叶子节点,所以成功检索到了敏感词:”**。在搜索过程中,只需要扫描一次被检测文本,而且对于被检测文本中不存在的敏感词,如这个例子中的 “坏蛋”和“坏人”,完全不会扫描到。

2. python 代码实现

    def match_word(self, message, repl="", need_first=False, need_all=False):
        """
        匹配敏感词
        :param message: 待检测的文本
        :param repl: 用于替换的字符,匹配的敏感词以字符逐个替换,如"你是大王八",敏感词"王八",替换字符*,替换结果"你是大**"
        :param need_first: True,返回匹配到的第一个敏感词
        :param need_all: True,返回匹配到的全部敏感词
        :return: 根据 need_first、need_all、need_replace 返回对应的结果
        """
        message = message.lower()
        ret = []
        start = 0
        sensitive_word = []
        while start < len(message):
            level = self.root
            # 是否违禁
            is_unlawful = False
            checked_chars = []
            for char in message[start:]:
                start += 1
                # 空字符过滤
                if not char.strip():
                    continue
                # 输入的敏感词中特殊符号的过滤    
                if char in self.skip_root and (need_first or need_all):
                    continue
                checked_chars.append(char)
                # 判断连续是否为敏感词,及敏感词的长度
                if char in level:
                    # 循环结束的条件,条件中有限定符:
                    # 1. 只有一个元素
                    # 2. 有多个元素,且其中有一个限定符,且下一个元素不在条件中
                    # 3. 文本结束,且有限定符,没有下一个字符
                    # 4. 不满足条件,则不需要替换
                    if self.delimit in level[char]:

                        if len(level[char]) == 1 or (start < len(message) and message[start] not in level[char]) or start >= len(message):
                            is_unlawful = True
                            sensitive_word.append(''.join(checked_chars))
                            # 返回第一个匹配到的敏感词,检索结束
                            if need_first:
                                return sensitive_word
                            break

                    level = level[char]
                # 跳过特殊字符    
                elif char in self.skip_root:
                    continue
                else:
                    break
			
            # 是否替换敏感词
            if is_unlawful:
                append_str = self.replace_char(checked_chars, repl)
                ret.append(append_str)
            else:
                # 未成功匹配但检索到部分敏感词,将指针移动到匹配的敏感词的第一个字符后
                if len(checked_chars) > 1:
                    start = start - len(checked_chars) + 1
                    ret.append(checked_chars[0])    
                else:
                    ret.extend(checked_chars)
		
        # 将匹配到的敏感词列表去重
        if need_all and sensitive_word:
            sensitive_word = sorted(set(sensitive_word), key=sensitive_word.index)

        return ''.join(ret) if not (need_first or need_all) else sensitive_word

完整代码示例

# !/usr/bin/env python
# -*- coding:utf-8 -*-
import json


class DFAUtils(object):
    """
    DFA算法:敏感词过滤
    """

    def __init__(self):
        """
        算法初始化
        """
        # 敏感词链表
        self.root = dict()
        # 敏感词限定
        self.delimit = '\x00'
        # 无意义词库,在检测中需要跳过的(这种无意义的词最后有个专门的地方维护,保存到数据库或者其他存储介质中)
        self.skip_root = [' ', '&', '!', '!', '@', '#', '$', '¥', '*', '^', '%', '?', '?', '<', '>', "《", '》', ",", "。",
                          ",", ".", "\\", "|", "/", "\'", "\"", "“", "‘", ";", ";", ":", ":", "~", "`", "-", "+", "=",
                          "_"]
        # 初始化敏感词库
        for sensitive_word in self.read_sensitive_words():
            self.add_word(sensitive_word)

    @staticmethod
    def read_sensitive_words():
        """
        读取敏感词库
        :return: 返回敏感词列表
        """
        sensitive_words_list = []

        with open('sensitive_words_conf/sensitive_words.conf', mode='r', encoding='utf-8') as fp:
            sensitive_words = fp.readlines()
            for sensitive_word in sensitive_words:
                # 敏感词英文变为小写、去除首尾空格和换行
                sensitive_word = sensitive_word.lower().strip()
                # 如果敏感词为空直接跳过
                if not sensitive_word:
                    continue
                # 将敏感词添加到敏感词列表
                sensitive_words_list.append(sensitive_word)

        return sensitive_words_list

    def add_word(self, sensitive_word):
        """
        敏感词入库
        :param sensitive_word: 敏感词
        """
        now_node = self.root
        # 遍历敏感词的每个字
        for i in range(len(sensitive_word)):
            # 如果这个字已经存在字符链的key中就进入其子字典
            if sensitive_word[i] in now_node:
                now_node = now_node[sensitive_word[i]]
            else:
                if not isinstance(now_node, dict):
                    break
                for j in range(i, len(sensitive_word)):
                    now_node[sensitive_word[j]] = {}
                    last_level, last_char = now_node, sensitive_word[j]
                    now_node = now_node[sensitive_word[j]]
                last_level[last_char] = {self.delimit: 0}
                break
        if i == len(sensitive_word) - 1:
            now_node[self.delimit] = 0

    def match_word(self, message, repl="", need_first=False, need_all=False):
        """
        匹配敏感词
        :param message: 待检测的文本
        :param repl: 用于替换的字符,匹配的敏感词以字符逐个替换,如"你是大王八",敏感词"王八",替换字符*,替换结果"你是大**"
        :param need_first: True,返回匹配到的第一个敏感词
        :param need_all: True,返回匹配到的全部敏感词
        :return: 根据 need_first、need_all、need_replace 返回对应的结果
        """
        message = message.lower()
        ret = []
        start = 0
        sensitive_word = []
        while start < len(message):
            level = self.root
            # 是否违禁
            is_unlawful = False
            checked_chars = []
            for char in message[start:]:
                start += 1
                if not char.strip():
                    continue
                if char in self.skip_root and (need_first or need_all):
                    continue
                checked_chars.append(char)
                # 判断连续是否为敏感词,及敏感词的长度
                if char in level:
                    # 循环结束的条件,条件中有限定符:
                    # 1. 只有一个元素
                    # 2. 有多个元素,且其中有一个限定符,且下一个元素不在条件中
                    # 3. 文本结束,且有限定符,没有下一个字符
                    # 4. 不满足条件,则不需要替换
                    if self.delimit in level[char]:

                        if len(level[char]) == 1 or (start < len(message) and message[start] not in level[char]) or start == len(message):
                            is_unlawful = True
                            sensitive_word.append(''.join(checked_chars))
                            if need_first:
                                return sensitive_word
                            break

                    level = level[char]
                elif char in self.skip_root:
                    continue
                else:
                    break

            if is_unlawful:
                append_str = self.replace_char(checked_chars, repl)
                ret.append(append_str)
            else:
                if len(checked_chars) > 1:
                    start = start - len(checked_chars) + 1
                    ret.append(checked_chars[0])
                else:
                    ret.extend(checked_chars)

        if need_all and sensitive_word:
            sensitive_word = sorted(set(sensitive_word), key=sensitive_word.index)

        return ''.join(ret) if not (need_first or need_all) else sensitive_word

    @staticmethod
    def replace_char(checked_chars, repl):
        """
        :param checked_chars: 被检测字符
        :param repl: 用于替换的字符,匹配的敏感词以字符逐个替换
        :return: 根据 repl 返回替换的字符
        """
        replace_msg = len(checked_chars) * repl if repl else checked_chars
        return replace_msg

    def get_first_word(self, message):
        """
        获取匹配到的词语
        :param message: 待检测的文本
        :return: 返回匹配到的第一个敏感词
        """
        first_word = self.match_word(message, need_first=True)
        return first_word[0] if first_word else "文本不涉及敏感词汇"

    def get_all_word(self, message):
        """
        获取匹配到的词语
        :param message: 待检测的文本
        :return: 返回匹配到的全部敏感词
        """
        all_word = self.match_word(message, need_all=True)
        return all_word if all_word else "文本不涉及敏感词汇"

    def replace_match_word(self, message):
        """

        获取匹配到的词语
        :param message: 待检测的文本
        :return: 返回已替换匹配到的全部敏感词的文本
        """
        replace_message = self.match_word(message, repl="*")
        return replace_message


dfa_sensitive_words = DFAUtils()

if __name__ == '__main__':
    # 待检测的文本
    msg = '我是中国人h封从德h,a冯素英aa,   aaa啊啊啊,  asf破达克赛德'
    print(msg)
    print('获取第一个敏感词:', dfa_sensitive_words.get_first_word(msg))
    print('获取全部的敏感词:', dfa_sensitive_words.get_all_word(msg))
    print('获取替换后的文本:', dfa_sensitive_words.replace_match_word(msg))
  • 2
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

I believe I can fly~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值