论如何用低版本Erlang使用AC算法来实现屏蔽字过滤

最近项目需要实现一个屏蔽字算法的功能,本来参考了
erlang使用AC算法过滤屏蔽字
这篇文章,也很感谢作者提供的思路,但我在实践测试的过程中仍发现有些许不足之处

首先第一个是我们项目自己的原因,低版本到无法使用map结构,所以我选择用ets表来替代此结构,第二就是生成字典树这部分,按照作者的方式,必须对原始的字库进行严格的排序、筛选等,尽管如此还是会有很多BUG。第三,就是检测函数,也是有一些BUG的,最后我全部进行了改进测试,可以说目前这个是一个可以直接进行使用,并尽量提升了其稳定性、准确性和效率,尽量减少了对于性能的消耗,目前的测试都是百分百通过。

注:生成时需要耗时较多,生成后进行检测会很快。项目的屏蔽字库大概为25万条

%%生成字典树
proTri() ->
  %%初始化屏蔽字库需要的各种表
  ets:new(succ, [{keypos, 1}, named_table, set, public]),
  ets:new(fail, [{keypos, 1}, named_table, set, public]),
  ets:new(output, [{keypos, 1}, named_table, set, public]),
  %%这个地方现在就用来初始化屏蔽字库了
  add().%初始化屏蔽字库

%%AC算法生成屏蔽字库树
add() ->
  ets:insert(output, {index, 1}), %全局自增索引
  List = data_invitation_blockwords:get(bw),
  %%max进程字典用来记录层数,方便之后从第一层开始遍历
  put(max, 0),
  lists:foreach(fun(Words) ->
    cb_write_server:add(Words) end, List),
  bfs(),
  erlang:erase(max), %清除这个键和对应的值
  ets:delete(temp_1). %这个表不会被遍历,所以需要手动删除

%%生成Tri树,Floor对应的是每个节点在第几层,以便放入同一层的表中,方便之后进行广度优先遍历
add(Word) ->
  add(Word, Word, 0, ets:lookup_element(output, index, 2), 1).

add([], Word, NowNum, NextNum, Floor) ->
  Max = get(max),
  ?IF(Max >= Floor - 1, ok, put(max, Floor - 1)), %记录最大层
  A = ?IF(ets:lookup(output, NowNum) =/= [], ets:lookup_element(output, NowNum, 2), []),
  ets:insert(output, {NowNum, lists:usort([Word | A])}),
  ets:insert(output, {index, NextNum});
add([First | Left], Word, NowNum, NextNum, Floor) ->
  case ets:lookup(succ, {NowNum, First}) of
    [] ->
      ets:insert(succ, {{NowNum, First}, NextNum}),
      TempETSName = list_to_atom(lists:concat(['temp_', Floor])),
      case ets:info(TempETSName) of
        ?undefined ->
          ets:new(TempETSName, [{keypos, 1}, named_table, set, public]);
        _ -> ok
      end,
      %%把当前状节点加上当前字符得到的下一个节点统统记录入层数表中,方便之后遍历
      ets:insert(TempETSName, {{NowNum, First}, NextNum}),
      add(Left, Word, NextNum, NextNum + 1, Floor + 1);
    [{_, Num}] -> add(Left, Word, Num, NextNum, Floor + 1)
  end.

%%用于找到下一个节点的回退节点
findFail(0, _NextChar) ->
  0;
findFail(NowNum, NextChar) ->
  %%找到当前节点如果匹配不上后可以回溯到的节点状态
  PreFailNum = case ets:lookup(fail, NowNum) of
                 [] -> 0;
                 [{_, TempNum1}] -> TempNum1
               end,
  %%回溯到的节点的状态能否也匹配上当前输入字符,进入它已有的下一个节点状态,不能匹配则继续回退直到根节点也没有为止
  case ets:lookup(succ, {PreFailNum, NextChar}) of
    [] -> findFail(PreFailNum, NextChar);
    [{_, TempNum2}] -> TempNum2
  end.

%%广度遍历搜索,遍历每一层,建立回退节点并补全节点的输出状态
bfs() ->
  bfs(2, get(max) + 1).

bfs(Max, Max) ->
  ok;
bfs(FloorNum, Max) ->
  EtsName = list_to_atom(lists:concat(['temp_', FloorNum])),
  ets:foldl(fun({{NowNum, NextChar}, NextNum}, Acc) ->

    FailNum = findFail(NowNum, NextChar),
    case FailNum of
      0 -> ok;
      _ ->
        %%插入失敗節點
        ets:insert(fail, {NextNum, FailNum}),
        %%檢查這個節點的回退节点是否有輸出屏蔽字,如果有输出,那么这个节点也包含了这个屏蔽单词,屏蔽单词需要加入到这个节点的输出列表中
        case ets:lookup(output, FailNum) of
          [] -> ok;
          [{_, PreOutputList}] ->
            case ets:lookup(output, NextNum) of
              [] -> ets:insert(output, {NextNum, PreOutputList});
              [{_, NowOutPutList}] -> ets:insert(output, {NextNum, NowOutPutList ++ PreOutputList})
            end
        end
    end,
    Acc
            end, 0, EtsName),
  %%删除建立的层数表
  ets:delete(EtsName),
  %%遍历下一层
  bfs(FloorNum + 1, Max).


%%检测是否包含屏蔽字,返回0表示未包含,可以使用
check(String) ->
  check(String, 0).

check([], _Num) -> 0; %0为未找到匹配项,其他为找到了匹配项需要屏蔽
check([First | Other], NowNum) ->
  NextNum = next_num(NowNum, First),
  case ets:lookup(output, NextNum) of
    [] -> check(Other, NextNum);
    Result -> Result
  end.

%%查找下一个该去的节点
next_num(Num, Char) ->
  case ets:lookup(succ, {Num, Char}) of
    [] ->
      FailNum = case ets:lookup(fail, Num) of
                  [] -> 0;
                  [{_, TempNum1}] -> TempNum1
                end,
      case Num of
        0 -> 0; %Num为0,初始状态的节点都匹配不上任何字符的话,这个就可以视为匹配不上了
        _ -> next_num(FailNum, Char) %Num不为零,那么就到节点的回退点继续进行检测
      end;
    [{_, NewNum}] -> NewNum
  end.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值