游戏屏蔽词算法——AC自动机lua版本

相关概念可以看  https://blog.csdn.net/txl199106/article/details/45315703

网上随便都有了。

另外在看之前请一定要先弄清楚Trie树和KMP匹配算法

AC自动机最大的好处是可以多屏蔽词同时匹配。(如果我没理解错的话)

但是不支持混淆匹配,例如 屏蔽词  AB,用户输入AXXXXB。这个时候不能进行混淆匹配

项目的另一套DFA算法是支持混淆匹配,但是不支持多屏蔽词同时匹配。

不知道有没有大佬能有同时支持2种的解决方案。

其他的算法分析我就不说了,网上一大把,但是实现都是C++的。

因为游戏业务需要,所以写了一套lua的AC,匹配写的有点糟糕,也掺杂了很多业务逻辑。希望大佬看到不要笑话。

上代码:

wordList是配置,按道理直接COPY过去就可以用,自己传配置列表进去

目前自己跑了一遍脚本,多词匹配貌似可以,但是没有大量随机验证,希望有碰到问题的可以留言。

没有做重复词的逻辑优化,如果有时间可能会加入,因为是前端屏蔽,所以对性能要求没有那么苛刻。

 --5.4更新  处理toCharArry对中文符号处理出现乱码的bug

--字符串转换为字符数组
--注入string table里面
function string.toCharArray(str)
    str = str or ""
    local array = {}
    local len = string.len(str)
    while str do
        local fontUTF = string.byte(str,1)

        if fontUTF == nil then
            break
        end
        --注意,如果输入的是中文,可能会占2、3、4位,而不是固定3位,例如 中文.占2位,但是其他非符号占3or4位,具体请看编码表的区间
        local byteCount = 0
        if fontUTF > 0 and fontUTF <= 127 then
            byteCount = 1
           elseif fontUTF >= 192 and fontUTF < 223 then
            byteCount = 2
           elseif fontUTF >= 224 and fontUTF < 239 then
            byteCount = 3
           elseif fontUTF >= 240 and fontUTF <= 247 then
            byteCount = 4
          end
          local tmp = string.sub(str,1,byteCount)
            table.insert(array,tmp)
            str = string.sub(str,byteCount + 1,len)
    end
    return array
end



local ACAutoMachine = {}    
ACAutoMachine.Trie = {
    char = "root",
    index = 1,
    isEnd = false,
    nodeDic = {},
    faild= nil,
}
local worldList = {}
local queue = {}

--创建trie树
function ACAutoMachine.creatTree(wordList)
    worldList = wordList
    for i,word in ipairs(wordList) do
        ACAutoMachine.creatBranchNode(ACAutoMachine.Trie,string.toCharArray(word))
    end

    --构造失败指针
    ACAutoMachine.buildNodeFaild(ACAutoMachine.Trie)
end

function ACAutoMachine.pushQueue(node)
    table.insert( queue, node)
end

function ACAutoMachine.popQueue()
    local tb = queue[1]
    table.remove(queue,1)
    return tb
end

--trie树分支的构建
function ACAutoMachine.creatBranchNode(root,charArry)
    local index = 1;
    local curNode = root
    local addressIndex = 1
    while index <= #charArry do
        local char = charArry[index]
        if curNode.nodeDic[char] ~= nil then -- 分支里面有这个字符,不做存储操作,指针指向这个字符node。
            curNode =  curNode.nodeDic[char]
            if  not curNode.isEnd then
                curNode.isEnd = index == #charArry
            end
        else
            local _node = {}
            _node.char = char
            _node.index = index
            if not _node.isEnd then
                _node.isEnd = index == #charArry --如果已经是结束字符,那么就不做处理,上同
            end
            _node.nodeDic = {}
            _node.faild = {}
            curNode.nodeDic[char] = _node
            curNode = curNode.nodeDic[char]
            
        end
        index = index + 1 
    end
end

function ACAutoMachine.buildNodeFaild(root)
    root.faild = nil
    ACAutoMachine.pushQueue(root)
    local parent  = {}
    local tmp_faild = {}
    while #queue > 0 do
        parent  = ACAutoMachine.popQueue()
        for char,node in pairs(parent.nodeDic) do
            if parent.char == "root" then --第一层的
                node.faild = parent
            else
                tmp_faild = parent.faild  --第一次进这里面,parent是第一层的,那么faild就是root
                while tmp_faild do --第一次 root  之后 有可能是root 有可能是某个node
                    if tmp_faild.nodeDic[char] then  --在第二层里面找
                        node.faild = tmp_faild.nodeDic[char]  --第一种情况,回溯的父节点的faild里面有,那么条件成立
                        break;
                    end
                    tmp_faild = tmp_faild.fail
                    if not tmp_faild then  --第一次,必然是空,于是指向root指针
                        node.faild = root
                    end 
                end
            end
            ACAutoMachine.pushQueue(node)
        end
    end
end



function ACAutoMachine.query(arry)
    local  p = ACAutoMachine.Trie
    local  len = #arry
    local indexList = {}
    local tempList = {}
    for i=1,len do
        local x = arry[i]
        --如果p找不到,并且p不是root
        while (not p.nodeDic[x]) and p.faild  do
            p = p.faild  --准备找P的失败节点
            if not p.faild then --找到根节点都没找到,代表之前的查找已经失败,不能完全匹配。清空标记列表
                tempList = {}
            end
        end
        p = p.nodeDic[x]  --开始寻找
        if not p then --还是找不到,指向root
            p = ACAutoMachine.Trie
        else
            table.insert( tempList, i)
        end
        
        if p.isEnd then  --某一组关键词已经完全匹配
            for i,index in ipairs(tempList) do --加入真实匹配列表
                table.insert(indexList, index)
            end
            tempList = {}--清空临时列表,继续找下一个关键字
        end
    end
    return indexList
end

function ACAutoMachine.replace(str,mark)
    mark = mark or "*"
    local arry = str:toCharArray()
    local maskList = ACAutoMachine.query(arry)
    local _str = ""
    for i,index in ipairs(maskList) do
        arry[index] = mark
    end
    for i,char in ipairs(arry) do
        _str = _str..char
    end
    return _str
end
function ACAutoMachine.getMaskWorlds()
    return worldList
end
return ACAutoMachine

 

 

 

 

posted on 2020-05-04 01:46  年轮下的  阅读(851)  评论(0编辑  收藏  举报

导航