独立游戏《星尘异变》UE5 C++程序开发日志8——实现敏感词过滤功能(AC自动机)

news2024/9/23 7:30:51

         在游戏中经常会有需要玩家输入一些内容的功能,例如聊天,命名等,这款游戏只有在存档时辉用到命名功能,所以这个过滤也只是一个实验性的功能,我们将使用AC自动机来实现,这是在我们把“csdn”这个词设置为屏蔽词后的效果:

目录

一、敏感词词典的处理

二、搭建AC自动机

1.自动机节点的数据机构

2.加载词典

3.建立字典树

4.建立失配指针

三、替换字符串中的敏感词


一、敏感词词典的处理

        我们是从别的地方找的开源词典,所以要做一下筛选,首先我们要去重,然后去除所有的标点符号空格和其他无关字符,然后同时去掉长度为1的字符,因为其会在AC自动机中表现的过于严格

wifstream InputTxt;
wofstream OutputTxt;
//词典的路径,这里是单独开了一个程序,所以和后面项目里相关代码用到的路径不同
InputTxt.open("Dict.txt", ios::out);
//使用宽字符串读入
wstring Word;
map<wstring, bool>Words;	
while (getline(InputTxt,Word))
{
    //去重
	if (Words.find(Word) == Words.end())
	{
        //去掉短字,但这里对中文无效,因为一个中文字长度大概率不为1
		if (Word.size() == 1)
			continue;
		for (auto& It1 : Word)
		{
            //统一成小写
			if (iswupper(It1))
			{
				It1 = towlower(It1);
			}
            //去除字符				
			if (iswpunct(It1)||iswblank(It1)||iswspace(It1))
			{
				Word.erase(It1);
				It1--;
			}				
		}
        //记录这个词处理完毕
		Words[Word] = true;
	}
}
InputTxt.close();
OutputTxt.open("Dict.txt", ios::out);
//将处理完的词重新写入词典
for (auto& It: Words)
{
	OutputTxt << It.first << endl;
}

二、搭建AC自动机

        AC自动机就是在字典树的基础上加入了类似于KMP的失配指针,当匹配串在树上失配时,会回溯到某个上一层的节点,该节点的所有父节点即前缀,和失配节点的所有父节点的后缀,形成最大匹配,使多模匹配的效率达到近似O(匹配串长度)

1.自动机节点的数据机构

        因为我们要将匹配到的敏感词替换成'*',所以相比于一般的自动机节点,要在每个词的末尾记录这个词的长度,同时因为不止26个字母,所以也用红黑树替代了数组

class FSensitiveWordFilterStruct
{
public:
	FSensitiveWordFilterStruct()=default;

	explicit FSensitiveWordFilterStruct(const wchar_t&InputCharacter):Character(InputCharacter){};
	
	//字符
	wchar_t Character{'#'};

	//匹配的字符串的长度
	int Length{0};

	//子节点
	TMap<wchar_t,std::shared_ptr<FSensitiveWordFilterStruct>>ChildNode;

	//失配指针
	FSensitiveWordFilterStruct* FailPointer{this};
};

        然后我们在游戏实例中声明自动机的根节点:

//屏蔽词过滤器树根
std::shared_ptr <FSensitiveWordFilterStruct>SensitiveWordFilterRoot;

        在游戏启动时初始化AC自动机,用到的函数后面一个一个讲:

UAstromutateGameInstance::UAstromutateGameInstance()
{
	//加载词典
	LoadTXTFile("/Movies/Dict.txt");
    //实例化自动机根节点
	SensitiveWordFilterRoot=std::make_shared<FSensitiveWordFilterStruct>(FSensitiveWordFilterStruct());
    //将词典中的词添加到树上
	for(const auto&It:*SensitiveWords)
	{
		AddWordToSensitiveWordTree(It);
	}
    //建立失配指针
	InitializeSensitiveWordTree();
}

2.加载词典

        这里我们把词典作为txt文件放在Movies文件夹下,因为该文件夹中的所有文件都会被原封不动的打包,我们将所有敏感词存到一个TArray中

//声明敏感词词典
TSharedPtr<TArray<FString>> SensitiveWords;
auto UAstromutateGameInstance::LoadTXTFile(const FString& Path)->void
{
    //获取词典路径
	FString Temp{FPaths::ProjectContentDir()+Path};
    //实例化词典数组
	SensitiveWords=MakeShared<TArray<FString>>(TArray<FString>());
    //加载所有词
	FFileHelper::LoadFileToStringArray(*SensitiveWords,*Temp);
	UE_LOG(LogTemp,Warning,TEXT("SensitiveWords loade %d Words"),SensitiveWords->Num());
}

3.建立字典树

        从根节点开始,遍历模式串,如果当前点没有当前字符对应的子节点,就创建之,然后无论有无都移动到该子节点

auto UAstromutateGameInstance::AddWordToSensitiveWordTree(const FString& InputString) const->void
{
    //获取根节点
	FSensitiveWordFilterStruct* Temp=SensitiveWordFilterRoot.get();
    //遍历模式串中的每一个字符
	for(const auto&It:InputString)
	{
		wchar_t CurrentChar{It};
        //如果当前点没有对应的子节点,就添加之
		if(!Temp->ChildNode.Contains(CurrentChar))
		{
			Temp->ChildNode.Add(CurrentChar,std::make_shared<FSensitiveWordFilterStruct>(FSensitiveWordFilterStruct(CurrentChar)));
		}
		Temp=Temp->ChildNode[CurrentChar].get();
	}
    //将词的长度记录在词尾
	Temp->Length=InputString.Len();
}

4.建立失配指针

        因为失配指针指向的节点一定在当前点的上层,所以我们进行bfs,首先将根节点的所有直连的子节点的失配指针指向根节点,因为这些点的上层节点只有根节点。然后对于一个失配点,如果其父节点的失配指针指向的点的子节点中有和该失配点相同的点,则失配点的失配指针指向该点,否则指向根节点

auto UAstromutateGameInstance::InitializeSensitiveWordTree() const -> void
{
    //bfs队列
	std::queue<std::shared_ptr<FSensitiveWordFilterStruct>>Queue;
    //将深度为1的点的失配指针指向根节点
	for(auto&It:SensitiveWordFilterRoot->ChildNode)
	{
		It.Value->FailPointer=SensitiveWordFilterRoot.get();
		Queue.push(std::make_shared<FSensitiveWordFilterStruct>(*It.Value));
	}
	while(!Queue.empty())
	{
		std::shared_ptr<FSensitiveWordFilterStruct> CurrentNode=Queue.front();
		Queue.pop();
        //遍历所有子节点
		for(auto&It:CurrentNode->ChildNode)
		{
            //父节点的失配指针指向的节点是否含有匹配的子节点
			if(!CurrentNode->FailPointer->ChildNode.Contains(It.Key))
			{
				It.Value->FailPointer=SensitiveWordFilterRoot.get();
			}
			else
			{
				It.Value->FailPointer=CurrentNode->FailPointer->ChildNode[It.Key].get();
			}
			Queue.push(std::make_shared<FSensitiveWordFilterStruct>(*It.Value));
		}
	}
}

三、替换字符串中的敏感词

        首先我们将玩家输入的字符串使用字典中字符串同样的方法进行处理,去除符号和空格,全部转为小写,然后遍历其每一个字符,不匹配就按失配指针移动,匹配就检查是否是词尾,如果是的话根据记录的词的长度算出这个词的区间,将这个居间内的所有字符替换成'*',该操作不会影响到后面的匹配,最后将字符串还原成原来有符号和空格的格式并返回

auto UAstromutateGameInstance::ReplaceSensitiveWords(const FString& RawString)->FString
{
	FString Result{""};
    //对玩家输入的字符串进行处理
	for(const auto&It:RawString)
	{
		if(iswpunct(It)||iswblank(It)||iswspace(It))
			continue;
		if(isupper(It))
			Result+=towlower(It);
		else
			Result+=It;
	}
	FSensitiveWordFilterStruct* Temp{SensitiveWordFilterRoot.get()};
    //遍历匹配串的每一个字符
	for(int i=0;i<Result.Len();i++)
	{
		wchar_t CurrentChar{Result[i]};
        //如果失配就一直回溯,直到根节点
		while(!Temp->ChildNode.Contains(CurrentChar)&&Temp!=SensitiveWordFilterRoot.get())
		{
			Temp=Temp->FailPointer;
		}
        //仍然适配就结束这个字符的搜索
		if(!Temp->ChildNode.Contains(CurrentChar))
		{
			Temp=SensitiveWordFilterRoot.get();
			continue;
		}
        //移动到匹配的节点
		Temp=Temp->ChildNode[CurrentChar].get();
		FSensitiveWordFilterStruct* Temp2{Temp};
        //遍历匹配到的所有词
		while(Temp2!=SensitiveWordFilterRoot.get())
		{
			if(Temp2->Length)
			{
                //根据长度算出该词其实位置
				for(int j=i-Temp2->Length+1;j<=i;j++)
				{
					Result[j]='*';
				}
			}
			Temp2=Temp2->FailPointer;
		}
	}
    //将处理完的字符串还原成输入的格式
	FString TrueResult{RawString};
	int CurrentIndex{0};
	for(auto&It:TrueResult)
	{
		if(iswpunct(It)||iswblank(It)||iswspace(It))
			continue;
		if(iswupper(It)&&iswlower(Result[CurrentIndex]))
		{
			continue;
		}
		It=Result[CurrentIndex++];
	}
	return TrueResult;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nginx代理缓存配置-Linux(CentOS)

代理缓存 1. 编写主配置文件2. 编辑虚拟机配置文件3. 重启nginx服务 nginx代理服务配置&#xff0c;基于http协议 开启代理缓存的前提是已经开启了代理服务&#xff0c;请确保已经开启代理服务 1. 编写主配置文件 主配置文件通常在/etc/nginx/nginx.conf&#xff0c;在该文件…

【Vue3】watch 监视 ref 定义的数据

【Vue3】watch 监视 ref 定义的数据 背景简介开发环境开发步骤及源码参数说明 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努…

江科大/江协科技 STM32学习笔记P6

文章目录 LED闪烁&LE流水&蜂鸣器一、操作STM32的GPIO步骤二、RCC库函数什么是AHB与APB&#xff1f; 三、GPIO库函数GPIO初始化选择IO接口工作方式 四、四种方法实现LED闪灯 LED闪烁&LE流水&蜂鸣器 一、操作STM32的GPIO步骤 1、使用RCC开启GPIO的时钟 2、使用…

【C语言报错已解决】Use of Uninitialized Variable

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引言&#xff1a; 在编程中&#xff0c;未初始化的变量是一个常见的问题&#xff0c;它可能导致程序的行为变得不可预测。未初…

CAD图块的对齐方法的使用技巧

对齐功能配合鼠标技巧才能正常使用&#xff0c;不然对齐的图形胡乱翻转。

Cxx Primer-chap7

类的基本思想是数据抽象和封装&#xff0c;前者强调interface和implement分离&#xff0c;后者在此基础上&#xff0c;强调访问控制符&#xff08;存疑&#xff09;。同时类的实现者和使用者考虑的角度不同&#xff0c;前者考虑实现效率&#xff0c;后者仅需关注功能即可&#…

PSINS工具箱函数介绍——insplot

insplot是一个绘图命令,用于将avp数据绘制出来 本文所述的代码需要基于PSINS工具箱,工具箱的讲解: PSINS初学指导基于PSINS的相关程序设计(付费专题)使用方法 此函数使用起来也很简单,直接后面加avp即可,如: insplot(avp);其中,avp为: 每行表示一个时间1~3列为姿态…

基于VUE的软件项目开发管理系统/项目管理系统/软件开发过程管理系统的设计与实现

摘 要 在Internet高速发展的今天&#xff0c;我们生活的各个领域都涉及到计算机的应用&#xff0c;其中包括软件项目开发管理系统的网络应用&#xff0c;在外国软件项目开发管理系统已经是很普遍的方式&#xff0c;不过国内的软件项目开发管理可能还处于起步阶段。软件项目开发…

elasticsearch 解决全模糊匹配最佳实践

事件背景&#xff1a; 某 CRM 系统&#xff0c;定义了如下两个表&#xff1a; 客户表 t_custom 字段名 类型 描述 idlong自增主键phonestring客户手机......... 客户产品关系表 t_custom_product 字段名 类型 描述 idlong自增主键custom_idlong客户idproduct_idlong产品…

第一百七十七节 Java IO教程 - Java路径操作

Java IO教程 - Java路径操作 比较路径 我们可以基于它们的文本表示来比较两个Path对象的相等性。 equals()方法通过比较它们的字符串形式来测试两个Path对象的相等性。 等式测试是否区分大小取决于文件系统。 以下代码显示如何比较Windows路径: import java.nio.file.Pat…

【Unity实战】yield return null还是WaitForEndOfFrame

当在Unity中编写协程&#xff08;尤其是协程套无限循环&#xff09;时&#xff0c;常常会用到yield关键字来控制协程的执行流程避免程序假死。以下是常见做法&#xff1a; yield return null 当使用yield return null时&#xff0c;协程会在下一帧继续执行。这意味着协程将暂…

vscode-CodeGeeX AI在vscode运用

1.CodeGeeX 代码自动生成和补全&#xff0c;代码翻译&#xff0c;自动添加注释&#xff0c;智能问答等 2.vscode中使用 3.官方网址 https://codegeex.cn/downloadGuide#vscode 进行登录注册使用&#xff0c;个人免费

机器学习 | 回归算法原理——多项式回归

Hi&#xff0c;大家好&#xff0c;我是半亩花海。接着上次的最速下降法&#xff08;梯度下降法&#xff09;继续更新《白话机器学习的数学》这本书的学习笔记&#xff0c;在此分享多项式回归这一回归算法原理。本章的回归算法原理基于《基于广告费预测点击量》项目&#xff0c;…

idea一键为实体类赋值

file -> settings -> plugins -> marketplace 把这个插件装上 找个实体&#xff0c;选中&#xff0c;altenter进入edit界面 我是选择只保留右边这种生成方法&#xff0c;然后选择ok 返回到那个实体&#xff0c;选择&#xff0c;altenter generate生成

前端开发知识(一)-html

1.前端开发需掌握的内容&#xff1a; 2.前端开发的三剑客&#xff1a;html、css、javascript Vue可以简化JavaScpript流程。 Element&#xff08;饿了么开发的&#xff09; &#xff1a;前端组件库。 Ngix&#xff1a;前端服务器。 3.前端开发工具&#xff1a;vscode 1)按…

PCL-基于超体聚类的LCCP点云分割

目录 一、LCCP方法二、代码实现三、实验结果四、总结五、相关链接 一、LCCP方法 LCCP指的是Local Convexity-Constrained Patch&#xff0c;即局部凸约束补丁的意思。LCCP方法的基本思想是在图像中找到局部区域内的凸结构&#xff0c;并将这些结构用于分割图像或提取特征。这种…

SVN文件夹没有图标(绿钩子和红感叹号)

3分钟教会你解决SVN文件夹没有绿勾和红色感叹号的问题_svn文件被改动过不显示红色-CSDN博客https://blog.csdn.net/weixin_43382915/article/details/124251563 关于SVN状态图标不显示的解决办法(史上最全) - 简书 (jianshu.com)https://www.jianshu.com/p/92e8e1f345c0

墨烯的C语言技术栈-C语言基础-018

char c; //1byte字节 8bit比特位 int main() { int a 10; //向内存申请四个字节,存储10 &a; //取地址操作符 return 0; } 每个字节都有地址 而a的地址就是它第一个字节的地址 要先开始调试才可以查看监控和查看内存 左边是地址 中间是内存中的数据 最后面的是…

【数据结构面试有那些常见问题?】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

Layui Selcet选择框动态选择问题

前言 时隔多日我也是重新回归写作&#xff0c;高考已经完毕&#xff0c;我将继续我的文章创作&#xff0c;今天我将分享的是我在开发我自己的一个新项目所遇到的问题&#xff0c;这里预告一下我的新项目: VitaApi管理系统 这个系统可以看作是萌新源api管理系统的延续&#xff…