最近项目需要实现一个屏蔽字算法的功能,本来参考了
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.