DFA算法在敏感词过滤的应用

相信大家对于游戏里聊天框的以下内容已经不陌生了

  • "我***"
  • “你真牛*”
  • “你是不是傻*”

一个垃圾的游戏环境是非常影响玩游戏的心情的,看到这些,就知道游戏已经帮我们屏蔽掉了那些屏蔽字了,对于玩游戏而言,心里会好受很多。敏感词识别对于游戏的重要性不言而喻。当然,除了游戏,也有很多业务场景可能需要敏感词检测,如果你接到这样一个需求的时候,你会怎么做?*

一、原生API

作为Java程序员,我的第一反应,一定是使用jdk原生的String类提供的contain或replace方法来进行包含判断或字符替换,这是最简单直接的方式。那我们就来看看String的实现方式:

contains

String在java中以char数组形式存储,而String.contains的实现,实际上是对数组的遍历查找匹配

 
// 最终调用方法
static int indexOf(char[] source, int sourceOffset, int sourceCount,
char[] target, int targetOffset, int targetCount,
int fromIndex) {
// ...
}

replace

String.replace有4个接口,实现为正则匹配替换或直接遍历替换

 
public String replace(char oldChar, char newChar) {
// 直接进行字符串遍历,替换第一个匹配的字符串
}
public String replace(CharSequence target, CharSequence replacement) {
// 创建Pattern,使用LITERAL模式进行正则匹配替换replaceAll
// 当设置LITERAL标志时,输入字符串中的所有字符都被视为普通字符。
// 这意味着正则表达式的特殊字符,如点号(.)、星号(*)、加号(+)等,都将失去它们在正则表达式中的特殊意义,被直接视为普通字符。
}
public String replaceAll(String regex, String replacement) {
// 创建Pattern,使用正则表达式模式匹配替换replaceAll
}
public String replaceFirst(String regex, String replacement) {
// 创建Pattern,使用正则表达式模式匹配替换replaceFirst,仅替换第一个匹配的字符串
}

通过jdk提供的String源码我们可以得到以下结果:

  • 使用contains方法进行包含判断,它的底层实现原理其实就是通过遍历目标字符串的字符数组进行挨个匹配;少量敏感词检测的时候是可行的,但如果目标字符串很大,并且要匹配的敏感词足够多的时候,它的遍历匹配效率是很低的。
  • replace则分两种实现,其中一种是类似contains方法,也是进行对目标字符串进行字符数组的遍历替换。
  • replace的另一种实现,是通过java的正则表达式去做匹配,正则匹配相比于遍历匹配,效率上不会有明显提升,但对于复杂模式的解析匹配会有比较明显的优势

其他语言的字符串操作API大同小异,具体看源码的实现方式

DFA

二、正则表达式

另外一种我们能想到的方式就是进行正则表达式的匹配了。前面提到,在java中如果使用String的api,它有部分接口就是使用正则表达式来实现的。
使用正则表达式有一定优势,也有一定缺陷。这就不得不提正则表达式的实现原理:FA(Finite Automaton:有限自动机)

DFA与NFA

FA又分为DFA和NFA,我们以正则ab|ac举例

  • NFA(Nondeterministic finite automaton:非确定性有限状态自动机)
    在NFA中表达式会构建为以下结构

    nfa

    • 非确定性:对于给定的输入符号,NFA可以从一个状态转移到多个状态。这意味着存在多种可能的状态转换路径,NFA在任何时间点都可以处于多个状态。
    • 回溯:由于NFA在处理输入时可以选择多条路径,因此可能需要回溯。当某条路径未能达到接受状态时,NFA会返回并尝试其他可能的路径。
    • 构造:NFA相对容易构造,特别是对于复杂的或包含多种可能的语言(例如正则表达式)。
    • 运行效率:由于其非确定性特性,NFA在运行时可能需要更多的计算资源,特别是在处理长输入字符串时。
  • DFA(Deterministic finite automaton:确定性有限自动机)
    在DFA中表达式会构建为以下结构

    dfa

    • 确定性:对于给定的输入符号,DFA从一个状态转移到另一个唯一确定的状态。这意味着DFA在任何时间点只能处于一个状态。
    • 无回溯:由于每个输入符号只对应一个状态转换,DFA在处理输入时不需要回溯。
    • 构造:相对于NFA,DFA可能更难直接构造,特别是对于复杂的语言,但它可以通过从NFA转换得到。
    • 运行效率:DFA在运行时通常更高效,因为它在处理输入时不需要考虑多种可能的状态路径。

理论上,NFA和DFA等效,它们都可以识别相同的语言类型。但在实际应用中,它们各有优势:NFA更适合于表示和构造复杂模式,而DFA在执行时更高效。

如果以上描述不能理解,这里其实可以做个不是特别恰当的比喻:广度优先搜索BFS和深度优先搜索DFS。

  • NFA可以转移到多个不同的状态。这就像是在图中有多条边从一个节点出发一样。如果将NFA的操作类比为一种搜索算法,它更接近于广度优先搜索(BFS)。在匹配过程中,NFA可以同时探索多条路径(或状态转换),就像BFS在搜索时会先访问所有邻接节点。然而,NFA通常不会存储所有可能的状态转换路径,而是在运行时动态生成它们。
  • DFA只能转移到一个唯一确定的状态。这就像是图中的每个节点仅有一条出边一样。尽管DFA在每一步只选择一条路径,但将其类比为深度优先搜索(DFS)并不准确。DFS是一种搜索算法,用于探索所有可能的路径直到它达到目标或结束条件。DFA则是一种确定性的状态机,它不需要“搜索”;它只是在状态之间单一确定地转换。

在正则表达式的实现中,有的基于DFA,有的基于NFA;尽管DFA的搜索路径比NFA短,但实际场景中,NFA更适合复杂模式的正则搜索。因此大多数正则实现还是基于NFA。
java中的正则表达式是基于NFA的实现

使用局限

当然了,正则表达式的实现到底是NFA还是DFA,并不是今天讨论的重点。

  • 资源消耗
    无论是NFA还是DFA,它们在匹配之前,都会先构造基于图的数据结构,因此,使用正则表达式进行敏感词匹配,一定逃不开构建这个数据结构的性能消耗和内存占用。

  • 回溯陷阱
    在使用正则表达式进行敏感词匹配时,如果是基于NFA实现的正则算法,则很有可能出现回溯陷阱。上面提到NFA在匹配时是会进行回溯的,因为它不知道后面有没有可能还会匹配成功,但是DFA从一开始就是确定的有限自动机,DFA是知道所有的匹配成功的情况,所以在使用NFA时,如果表达式写的不注意,很可能出现大量回溯。这样大量的回溯很可能造成在进行正则表达式的匹配时,CPU会飚高的情况。

解决方案

  • 资源消耗很好解决:对于服务器来说,只需要在启动服务器之前,对配置好的敏感词做好正则表达式的初始化即可,即便是需要灵活配置,也可以通过动态加载再进行内存替换来解决。
  • 要解决NFA回溯问题,也有很多方式:比如表达式中尽可能提取公共部分、适当拆分、不要量词嵌套、使用非贪婪模式等多种优化手段。这些优化手段都是从表达式本身入手,这意味着所有人在编写敏感词匹配的正则表达式时,都需要时刻注意回溯陷阱,并且对每一个表达式都要做好性能测试。

如果注意好以上点,使用正则表达式进行敏感词匹配在业务场景中也是可行的。甚至于对于复杂语义的敏感词配置来说,只有正则表达式能实现需求

DFA

三、DFA

上文中其实已经提到,相比于NFA的不确定性,DFA是具有确定性的有限自动机。它之所以具有确定性,从结构上来说,它的每一个状态都只对应一个状态转换,因此它也无需进行回溯,因此它的匹配性能也比NFA要高。

当然了DFA的缺点就是它很难处理复杂的语义。但是对于敏感词来说,为了效率,我们其实可以把那些复杂的语义简单化;另外一个和正则匹配一样的点,就是构建DFA有向图所带来的开销和内存占用,这一点也能通过服务器启动加载和动态内存替换解决。
所以其实一旦我们解决掉DFA的痛点,便能扬长避短,既享受DFA高效率,又使其能胜任业务场景。

不过需要注意的是,这里我们就不再使用正则表达式进行敏感词匹配了,而是直接实现一套基于DFA的敏感词匹配算法。你可能会有疑问,既然正则表达式也可以使用DFA,那我们为什么不使用基于DFA的正则表达式呢?
这也很好理解,使用正则表达式,我们只能把每一条表达式单独构建成一个个图的数据结构,它的粒度只能到每一条表达式。而我们自己实现DFA,则可以把所有的敏感词全部构建成同一个大的DFA图,它维度则是全服所有敏感词。这样既可以省去一定的内存空间,也可以减少匹配次数。

使用原理

使用DFA来实现敏感词匹配的原理,其实是在初始化时,把所有的敏感词拆成一个个的字,然后组织成一个很大的有向图的结构。其实也是用到编程思想中的空间换时间思想。比如有以下敏感词:

  • 打死你
  • 打死他
  • 打他
  • 揍他
    经过DFA的树组织,最终会得到以下结构:

DFA

其中,绿色的Entry代表入口节点,而蓝色的代表中止节点,当玩家输入一句话时,会通过遍历玩家发的每一个字,再去这个DFA有向图中去匹配
如果玩家发送“我要揍他”,那么“揍他”两个字就能通过“Entry->揍->他”这样的路径匹配上
如果玩家发送“我要揍你”,那么“”字能通过“Entry->揍”这样的路径匹配上,但因为“”不是中止节点,所以这句话不能算敏感词

逻辑实现

1. DFA初始化

这一步作用是构建DFA图

 
public boolean initialize(String[] keyWords) {
clear();
// 构造DFA
for (int s = 0; s < keyWords.length; s++) {
String _keyword = keyWords[s];
if (_keyword == null || (_keyword = _keyword.trim()).length() == 0) {
continue;
}
char[] patternTextArray = _keyword.toCharArray();
DFANode currentDFANode = dfaEntrance;
for (int i = 0; i < patternTextArray.length; i++) {
final char _c = patternTextArray[i];
// 逐点加入DFA
final Character _lc = toLowerCaseWithoutConfict(_c);
DFANode _next = currentDFANode.dfaTransition.get(_lc);
if (_next == null) {
_next = new DFANode();
currentDFANode.dfaTransition.put(_lc, _next);
}
currentDFANode = _next;
}
if (currentDFANode != dfaEntrance) {
currentDFANode.isTerminal = true;
}
}
buildFailNode();
return true;
}
2. DFA匹配检测

匹配字检测,一旦检测到中止节点,则返回true

 
public boolean contain(final String inputMsg) {
char[] input = inputMsg.toCharArray();
DFANode currentDFANode = dfaEntrance;
DFANode _next = null;
for (int i = 0; i < input.length; i++) {
final Character _lc = this.toLowerCaseWithoutConfict(input[i]);
if (!isIgnore(_lc)) {
_next = currentDFANode.dfaTransition.get(_lc);
while (_next == null && currentDFANode != dfaEntrance) {
currentDFANode = currentDFANode.failNode;
_next = currentDFANode.dfaTransition.get(_lc);
}
}
if (_next != null) {
// 找到状态转移,可继续
currentDFANode = _next;
}
// 看看当前状态可退出否
if (currentDFANode.isTerminal) {
// 可退出,记录,可以替换到这里
return true;
}
}
return false;
}
3. DFA字符替换

根据节点搜索匹配,走到中止节点则回溯依次替换

 
public String filt(String s) {
char[] input = s.toCharArray();
char[] result = s.toCharArray();
boolean _filted = false;
DFANode currentDFANode = dfaEntrance;
DFANode _next = null;
int replaceFrom = 0;
int ignoreLength = 0;
boolean endIgnore = false;
for (int i = 0; i < input.length; i++) {
final Character _lc = this.toLowerCaseWithoutConfict(input[i]);
_next = currentDFANode.dfaTransition.get(_lc);
while (_next == null && !isIgnore(_lc) && currentDFANode != dfaEntrance) {
currentDFANode = currentDFANode.failNode;
_next = currentDFANode.dfaTransition.get(_lc);
}
if (_next != null) {
// 找到状态转移,可继续
currentDFANode = _next;
if(currentDFANode.level == 1) {
ignoreLength = 0;
}
}
if (!endIgnore && currentDFANode != dfaEntrance && isIgnore(_lc)) {
ignoreLength++;
}
// 看看当前状态可退出否
if (currentDFANode.isTerminal) {
endIgnore = true;
// 可退出,记录,可以替换到这里
int j = i - (currentDFANode.level - 1) - ignoreLength;
if (j < replaceFrom) {
j = replaceFrom;
}
replaceFrom = i + 1;
for (; j <= i; j++) {
result[j] = this.subChar;
_filted = true;
}
currentDFANode = dfaEntrance;
ignoreLength = 0;
endIgnore = false;
}
}
if (_filted) {
return String.valueOf(result);
} else {
return s;
}
}

DFA

怎么选择

  • 使用原生api进行遍历匹配在数据达到一定量级时一定会有性能问题的,一般不采用这种方式。
  • 使用正则表达式优势在于灵活配置,但需注意回溯陷阱问题;正则表达式预编译会占用一定内存空间。
  • 使用DFA在简单确定的语义中优势明显,但难以处理复杂语义;DFA初始化构建有向图会占用内存空间,一般敏感词数量是会达到二三十万的量级的,有向图大小会达到M级别,好在现在内存并不值钱,空间换时间是一个可取的办法。

DFA应用场景

  • 编译器设计:DFA常用于词法分析器,用于识别关键字、运算符、标识符等
  • 字符串搜索和匹配:常用于字符串匹配算法,比如文本编辑器,敏感词等
  • 网络安全检测:DFA用快速识别恶意流量模式
  • 自然语言(NPL)处理:用于文本分析和处理任务
  • 正则表达式引擎:虽然很多正则表达式引擎基于非确定性有限自动机(NFA),但也有一些引擎或工具使用DFA来提高匹配效率,特别是在匹配简单模式时
  • 更多...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/296214.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Raft no-op日志

no-op日志 为什么 Leader 不能提交之前任期的日志&#xff0c;只能通过提交自己任期的日志&#xff0c;从而间接提交之前任期的日志。 先按错误的情况&#xff0c;也就是 Leader 可以提交之前任期的日志。那么上述的流程&#xff1a; s1 是任期 2 的 Leader(仔细看&#xff0…

【Tomcat】在一台计算机上运行两个Tomcat服务

首先把Tomcat整个文件复制一份放在其他文件夹路径中 1.修改环境变量 添加环境变量在系统变量里面 “CATALINA_HOME” 指向一个路径 “CATALINA_HOME1” 指另一个Tomcat路径 2.修改startup里面的环境变量&#xff0c;全部修改 修改"D:\Java\apache-tomcat-8.5.45\bin&…

Raft Lab3A

Lab3 需要在 Raft 层上实现一个 fault-tolerant key-value service&#xff0c;满足强一致性&#xff0c;也就是线性一致性 (Linearizable Consistency)。线性一致性保证整个系统看起来好像只有一个副本&#xff0c;其中所有的操作都是原子性的。简单地说&#xff0c;线性一致性…

Unity之预制体与变体

PS:不用说了&#xff0c;我在写博客就是在摸鱼 一、预制体 不知道大家小时候有没有看过火影&#xff0c;记得剧情最开始的时候水木哄骗鸣人去偷封印之书&#xff0c;反而让鸣人学会了多重影分身之术&#xff1a; 好了&#xff0c;小编绞尽脑子终于想好怎么向大家介绍预制体了&a…

高级分布式系统-第3讲 网络与网络互联

万维网的诞生 1957年10月4日&#xff0c; 苏联发射了人类第一颗人造卫星—斯普特尼克一号 美国政府震惊不已。 他们认为&#xff0c; 在日趋激烈的冷战对抗中&#xff0c; 自己已经全面落后于苏联。 为了扭转这一局面&#xff0c; 美国国防部很快于1958 年 2 月组建了一个神秘…

红队专题-Web安全/渗透测试-文件上传/下载/包含

文件上传/下载/包含 招募六边形战士队员利用目录穿越反弹SHELL实战测试2.2 提交报文修改检测3.2 文件内容检测绕过完整文件结构 检测 第四章&#xff1a;解析漏洞第一节 常见解析漏洞iis/nginx php fastcgi 取值错误 解析漏洞 &#xff08;配置错误&#xff09;nginx 文件名逻…

2024年,Web3技术的“iPhone时刻”会出现吗?

出品&#xff5c;欧科云链研究院 关于2024年的Web3市场&#xff0c;大多数人目前是持乐观态度的。随着宏观政策稳定&#xff0c;美国和香港地区不断释放加密资产现货ETF的利好&#xff0c;叠加铭文热潮及减半周期临近&#xff0c;市场对Web3及加密资产的兴趣正持续上涨。年后的…

项目从npm迁移到pnpm

场景如下&#xff1a;在安装Vue3时默认为使用Npm安装&#xff0c;如图所示&#xff1a; 安装完后项目就包含了基于NPM的node_modules、package.json&#xff0c;以及package-lock.json 如果想使用pnpm去安装依赖项的话&#xff0c;可以通过如下几个步骤实现&#xff1a; ①删…

UICollection Compositional Layout全详解

本文字数&#xff1a;8325字 预计阅读时间&#xff1a;45分钟 01 Collection View Layout全详解 UICollectionView在iOS中是构建复杂布局的强大工具。iOS13中引入的 UICollectionViewCompositionalLayout为创建自定义布局提供了全新的可能性。本文将深入探讨Compositional Lay…

复试 || 就业day04(2024.01.05)项目一

文章目录 前言线性回归房价预测加载数据数据查看数据拆分数据建模模型的验证、应用模型的评估 总结 前言 &#x1f4ab;你好&#xff0c;我是辰chen&#xff0c;本文旨在准备考研复试或就业 &#x1f4ab;本文内容来自某机构网课&#xff0c;是我为复试准备的第一个项目 &#…

JAVA中小型医院信息管理系统源码 医院系统源码

开发框架&#xff1a;SpringBootJpathymeleaf 搭建环境&#xff1a;jdk1.8idea/eclipsemaven3mysql5.6 基于SpringBoot的中小型医院信息管理系统&#xff0c;做的比较粗糙&#xff0c;但也实现了部分核心功能。 就诊卡提供了手动和读卡两种方式录入&#xff0c;其中IC读卡器使用…

Python(30):非对称加密算法RSA的使用(openssl生成RSA公私钥对)

Python(30)&#xff1a;非对称加密算法RSA的使用(openssl生成RSA公私钥对) 1、openssl生成RSA公私钥对 1.1、生成RSA公私钥对命令 [rootloaclhost ~]# openssl OpenSSL> genrsa -out rsa_private_key.pem 1024 Generating RSA private key, 1024 bit long modulus .. ...…

PN协议下,上位机如何通过RJ45口远程控制PLC?

在实际系统中&#xff0c;车间里分布多台PLC&#xff0c;需要用上位机软件集中控制。通常所有设备距离在几十米到上百米不等。在有通讯需求的时候&#xff0c;如果布线的话&#xff0c;工程量较大且不美观&#xff0c;这种情况下比较适合采用无线通信方式。 本方案以组态王和2…

【AI视野·今日Robot 机器人论文速览 第六十八期】Tue, 2 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Tue, 2 Jan 2024 Totally 12 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Edge Computing based Human-Robot Cognitive Fusion: A Medical Case Study in the Autism Spectrum Disorder Therapy Author…

【Nginx】#安装配置及应用场景使用说明

文章目录 常见公网DNS服务1.Nginx的安装版本区别解压文件1.1编译安装1.2 启动安装1.3 安装成系统服务 2.运行原理3.Nginx配置及应用场景3.1最小配置worker_processesworker_connectionsinclude mime.types;default_type application/octet-stream;sendfifile on;keepalive_time…

云原生战专题 | 深入浅出分析云原生微服务的技术结构和架构设计

深入浅出分析云原生微服务的技术结构和架构设计 云原生容器技术背景容器编排Kubernetes控制平面的四大组件Kubernetes在容器编排中的设计要点 云原生微服务典型架构第一代微服务架构第二代微服务架构第三代微服务架构第四代微服务架构 未来的云原生架构 — Serverless 云原生容…

gitlab 配置 二

一 环境说明 群晖Nas DS418DELL XPS serverGitlab ce 二 需要实现的功能 外网可以访问&#xff0c;gitlab使用https的方式访问。wiki issue 等都可以上传图片和附件。 三 操作步骤 因为群晖上有证书&#xff0c;并且由群晖做转发功能。因此证书上&#xff0c;采用群晖的证书…

Linux LVM逻辑卷

一、LVM的定义 LVM 是 Logical Volume Manager 的简称&#xff0c;译为中文就是逻辑卷管理。它是 Linux 下对硬盘分区的一种管理机制。LVM 适合于管理大存储设备&#xff0c;并允许用户动态调整文件系统的大小。此外&#xff0c;LVM 的快照功能可以帮助我们快速备份数据。LVM 为…

聚观早报 |谷歌起草“机器人宪法”;极越与福耀集团达成合作

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 1月6日消息 谷歌起草“机器人宪法” 极越与福耀集团达成合作 三星电子宣布与现代汽车集团合作 OpenAI下周将推GP…

【elfboard linux 开发板】9. 虚拟机扩容和内核编译

1. 虚拟机扩容 需要将虚拟机的快照全都删除&#xff0c;并且将运行的系统关机点击扩展&#xff0c;改为需要的磁盘大小安装gparted工具 sudo apt-get install gparted 如果报错&#xff0c;则按照出错内容修改&#xff0c;一般是出现下载错误&#xff0c;可以使用下列命令&…