基于前缀树 + 词库的敏感词查找方案

409 阅读1分钟

前言

近期在做公司历史文章敏感词检测,网安提供了一份词库。早期同学实现的方式是通过 es 中的全文索引(IK 分词)进行匹配,由于分词的特性导致了许多敏感词无法有效的被检测出来,从而导致了处罚。近期重启文章清理,这边分享一种基于前缀树 + 词库的敏感词匹配方案,查找时间复杂度在 O(n) - O(n²)之间,n 为文章长度,复杂度与敏感词数量无关。


一、新建前缀树节点类

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 敏感词前缀树节点
 *
 * @author omg on 2021/10/28
 **/
public class SensitiveTrieNode {

    /**
     * 是否为单词结尾
     */
    private Boolean end;

    /**
     * 下一个节点
     */
    private Map<Character, SensitiveTrieNode> nodes;

    public SensitiveTrieNode() {
        this.nodes = new ConcurrentHashMap<>();
    }

    public Boolean getEnd() {
        return end;
    }

    public void setEnd(Boolean end) {
        this.end = end;
    }

    public Map<Character, SensitiveTrieNode> getNodes() {
        return nodes;
    }

    public void setNodes(Map<Character, SensitiveTrieNode> nodes) {
        this.nodes = nodes;
    }
}

二、新建前缀词树查找类

import lombok.Data;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 敏感词前缀树
 *
 * @author omg on 2021/10/28
 **/
public class SensitiveTrieTree {

    private Map<Character, SensitiveTrieNode> roots;

    public SensitiveTrieTree() {
        roots = new ConcurrentHashMap<>();
    }

    /**
     * 添加敏感词
     *
     * @param word 敏感词
     */
    public void addWord(String word) {
        if (word == null || word.isEmpty()) {
            return;
        }
        Map<Character, SensitiveTrieNode> currNodes = roots;
        for (int i = 0; i < word.length(); i++) {
            SensitiveTrieNode node = currNodes.get(word.charAt(i));
            if (node == null) {
                node = new SensitiveTrieNode();
                node.setEnd(i == word.length() - 1);
                currNodes.put(word.charAt(i), node);
            }
            currNodes = node.getNodes();
        }
    }

    /**
     * 获取文本中的敏感词
     * <p>
     * 时间复杂度:
     * n = 文本长度
     * 最差 o(n^2)
     * 最优 o(n)
     * </p>
     *
     * @param txt 要查询敏感词的文本
     * @return {@link List<String>}
     */
    public Set<String> getWords(String txt) {
        Set<String> result = new HashSet<>();
        if (txt == null || txt.isEmpty()) {
            return result;
        }
        for (int i = 0; i < txt.length(); i++) {
            Map<Character, SensitiveTrieNode> currNodes = roots;
            for (int j = i; j < txt.length(); j++) {
                SensitiveTrieNode node = currNodes.get(txt.charAt(j));
                if (node == null) {
                    break;
                }
                if (node.getEnd()) {
                    result.add(txt.substring(i, j + 1));
                    i = j;
                    break;
                }
                currNodes = node.getNodes();
            }
        }
        return result;
    }
}

三、调用测试

public static void main(String[] args) {
    SensitiveTrieTree trieTree = new SensitiveTrieTree();
    trieTree.addWord("新疆");
    trieTree.addWord("西藏");
    System.out.println(trieTree.getWords("东躲西藏,我把东西藏哪了,开辟新疆图"));
}

调用结果 image.png

四、总结

以上就是通过前缀树 + 词库实现敏感词匹配的实现方式。