DFA敏感词过滤算法--Python实现

问题描述:

涉及语言对话的项目中,难免遇到敏感词过滤的需求。这里介绍一下常用的DFA(Deterministic Finite Automaton 确定有限自动机)算法,使用python实现。

DFA流程:

我们的目光首先回到需求--敏感词过滤--上面,逐步分化这个任务。

任务目标

一段文本里面所有的敏感词被替换为*

特点:敏感词不唯一,待检测文本有长有短。典型的多模式匹配

单模式匹配:在一个较长的文本字符串中查找一个单一的目标字符串或模式。这种情况下,搜索的重点是确定这一个模式是否存在于文本中,以及它出现的位置。

多模式匹配:同时在文本中搜索多个模式或关键词。这种类型的匹配通常用于如文本分析、过滤系统(例如敏感词过滤)和病毒扫描等场景,其中需要同时检测文本中的多个关键词或模式。

任务流程

1.敏感词的存放 2.敏感词与文本的匹配 3.匹配后的过滤替换

那么DFA的思路就是,将敏感词按照树的结构去存放,进行路径的查找,迅速且可以公用前缀

我们先理解DFA的思路:

假设我们的敏感词里面有这么几个:sexy,hello,help,helpline(纯粹举例,无其他含义)。那么我们应该这样存放:

{
    's': {
        'e': {
            'x': {
                'y': {
                    '\x00': 0
                }
            }
        }
    },
    'h': {
        'e': {
            'l': {
                'l': {
                    'o': {
                        '\x00': 0
                    }
                },
                'p': {
                    '\x00': 0,
                    'l': {
                        'i': {
                            'n': {
                                'e': {
                                    '\x00': 0
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

这里,使用嵌套字典模拟树结构,最后的终止状态字典,键为分隔符 \x00 ,值为0,来确定一个敏感词被查询出。利用嵌套字典,可以逐层进行查询,当前层匹配以后,即可进入下一层。同时,由于层级的分布,可以公用前缀,在不一样的符号处分开即可。

图解

这样就可以显著看出DFA存放敏感词的结构,采用树结构存储,将终止状态设置为叶子节点。同层次按照不同的路径查找,简单快捷。

查找举例

假设我们的文本为 A sexy girl held the helpline and said, Hello.

这时候的流程应该是:

1.统一转小写,去空白字符:a sexy girl held the helpline and said, hello.

2.设置保存过滤后文本的列表ret,并且记录层级

3.输入a,查询第一层,发现无匹配,传入ret   即 [a]

4.输入 s 第一层匹配,层级+1,继续匹配 e,x,y 遇到分隔符,替换sexy为****,加入ret,即 [a ****]

5.继续查询 girl,不匹配 ret [a **** girl]

6.查询 hel匹配,到d,发现不匹配,原字符传入,ret [a **** girl held]

7.按照这个逻辑完成全部查询,即,ret [a **** girl held the ******** and said, *****.]

8.输出ret的内容即:a **** girl held the ******** and said, *****.   完成替换。

这就是DFA敏感词过滤的算法!

下面给出python的具体实现:

class DFAFilter():

    def __init__(self):
        # 存放敏感词的字典
        self.keyword_chains = {}
        # 设置分隔符为 '\x00',用于在特定场合下作为字符串的分隔标志
        self.delimit = '\x00'

    def add(self, keyword):
        """
        将给定的敏感词添加到敏感词链中。

        :param keyword: 需要添加的关键词,将被转换为小写并去除首尾空白。
        :return: 无返回值。
        """
        keyword = keyword.lower()  # 将敏感词转换为小写
        chars = keyword.strip()  # 去除敏感词的首尾空白字符
        if not chars:  # 如果处理后的敏感词为空,则直接返回
            return
        level = self.keyword_chains  # 初始化根级别

        # 遍历敏感词中的每个字符,逐级向下创建或更新字典结构
        for i in range(len(chars)):
            # 如果字符在当前级别存在,说明可以公用,进入下一级
            if chars[i] in level:
                level = level[chars[i]]
            # 如果不存在,从当前位置创建所有缺失级别到敏感词末尾
            else:
                for j in range(i, len(chars)):
                    level[chars[j]] = {}
                    last_level, last_char = level, chars[j]
                    level = level[chars[j]]
                # 在最后一个创建的级别中,添加一个以self.delimit为键,值为0的项
                last_level[last_char] = {self.delimit: 0}
                break

        # 如果遍历到了关键词的末尾,则在当前级别添加一个以self.delimit为键,值为0的项
        if i == len(chars) - 1:
            level[self.delimit] = 0

    def parse(self, path):
        if path.endswith('pkl'):
            with open(path, "rb") as f:
                data = pickle.load(f)
                for keyword in data:
                    self.add(keyword.strip())
        else:
            with open(path, "r") as f:
                for keyword in f:
                    self.add(keyword.strip())

    def filter(self, message, repl="*"):
        """
        过滤给定消息中的敏感关键词,用指定字符替换。

        参数:
        - message: 待过滤的字符串消息。
        - repl: 用于替换敏感词的字符,默认为"*"。

        返回值:
        - 过滤后的字符串。
        """
        message = message.lower()
        ret = []  # 存储过滤后的字符
        start = 0  # 指向当前处理的字符位置
        while start < len(message):
            level = self.keyword_chains  # 初始化敏感词层级
            step_ins = 0  # 记录当前嵌套层级
            for char in message[start:]:
                if char in level:
                    step_ins += 1
                    if self.delimit not in level[char]:
                        level = level[char]
                    else:
                        # 当遇到分隔符时,添加替换字符到结果中,并调整起始位置
                        ret.append(repl * step_ins)
                        start += step_ins - 1
                        break
                else:
                    # 当字符不在关键词列表中,添加原字符到结果中,并结束当前循环
                    ret.append(message[start])
                    break
            start += 1  # 更新起始位置

        return ''.join(ret)  # 将结果列表转换为字符串并返回

与其他字符串匹配算法对比分析:

这里列出与BS(后向排序映射),KMP算法,和python包中的replace函数的对比分析。

1. BSFilter(后向排序映射)

  • 优点
    • 简单实现,直接使用 Python 的字典和集合进行关键词的管理和快速查找。
    • 适合于关键词数量不是非常大的情况,尤其是单词较短的关键词。
  • 缺点
    • 对于含有大量关键词的大型文本,效率可能较低,因为每个单词或字符都需要检查是否在字典中,且可能重复替换。
    • 不能有效处理重叠关键词或多模式匹配的复杂情况。
  • 时间复杂度
    • 理论上,最坏情况下的时间复杂度可以达到 O(mnk),其中 m 是消息长度,n 是平均关键词长度,k 是关键词数量。实际上,这取决于消息和关键词的具体内容。

2. DFA 算法

  • 优点
    • 高效处理多关键词匹配,尤其是在关键词共享前缀时表现优越。
    • 一次扫描完成匹配,适用于流式处理。
  • 缺点
    • 预处理(构建 DFA)成本高,尤其是关键词很多时。
    • 可能需要较多的内存来存储状态机。
  • 时间复杂度
    • 匹配过程的时间复杂度为 O(m),m 是文本长度。

3. Python 的 replace 方法

  • 优点
    • 极其快速和高效,对于简单的字符串替换任务几乎是最优的。
    • 内部实现高度优化,易于使用。
  • 缺点
    • 不适合复杂的模式匹配或多模式匹配任务。
    • 无法处理重叠关键词或需要查找的关键词间的关系。
  • 时间复杂度
    • 实现依赖于具体的 Python 解释器,通常非常接近于 O(m)。

4. KMP 算法

  • 优点
    • 解决单模式匹配问题非常高效,特别是长模式字符串。
    • 预处理阶段计算部分匹配表,避免不必要的比较。
  • 缺点
    • 预处理有一定的开销,尤其是对于较长的模式字符串。
    • 不适合同时处理多个模式的匹配。
  • 时间复杂度
    • 预处理时间复杂度为 O(n),匹配时间复杂度为 O(m),n 是模式长度,m 是文本长度。

总结

选择哪种算法取决于具体的应用场景。如果需要处理大量的文本数据和多关键词匹配,DFA可能更合适。对于简单的替换任务,Python的 replace 方法可能是最快的。而对于高效的单模式长字符串匹配,KMP是一个好的选择。BSFilter 提供了一种简便的方法来处理较少数量的关键词替换,但可能不适合大规模数据处理。

  • 44
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值