Java实现AhoCorasick算法过滤中文敏感词

AhoCorasick算法简介

Aho-Corasick算法简称AC算法,通过将模式串预处理为确定有限状态自动机,扫描文本一遍就能结束。其复杂度为O(n),即与模式串的数量和长度无关。详细介绍可参考这篇文章,下面直接上代码:

构建AhoCorasick类

类中使用了单例模式,以免在代码中频繁通过new实例化AhoCorasick类,浪费内存,提升接口性能

public class AhoCorasickAutomation {
/*本示例中的AC自动机处理中文类型的字符串,所以数组的长度是128*/
    private static final int Unicode = 65536;
     
    /*AC自动机的根结点,根结点不存储任何字符信息*/
    private Node root;
     
    /*待查找的目标字符串集合*/
    private List<String> target;
     
    /*表示在文本字符串中查找的结果,key表示目标字符串, value表示目标字符串在文本串出现的位置*/
    private HashMap<String, List<Integer>> result;
     
    /*内部静态类,用于表示AC自动机的每个结点,在每个结点中我们并没有存储该结点对应的字符*/
    private static class Node{
         
    /*如果该结点是一个终点,即,从根结点到此结点表示了一个目标字符串,则str != null, 且str就表示该字符串*/
    String str;
         
    /*ASCII == 65536, 所以这里相当于65536叉树*/
    Node[] table = new Node[Unicode];
         
    /*当前结点的孩子结点不能匹配文本串中的某个字符时,下一个应该查找的结点*/
    Node fail;
         
    public boolean isWord(){
            return str != null;
        }
         
    }
    /*单例实例*/
    private static volatile AhoCorasickAutomation instance;
    /*target表示待查找的目标字符串集合*/
    private AhoCorasickAutomation(List<String> target){
        root = new Node();
        this.target = target;
        buildTrieTree();
        build_AC_FromTrie();
    }
       /*获取单例实例的方法*/
    public static AhoCorasickAutomation getInstance(List<String> target) {
        if (instance == null) {
            synchronized (AhoCorasickAutomation.class) {
                if (instance == null) {
                    instance = new AhoCorasickAutomation(target);
                }
            }
        }
        return instance;
    }
    /*由目标字符串构建Trie树*/
    private void buildTrieTree(){
        for(String targetStr : target){
            Node curr = root;
            for(int i = 0; i < targetStr.length(); i++){
                char ch = targetStr.charAt(i);
                if(curr.table[ch] == null){
                    curr.table[ch] = new Node();
                }
                curr = curr.table[ch];
            }
            /*将每个目标字符串的最后一个字符对应的结点变成终点*/
            curr.str = targetStr;
        }
    }
    /*由Trie树构建AC自动机,本质是一个自动机,相当于构建KMP算法的next数组*/
    private void build_AC_FromTrie(){
        /*广度优先遍历所使用的队列*/
        LinkedList<Node> queue = new LinkedList<Node>();
         
        /*单独处理根结点的所有孩子结点*/
        for(Node x : root.table){
            if(x != null){
                /*根结点的所有孩子结点的fail都指向根结点*/
                x.fail = root;
                queue.addLast(x);/*所有根结点的孩子结点入列*/
            }
        }

    while(!queue.isEmpty()){
            /*确定出列结点的所有孩子结点的fail的指向*/
            Node p = queue.removeFirst();
            for(int i = 0; i < p.table.length; i++){
                if(p.table[i] != null){
                    /*孩子结点入列*/
                      queue.addLast(p.table[i]);
                    /*从p.fail开始找起*/
                    Node failTo = p.fail;
                    while(true){
                        /*说明找到了根结点还没有找到*/
                        if(failTo == null){
                            p.table[i].fail = root;
                            break;
                        }

                        /*说明有公共前缀*/
                        if(failTo.table[i] != null){
                            p.table[i].fail = failTo.table[i];
                            break;
                        }else{/*继续向上寻找*/
                            failTo = failTo.fail;
                        }
                    }
                }
            }
        }
    }

     /*在文本串中查找所有的目标字符串*/
    public HashMap<String, List<Integer>> find(String text){
        /*创建一个表示存储结果的对象*/
        result = new HashMap<String, List<Integer>>();
        for(String s : target){
            result.put(s, new LinkedList<Integer>());
        }
        Node curr = root;
        int i = 0;
        while(i < text.length()){
            /*文本串中的字符*/
            char ch = text.charAt(i);
            /*文本串中的字符和AC自动机中的字符进行比较*/
            if(curr.table[ch] != null){
                /*若相等,自动机进入下一状态*/
                curr = curr.table[ch];
                 
                if(curr.isWord()){
                    result.get(curr.str).add(i - curr.str.length()+1);
                }
                /*这里很容易被忽视,因为一个目标串的中间某部分字符串可能正好包含另一个目标字符串,
                 * 即使当前结点不表示一个目标字符串的终点,但到当前结点为止可能恰好包含了一个字符串*/
                if(curr.fail != null && curr.fail.isWord()){
                    result.get(curr.fail.str).add(i - curr.fail.str.length()+1);
                }
                /*索引自增,指向下一个文本串中的字符*/
                i++;
            }else{
                /*若不等,找到下一个应该比较的状态*/
                curr = curr.fail;
                /*到根结点还未找到,说明文本串中以ch作为结束的字符片段不是任何目标字符串的前缀,
                 * 状态机重置,比较下一个字符*/
                if(curr == null){
                    curr = root;
                    i++;
                }
            }
        }
        return result;
    }

}

读取敏感词文件

敏感词文档可在这里获取,其中包含了互联网大部分中文敏感词汇,下载后可存放本地,通过FileReader读取内容,加载至本地缓存或者redis等中间件中,本文存入本地缓存,使用ConcurrentHashMap存储

@Scheduled(fixedDelay = 12 * 60 * 60 * 1000)
    public void readSensetiveWord() throws IOException {
        log.info("==============开始读取敏感词============");
        List<String> sensetiveWord = new ArrayList<>();
        //读取本地文件,存入本地缓存
       File file = new File("G:\\敏感词\\敏感词.txt");
        //读取file
        BufferedReader br = new BufferedReader(new FileReader(file));
        String line;
        while ((line = br.readLine()) != null){
            sensetiveWord.add(line);
        }
        log.info("==============敏感词读取完毕============");
        //存入本地缓存ConcurrentHashMap
        sensitiveWordsMap.put("sensetiveWordList", sensetiveWord);
        log.info("==============敏感词存入本地缓存============");
}

构建Service类

@Service
@Slf4j
public class ACService {
    private final ConcurrentHashMap<String, List<String>> sensitiveWordsMap = new ConcurrentHashMap<>();
    public BaseResult filterWord(InputDto inputDto) {
        String text = inputDto.getText();
        log.info("用户输入的内容为:{}", text);
        //取出本地缓存的敏感词
        List<String> sensetiveWordList = sensitiveWordsMap.get("sensetiveWordList");
        if (sensetiveWordList == null || sensetiveWordList.size() == 0) {
            log.info("敏感词为空,请检查敏感词文件");
            return BaseResult.build(500, "敏感词为空,请检查敏感词文件");
        }
        //调用AhoCorasickAutomation进行敏感词过滤
            AhoCorasickAutomation ahoCorasickAutomation = AhoCorasickAutomation.getInstance(sensetiveWordList);
            HashMap<String, List<Integer>> result = ahoCorasickAutomation.find(text);
            //对敏感词进行替换
            for(Map.Entry<String, List<Integer>> entry : result.entrySet()){
                for(Integer index : entry.getValue()){
                    text = text.substring(0, index) + "*和谐*" + text.substring(index + entry.getKey().length());
                }
            }
            log.info("敏感词过滤结果为:{}", text);
            return BaseResult.build(200, "敏感词过滤成功", text);
        }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值