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

         在游戏中经常会有需要玩家输入一些内容的功能,例如聊天,命名等,这款游戏只有在存档时辉用到命名功能,所以这个过滤也只是一个实验性的功能,我们将使用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()){//去掉短字,但这里对中文无效,因为一个中文字长度大概率不为1if (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);elseResult+=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.mzph.cn/news/875473.shtml

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

相关文章

Linux下如何安装配置Zsh Shell

Zsh是一种强大的Shell&#xff0c;它是在Bash原有功能的基础上进行了扩展和改进&#xff0c;提供了更多的特性和功能。在Linux下安装和配置Zsh Shell相对简单&#xff0c;下面将详细介绍安装和配置Zsh Shell的步骤。 第一步&#xff1a;安装Zsh 在Linux上安装Zsh有几种不同的…

贪心算法 | 763.划分字母区间

题目描述 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段&#xff0c;同一字母最多出现在一个片段中。 注意&#xff0c;划分结果需要满足&#xff1a;将所有划分结果按顺序连接&#xff0c;得到的字符串仍然是 s 。 返回一个表示每个字符串片段的长度的列表。…

解决zabbix-server7 中文乱码问题

系统使用centos9 安装中文支持 yum install -y fontconfig langpacks-zh_CN.noarch 检查是否已有中文字体&#xff1a; fc-list :langzh 看到 直接使用GOOGLE的字体 ln -fs /usr/share/fonts/google-noto-cjk/NotoSansCJK-DemiLight.ttc /etc/alternatives/zabbix-web-fo…

bool数组的理解和应用[C++]

文章目录 bool数组的用法bool数组的定义声明bool数组的初始化访问和修改数组元素遍历数组 运用bool数组简单代码 在今天做题中发现了bool类不仅能用于函数类型还能用于数组类型&#xff0c;好奇查了查发现bool还有很多用处&#xff1a;基本变量&#xff0c;在枚举类型中会用到&…

【C语言】结构体详解 -《探索C语言的 “小宇宙” 》

目录 C语言结构体&#xff08;struct&#xff09;详解结构体概览表1. 结构体的基本概念1.1 结构体定义1.2 结构体变量声明 2. 结构体成员的访问2.1 使用点运算符&#xff08;.&#xff09;访问成员输出 2.2 使用箭头运算符&#xff08;->&#xff09;访问成员输出 3. 结构体…

【CSS】1 像素问题

CSS 中的 1 像素问题指的是在⾼分辨率屏幕上显示的 1 像素边框或者细线在实际显示时会⽐ 1 个物理像素更宽或更粗&#xff0c;从⽽导致边框或者细线看上去⽐预期的更粗或者更宽。 造成这个问题的原因是由于⾼分辨率屏幕的像素密度⽐传统的屏幕要⾼&#xff0c;所以在屏幕上显示…

一个C++模板工厂的编译问题的解决。针对第三方库的构造函数以及追加了的对象构造函数。牵扯到重载、特化等

一窥模板的替换和匹配方式&#xff1a;偏特化的参数比泛化版本的还要多&#xff1a;判断是不是std::pair&#xff1c;,&#xff1e;。_stdpair模板参数太多-CSDN博客 简介 在一个项目里&#xff0c;调用了第三封的库&#xff0c;这个库里面有个类用的很多&#xff0c;而且其构…

边界网关IPSEC VPN实验

拓扑&#xff1a; 实验要求&#xff1a;通过IPSEC VPN能够使PC2通过网络访问PC3 将整个路线分为三段 IPSEC配置在FW1和FW2上&#xff0c;在FW1与FW2之间建立隧道&#xff0c;能够传递IKE&#xff08;UDP500&#xff09;和ESP数据包&#xff0c;然后在FW1与PC2之间能够流通数据…

GitHub 详解教程

1. 引言 GitHub 是一个用于版本控制和协作的代码托管平台&#xff0c;基于 Git 构建。它提供了强大的功能&#xff0c;使开发者可以轻松管理代码、追踪问题、进行代码审查和协作开发。 2. Git 与 GitHub 的区别 Git 是一个分布式版本控制系统&#xff0c;用于跟踪文件的更改…

学术研讨 | 基于区块链的隐私计算与数据可信流通研讨会顺利召开

近日&#xff0c;由国家区块链技术创新中心组织的“基于区块链的隐私计算与数据可信流通研讨会”顺利召开&#xff0c;会议邀请了来自全国高校和科研院所的相关领域专家&#xff0c;围绕基于区块链与隐私计算技术的应用需求、研究现状、发展趋势、重点研究方向与研究进展等内容…

Go并发GMP调度模型

如何知道一个对象是分配在栈上还是堆上&#xff1f; Go和C不同&#xff0c;Go的逃逸分析是在编译器完成的&#xff1b;go局部变量会进行逃逸分析。如果变量离开作用域后没有被引用&#xff0c;则优先分配到栈上&#xff0c;否则分配到堆上。那么如何判断是否发生了逃逸呢&#…

数据结构之《队列》

在数据结构之《栈》章节中学习了线性表中除了顺序表和链表外的另一种结构——栈&#xff0c;在本篇中我们将继续学习另一种线性表的结构——队列&#xff0c;在通过本篇的学习后&#xff0c;你将会对栈的结构有充足的了解&#xff0c;在了解完结构后我们还将进行栈的实现。一起…

vue3-02声明响应式状态ref()

一、使用 组合式 API 中&#xff0c;推荐使用 ref() 函数来声明响应式状态&#xff0c;例如 import { ref } from vue const count ref(0)注意点1&#xff1a;若想获取ref定义的参数&#xff0c;必须获取参数的value值&#xff0c; 比如&#xff1a; console.log(count, co…

2024海外电商数据分析之南美篇

南美洲&#xff0c;一片广袤而充满活力的大陆&#xff0c;以其独特的地理位置和丰富的自然资源&#xff0c;孕育了15个国家和4.3亿人口。与北美洲的三国&#xff08;美国、加拿大和墨西哥&#xff09;及中美洲的七国相比&#xff0c;南美洲以其年轻的人口结构和巨大的市场潜力&…

【LLM】-08-搭建问答系统-语言模型,提问范式与 Token

目录 1、语言模型 1.1、训练过程&#xff1a; 1..2、大型语言模型分类&#xff1a; 1.3、指令微调模型训练过程&#xff1a; 2、Tokens 3、Helper function辅助函数 (提问范式) 4、计算token数量 1、语言模型 大语言模型&#xff08;LLM&#xff09;是通过预测下一个词…

一款允许使用Docker部署本地托管的、基于 Web 的 PDF 操作工具

大家好&#xff0c;今天给大家分享的是一个基于Spring Boot开发的开源项目&#xff0c;旨在提供一个功能强大的基于Docker的本地托管PDF操作工具Stirling PDF。 项目介绍 Stirling-PDF是一个全面的PDF工具箱&#xff0c;适用于个人和企业用户&#xff0c;尤其对于那些重视数据…

js 优雅的实现模板方法设计模式

在JavaScript中&#xff0c;优雅地实现模板方法设计模式通常意味着我们要遵循一些最佳实践&#xff0c;如清晰地定义算法的骨架&#xff08;模板方法&#xff09;&#xff0c;并确保子类能够灵活地扩展或修改这些算法中的特定步骤。由于JavaScript是一种动态语言&#xff0c;我…

CasaOS设备使用Docker安装SyncThing文件同步神器并实现远程管理

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

巨量数据表分页问题

1 背景 分页性能问题,之前遇到过这类问题,拿出来再讨论下 2 分析 分页性能问题,特别是在数据量大的情况下,是一个常见的问题。通常,当我们使用类似 LIMIT 和 OFFSET 的SQL语句进行分页时,性能问题尤其明显。这是因为随着 OFFSET 的增加,数据库需要跳过更多的行才能获…

C++树形结构(1 基础)

目录 一.基础&#xff1a; 1.概念&#xff1a; 2.定义&#xff1a; Ⅰ.树的相关基础术语&#xff1a; Ⅱ.树的层次&#xff1a; 3.树的性质&#xff1a; 二.存储思路&#xff1a; 1.结构体存储&#xff1a; 2.数组存储&#xff1a; 三.树的遍历模板&#xff1a; 四.信…