Lua 实现屏蔽字(全字段匹配以及简易模糊匹配)

屏蔽字

	最近在工作中做了一个聊天系统,接触到了屏蔽字,由于一开始并没有重视,导致上线后由于数据量较大出现了
卡顿的情况,多方排查后才发现原来是这个不起眼的小家伙导致的卡顿,这里就跟大家分享一下做屏蔽字的一点点小
心得。
	首先实现屏蔽字我们必须需要的东西有两个,一个是我们屏蔽字的算法,另一个就是屏蔽字的表了,屏蔽字表应
该在网上都能搜到,至于是不是你想要的格式如果不是的话就只有自己处理一下了。
	我这里使用的屏蔽字表格式为:
	MaskWordLib ={
					[1] = {
						word = "傻逼",
					},
				}
原始暴力算法
--暴力循环
function obj:MaskBadWord(text)
	for k,v in pairs(MaskWordLib) do
		local textLen = string.widthSingleGbk(v.word)
		if textLen <= 2 then
			text = string.gsub(text,v.word,"*")
		elseif textLen > 2 and textLen < 6 then 
			text = string.gsub(text,v.word,"**") 			
		else
			text = string.gsub(text,v.word,"***")
		end
	end
	return text
end

这里就是简单的判断屏蔽字的长度再通过循环去测试每一个屏蔽字是否出现在字符串中,也是我们一开始使用的屏蔽字算法由于之前只是取名或者个性签名等少量的使用并没有注重算法的效率所以也就忽略了。

递归优化
local MaskWordLibGbkList = {}--全局变量储存排序后的屏蔽字库
local OutPutText = ""--全局变量储存最后返回的字符串(因为递归时会多处修改所以需要全局变量)
--屏蔽字方法
function obj:MaskBadWord(text)
	OutPutText = text --最终输出值
	local localText = text --匹配函数参数
	obj:FuzzyFindMaskWork(localText,1)--模糊匹配
	--obj:FindMaskWork(localText,1)--全额匹配
	return OutPutText
end
--全额匹配屏蔽字
function obj:FindMaskWork(localText,lastMaskWordGbk)--lastMaskWordGbk 是上一次匹配到的屏蔽字长度
	local textGbk = string.widthSingleGbk(localText)--字符串长度
	local textLen = string.len(localText)--字符串len长度
	local Mask = "*"--替换屏蔽字的符号
	for i=1,#MaskWordLibGbkList do --MaskWordLibGbkList 是通过GBK排序后的屏蔽字表
		if MaskWordLibGbkList[i].Gbk >= lastMaskWordGbk and MaskWordLibGbkList[i].Gbk <= textGbk and MaskWordLibGbkList[i].Len <= textLen then
			lastMaskWordGbk = MaskWordLibGbkList[i].Gbk
			if MaskWordLibGbkList[i].Gbk <= 2 then
				Mask="*"
			elseif MaskWordLibGbkList[i].Gbk > 2 and MaskWordLibGbkList[i].Gbk < 6 then
				Mask="**"
			else
				Mask="***"
			end
			local findWordFirstPos = string.find(localText,MaskWordLib[MaskWordLibGbkList[i].wordId].word)
			local _,findWorldLastPos = string.find(localText,MaskWordLib[MaskWordLibGbkList[i].wordId].word)
			local firstWordLen,lastWordLen = obj:GetFirstAndLastWordLen(MaskWordLib[MaskWordLibGbkList[i].wordId])
			if findWordFirstPos ~= nil then
				OutPutText = string.gsub(OutPutText,MaskWordLib[MaskWordLibGbkList[i].wordId].word,Mask)
				if findWordFirstPos>1 then
					local textFirst = string.sub(localText, 1, findWordFirstPos-1)
					obj:FindMaskWork(textFirst,lastMaskWordGbk)
				end
				if findWorldLastPos+lastWordLen < textLen then
					local textLast = string.sub(localText, findWorldLastPos+lastWordLen ,textLen)
					obj:FindMaskWork(textLast,lastMaskWordGbk)
				end
			end
		else
			break;
		end
	end
end

这里需要注意的就是找到屏蔽字后只需要将前半部分以及后边部分用类似于快排的方式递归就行,需要
避坑的就是前半段很好取后半部分需要知道最后一个屏蔽字长度,否则要么会多取一个字要么函数报错,因为string.find()参数内如果是汉字则用len长度的首位表示,但是string.sub()要取的话则要首字的第一位以及尾字的最后一位才能取出完整字符串

--模糊匹配屏蔽字
function obj:FuzzyFindMaskWork(localText,MaskWordGbk)
	local textGbk = string.widthSingleGbk(localText)
	local textLen = string.len(localText)
	local Mask = "*"
	for i=1,#MaskWordLibGbkList do
		if MaskWordLibGbkList[i].Gbk >= MaskWordGbk and MaskWordLibGbkList[i].Gbk <= textGbk and MaskWordLibGbkList[i].Len <= textLen then
			if MaskWordLibGbkList[i].Gbk <= 2 then
				Mask="*"
			elseif MaskWordLibGbkList[i].Gbk > 2 and MaskWordLibGbkList[i].Gbk < 6 then
				Mask="**"
			else
				Mask="***"
			end
			MaskWordGbk = MaskWordLibGbkList[i].Gbk
			local firstMaskWordLen,lastMaskWordLen = obj:GetFirstAndLastWordLen(MaskWordLib[MaskWordLibGbkList[i].wordId].word)
			local firstword = string.sub(MaskWordLib[MaskWordLibGbkList[i].wordId].word,1,firstMaskWordLen)
			local lastword = string.sub(MaskWordLib[MaskWordLibGbkList[i].wordId].word,-lastMaskWordLen)
			local findMaskWordFirstPos = string.find(localText,firstword)	
			local wordList = ""
			local wordListLen = 0
			if findMaskWordFirstPos~=nil then
				local findMaskWorldLastPos  = string.find(localText,lastword)
				if findMaskWorldLastPos~=nil then
					wordListLen = findMaskWorldLastPos-findMaskWordFirstPos
					if wordListLen>0 and wordListLen < MaskWordLibGbkList[i].Len * 2 then
						wordList = string.sub(localText,findMaskWordFirstPos,findMaskWorldLastPos+lastMaskWordLen-1)
						OutPutText = string.gsub(OutPutText,wordList,Mask)
						if findMaskWordFirstPos>1 then
							local textFirst = string.sub(localText, 1, findMaskWordFirstPos-1)
							obj:FuzzyFindMaskWork(textFirst,MaskWordGbk)
						end
						if findMaskWorldLastPos<textLen then
							local textLast = string.sub(localText, findMaskWorldLastPos+lastMaskWordLen,textLen)
							obj:FuzzyFindMaskWork(textLast,MaskWordGbk)
						end
					end	
				end	
			end	
		else
			break;
		end
	end
end

这里模糊匹配的思想也是在CSDN上参考的,就是将屏蔽词的首字和尾字取出来,然后先找首字,如果找到了就继续找尾字,都找的的情况下将字符串中首位两字之间的整段取出,如果取出字符串len长度小于二倍的屏蔽词len长度的话那么就有可能是<傻)(逼 >,这样的形势,当然这样也不可避免的会误屏蔽部分发言,但是总的来说还是利大于弊,因为误屏蔽的字段很难说是完完全全干净的。

--给屏蔽字表排序
function obj:SortMaskWord()
	for k,v in pairs(MaskWordLib) do
		local list = {
						Len = string.len(v.word),
						Gbk = string.widthSingleGbk(v.word),
						wordId = k,
					}
		table.insert(MaskWordLibGbkList,list)
	end
	table.sort(MaskWordLibGbkList , function(a,b)
		if a.Gbk==b.Gbk then
			return a.Len<b.Len
		else
			return a.Gbk<b.Gbk
		end	
	end)
end

这个用在启动加载时,因为启动一次只需要排序一次,增加了len长度和GBK长度,或者也可以将表更新为我们这样的格式和排序,但是我们屏蔽字词会随着大神的各种作死方式而增加,所以每次启动排序一次还是比较好的,排序方式的话用的是封装好的函数,如果你们没有的话,可以搜一个高效的排序方式,我当时的屏蔽字库有十万条,自己写的话做demo可以真正使用还是需要使用高效的算法。

--输出首尾字符长度
function obj:GetFirstAndLastWordLen(inputstr)
   local lenInByte = #inputstr
   local i = 1
   local firstWordLen = 1
   local lastWordLen = 1
    while (i<=lenInByte)
    do
        local curByte = string.byte(inputstr, i)
        local byteCount = 1;
        if curByte>0 and curByte<=127 then
            byteCount = 1                                           --1字节字符
        elseif curByte>=192 and curByte<223 then
            byteCount = 2                                           --双字节字笿
        elseif curByte>=224 and curByte<239 then
            byteCount = 3                                           --汉字
        elseif curByte>=240 and curByte<=247 then
            byteCount = 4                                           --4字节字符
        end
		if i==1 then
			firstWordLen = byteCount
		elseif i + byteCount > lenInByte then
			lastWordLen = byteCount
		end	
        local char = string.sub(inputstr, i, i+byteCount-1)         --看看这个字是什乿
        i = i + byteCount                                           -- 重置下一字节的索弿
    end
    return firstWordLen,lastWordLen
end

这个函数其实是string.widthSingleGbk()的源码稍做修改得来的,因为没有找到什么太好的获取字符串指定字符的len长度的方式所以用这个函数改编了一下自己写了一个,如果有封装好的方法,也希望大佬可以告知一下。

总结

速度上说是比原始的方法快了很多,当然主要原因不是新方法太好而是老方法太笨,其次就是全字段匹配是不符合当今社会人的脑洞的,因为任何字词都会以你想不到但是看得懂的方式出现在你面前,而我们这里的模糊匹配呢解决了一部分但是也没完全解决,所谓魔高一尺道高一丈嘛,如果他把第一个字或者最后一个字换成其他的字呢,还有就是文字顺序的问题很多时候乱序但是人们一样可以读的懂,我觉得更好的方法有很多我暂时的思路是屏蔽词全拆开逐字在句中找,当然是在前后一定范围内,然后将找到的最长串取出看匹配率超过50%则认为其是违规又或者是根据屏蔽字长度设置不同的通过匹配率,大家有什么好的想法可以一起讨论呀
最后因为整个函数是文字控制函数还有其他方法所以做成了obj,使用时不用在意这个自己写方法名调用就可以了。还有就是这是我的第一篇文章,以后可能会分享一些自己在工作和学习中能用到的一些实用的小方法啊小demo之类的,一起学游戏开发吧。

再说两句

最近元宇宙+NFT的概念很火,这让我想起了许多年前玩侠盗飞车时自己脑子里的想法,也是这个想法让我一步一步走向了游戏开发,本来写了很多,算了哈哈,以后有分享别的东西再写吧。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值