敏感词过滤算法 —— DFA

DFA,全称 Deterministic Finite Automaton 即确定有自动机:从一个状态通过一系列的事件转换到另一个状态,即 state -> event -> state。 


  • 确定:状态以及引起状态转换的事件都是可确定的,不存在“意外”。

  • 有穷:状态以及事件的数量都是可穷举的。


计算机操作系统中的进程状态与切换可以作为 DFA 算法的一种近似理解。如上图所示,其中椭圆表示状态,状态之间的连线表示事件,进程的状态以及事件都是可确定的,且都可以穷举。

数据结构

敏感词过滤算法 —— DFA                                                              图1. DFA算法数据结构

观察图1,如果对Trie树(前缀树)数据结构有了解的话,就可以发现DFA本质上是构造一颗Trie树。如果抛开root节点的话,实际上是一个森林,每一个节点都的数据结构如下:敏感词过滤算法 —— DFA                                                                图2 DFA节点结构

DFA算法的核心就是围绕这一颗树展开的。

匹配过程:

以图1 为例子,讲解一下DFA算法的匹配过程:假设有个input={他是个超凡脱俗的人},在DFA算法中具体是如何匹配的呢?

  1. index = 0,event= {他} ==> exist={false}
  2. index = 1,event= {是} ==> exist={false}
  3. index = 2,event= {个} ==> exist={false}
  4. index = 3,event= {超} ==> exist={true},isEnd={false},state={超}
  5. index = 4,event= {凡},state= {超} ==> exist{true} ,isEnd = {false},state={超凡}
  6. index = 5,event= {脱},state= {超凡} ==> exist{true} ,isEnd = {false},state={超凡脱}
  7. index = 6,event= {俗},state= {超凡脱} ==> exist{true} ,isEnd = {end},state={超凡脱俗},匹配成功
  8. index = 7,event= {的} ==> exist={false}
  9. index = 8,event= {人} ==> exist={false}
  10. end,output={超凡脱俗}

翻译一下上面流程就是:

  1. 循环遍历需要匹配的字符串,根据当前下标的字( text.charAt(index) )去判断Trie树中是否存在该节点,在上述例子中,直到第四步才匹配到 “超” 字,这表示存在以“超”开头的敏感词,由于isEnd={false}表明这只是个中间态,而不是一个完整词,所以还不能说命中了敏感词 “超”
  2. 下标右移动,当前词为“凡”,然后在Trie树中判断以“超”节点为起始节点,“凡” 为事件,匹配到状态“超凡”这个节点,由于 “超凡”的isEnd={false},所以不算命中。
  3. 直到第7步,以“超凡脱”为起始节点,“俗”为事件,匹配到状态“超凡脱俗”这个节点,并且isEnd={true},表明为终止态,说明已经是一个完整的词,所以命中敏感词 “超凡脱俗”。

敏感词过滤算法 —— DFA                                                                图3. DFA匹配过程

复杂度分析

  • 时间复杂度 由于底层使用的是Trie树,所有分支的存储是使用HashMap,查找单个字符(Map.get())的时间复杂度为O(1) ;但是对于输入的字符串,需要进行一次完整的遍历,所以对应的时间复杂度为O(n)。
  • 空间复杂度 查询时,无需额外的空间

具体实现

1.Trie树每个节点: DictionaryTreeState

/**
 * 表示DFA/字典树中的状态节点
 */

public class  {
    // 是否是终止状态
    boolean isEnd;
    // 可走的边,key为边的条件,value为下一个状态
    Map<String, DictionaryTreeState> edges;
 
    public DictionaryTreeState() {
        this.isEnd = false;
        edges = new HashMap<>();
    }
 
    public void addEdge(String key, DictionaryTreeState nextDictionaryTreeState) {
        this.edges.put(key, nextDictionaryTreeState);
    }
 
    public void addEdge(char key, DictionaryTreeState nextDictionaryTreeState) {
        addEdge(String.valueOf(key), nextDictionaryTreeState);
    }
 
    /**
     * 根据 key 找到下一个状态
     *
     * @param key
     * @return
     */

    public DictionaryTreeState getTrieState(String key) {
        return this.edges.get(key);
    }
 
    /**
     * 根据 key 找到下一个状态
     *
     * @param key
     * @return
     */

    public DictionaryTreeState getTrieState(char key) {
        return getTrieState(String.valueOf(key));
    }
 
    /**
     * 是否是结束状态
     *
     * @return
     */

    public boolean isEnd() {
        return this.isEnd;
    }
 
    public void setEnd(boolean isEnd) {
        this.isEnd = isEnd;
    }
 
    /**
     * 是否是终止(叶子)节点
     *
     * @return
     */

    public boolean isTerminal() {
        return this.edges.isEmpty();
    }
 
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("{"").append("isEnd":").append(isEnd)
                .append(",");
        for (Map.Entry<String, DictionaryTreeState> entry : edges.entrySet()) {
            sb.append(""").append(entry.getKey()).append("":").append(entry.getValue()).append(",");
        }
        sb.setLength(sb.length() - 1);
        sb.append("}");
        return sb.toString();
    }
}

2.构造Trie树 DictionaryTreeBuilder

public class DictionaryTreeBuilder {
    private final List<String> words;
 
    public DictionaryTreeBuilder() {
        this.words = new ArrayList<>();
    }
 
    public DictionaryTreeBuilder addWords(Collection<String> words) {
        this.words.addAll(words);
        return this;
    }
 
    public DictionaryTreeBuilder addWords(String... words) {
        return this.addWords(Arrays.asList(words));
    }
 
    public DictionaryTreeState build() {
        if (words.size() == 0) {
            return null;
        }
 
        DictionaryTreeState root = new DictionaryTreeState();
        for (String word : words) {
            if (word == null || "".equals(word)) {
                continue;
            }
            DictionaryTreeState current = root;
            for (char c : word.toCharArray()) {
                DictionaryTreeState next = current.getTrieState(c);
                if (next == null) {
                    next = new DictionaryTreeState();
                    current.addEdge(c, next);
                }
                current = next;
            }
            current.setEnd(true);
        }
 
        return root;
    }
}

3.敏感词检测 SensitiveWordsCheckUtil

public class SensitiveWordsCheckUtil {
    private final DictionaryTreeState root;
 
    public SensitiveWordsCheckUtil(DictionaryTreeState root) {
        this.root = root;
    }
 
    public Set<String> check(String text) {
        if (text == null || "".equals(text)) {
            return null;
        }
        Set<String> illWords = new LinkedHashSet<>();
        for (int i = 0; i < text.length(); i++) {
            StringBuilder stringBuilder = new StringBuilder();
            char c = text.charAt(i);
            DictionaryTreeState start = root.getTrieState(c);
            if (start == null) {
                continue;
            }
 
            stringBuilder.append(c);
            int j = i;
            DictionaryTreeState next = start;
            // 记录最近匹配成功的敏感词,主要是为了实现最大匹配
            String illWord = null;
            while (true) {
                j++;
                if (j == text.length()) {
                    break;
                }
                c = text.charAt(j);
                next = next.getTrieState(c);
                stringBuilder.append(c);
                if (next == null) {
                    break;
                }
                if (next.isEnd()) {
                    illWord = stringBuilder.toString();
                }
                if (next.isTerminal()) {
                    break;
                }
            }
            if (illWord != null && !"".equals(illWord)) {
                illWords.add(illWord);
            }
        }
 
        return illWords;
    }
}


原文始发于微信公众号(Hephaestuses):敏感词过滤算法 —— DFA

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/45006.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!