当前位置: 首页 > news >正文

独立游戏《星尘异变》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.lryc.cn/news/408547.html

相关文章:

  • 使用 Swagger 在 Golang 中进行 API 文档生成
  • Pip换源实战指南:加速你的Python开发
  • 【数据结构】常用数据结构的介绍:理解与应用
  • 【优秀python系统毕设】基于Python flask的气象数据可视化系统设计与实现,有LSTM算法预测气温
  • 【康复学习--LeetCode每日一题】2951. 找出峰值
  • PYTHON学习笔记(八、字符串及的使用)
  • 文件共享功能无法使用提示错误代码0x80004005【笔记】
  • FTP(File Transfer Protocal,文件传输协议)
  • DevEco Studio中使用Qt,编写HarmonyOS程序
  • 基于单文档的MFC图像增强
  • 云计算实训13——DNS域名解析、ntp时间服务器配置、主从DNS配置、多区域DNS搭建
  • 【C#】Visual Studio2022打包依赖第三方库的winForm程序为exe
  • 《算法笔记》总结No.11——数字处理(上)欧拉筛选
  • DP学习——享元模式
  • 无人机10公里WiFi图传摄像模组,飞睿智能超清远距离无线监控,智能安防新潮流
  • SAP S/4HANA Cloud Public Edition
  • LabVIEW汽车动态信号模拟系统
  • chrome 插件:content-script 部分逻辑在页面无法生效,可考虑插入 script 到页面上
  • 【前端 10】初探BOM
  • PostgreSQL入门与进阶学习,体系化的SQL知识,完成终极目标高可用与容灾,性能优化与架构设计,以及安全策略
  • ODBC+FreeTDS从Linux访问Windows SqlServer数据库
  • Chainlit一个快速构建成式AI应用的Python框架,无缝集成与多平台部署
  • leetcode日记(51)不同路径Ⅱ
  • 图解分布式事务中的2PC与Seata方案
  • 数据结构(Java):Map集合Set集合哈希表
  • 网络战时代的国家安全:策略、技术和国际合作
  • 【elasticsearch实现优先展示连词并按某个字段折叠显示最新一条】
  • Golang | Leetcode Golang题解之第284题窥视迭代器
  • C语言中的结构体
  • 3.qml与c++模块化开发