alamide的笔记库「 87篇笔记 」「 小破站已建 0 天啦 🐶 」


敏感词汇过滤

2023-06-03, by alamide

敏感词过滤的算法,使用 DFA(Deterministic Finite Automaton) 算法,确定有穷自动机。其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。

1.存储结构

原本准备使用 Redis 来存储,但是感觉性能不如直接存在 HashMap 中好,因为一般敏感词使用的频率还是比较高的。为了保证访问速度,直接用 HashMap 来存储,不用 ConcurrentHashMap,在更新敏感词库时,参考 CopyOnWriteArrayList,可以直接用新生成的敏感词树替换原有的树。

2.Code

因为 Java Character 使用两个字节表示,所以有些字符需要两个 Char 表示,但是并不影响敏感词的过滤

@Component
public class DFAFilterSensitiveWord {
    private Map<String, Object> tree = new HashMap<>();

    @Resource
    private SensitiveWordMapper mapper;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class R {
        private Integer code;
        private String msg;
        //保证 json key 的顺序
        private Map<String, Object> data = new TreeMap<>();

        public static R success() {
            return new R(200, "success", new TreeMap<>());
        }

        public static R error() {
            return new R(400, "failure", null);
        }

        public R data(String key, Object value) {
            data.put(key, value);
            return this;
        }
    }

    @PostConstruct
    private void readSensitiveWord() {
        final List<SensitiveWord> words = mapper.getWords();
        final Set<String> stringSet = words.stream().map(SensitiveWord::getWord).collect(Collectors.toSet());
        Map<String, Object> origin = tree;
        tree = generateSensitiveTree(stringSet);
        origin.clear();
    }

    public synchronized void updateSensitiveTree() {
        readSensitiveWord();
    }

    private Map<String, Object> generateSensitiveTree(Set<String> words) {
        Map<String, Object> sensitiveTree = new HashMap<>();
        for (String word : words) {
            Map<String, Object> current = sensitiveTree;
            for (int i = 0; i < word.length(); i++) {
                String key = String.valueOf(word.charAt(i));

                if (current.containsKey(key)) {
                    current = (Map<String, Object>) current.get(key);
                } else {
                    Map<String, Object> map = new HashMap<>();
                    current.put(key, map);
                    current = map;
                }

                if (i == word.length() - 1) {
                    current.put("isEnd", true);
                }
            }
        }

        return sensitiveTree;
    }

    public R filterSensitiveWord(String content) {
        Map<String, Object> current = tree;
        StringBuilder word = new StringBuilder();
        StringBuilder resStr = new StringBuilder(content);
        List<String> sensitiveWords = new ArrayList<>();

        boolean matching = false;
        int index = 0;
        while (index < content.length()) {
            String c = String.valueOf(content.charAt(index));
            if (current.containsKey(c)) {
                matching = true;
                current = (Map<String, Object>) current.get(c);
                word.append(c);
                index++;
            } else {
                if (matching) {
                    if ((boolean) current.getOrDefault("isEnd", false)) {
                        resStr.replace(index - word.length(), index, "*".repeat(word.length()));
                        sensitiveWords.add(word.toString());
                    }
                    current = tree;
                    word = new StringBuilder();
                    matching = false;
                } else {
                    index++;
                }
            }
        }

        //最后一个词是敏感词
        if (matching) {
            if ((boolean) current.getOrDefault("isEnd", false)) {
                resStr.replace(index - word.length(), index, "*".repeat(word.length()));
                sensitiveWords.add(word.toString());
            }
        }

        return R.success().data("content", resStr).data("sensitiveWords", sensitiveWords);
    }
}

3.测试

@RestController
@RequestMapping("/sensitive")
public class SensitiveWordController {

    @Autowired
    private DFAFilterSensitiveWord filterSensitiveWord;

    @PostMapping("/filter")
    public R filter(@RequestBody String content) {
        return filterSensitiveWord.filterSensitiveWord(content);
    }
}

敏感词

法轮功
falungong
taidu
1450
微信
🐷
SB
🔪

RequestBody

到南京时,有朋友约去游逛,勾留了一日;第二日上午便须渡江到浦口,下午上车北去。父亲因为事忙,本已说定不送我,
叫旅馆里一个熟识的茶房陪我同去。他再三嘱咐茶房,甚是仔细。但他终于不放心,怕茶房不妥帖;颇踌躇了一会。
其🐷实我那年已二十岁,北京已来往过两三次,是没有什么要紧的了。他踌躇了一会,终于决定还是自己送我去。
我再三劝他不必去;他只说:“不要紧,他们去不好!”
我们过了江,进了taidu车站。我SB买票,他忙着照看行李。行李太多了,得向脚夫行些小费才可过去。
他便又忙着和他们讲价钱。我那时真是聪明过分,总觉他说话不大漂亮,非自己插嘴不可,但他终于讲falungong定了价钱;
就送我上车。他给我拣定了靠车门的一张椅子;我将他给我做的紫毛大衣铺好座位。他嘱我路上小心,夜里要警醒些,不要受凉。
又嘱托茶房好好照应我。我心里法轮功暗笑他的迂;他们只认得钱,托他们1450只是白托!而且我这样大年纪的人,难道还不能料理自🔪己么?
唉,我现在想想,那时真是太聪明了!

Response

{
    "code": 200,
    "msg": "success",
    "data": {
        "content": "到南京时,有朋友约去游逛,勾留了一日;第二日上午便须渡江到浦口,下午上车北去。父亲因为事忙,本已说定不送我,\n叫旅馆里一个熟识的茶房陪我同去。他再三嘱咐茶房,甚是仔细。但他终于不放心,怕茶房不妥帖;颇踌躇了一会。\n其**实我那年已二十岁,北京已来往过两三次,是没有什么要紧的了。他踌躇了一会,终于决定还是自己送我去。\n我再三劝他不必去;他只说:“不要紧,他们去不好!”\n我们过了江,进了*****车站。我**买票,他忙着照看行李。行李太多了,得向脚夫行些小费才可过去。\n他便又忙着和他们讲价钱。我那时真是聪明过分,总觉他说话不大漂亮,非自己插嘴不可,但他终于讲*********定了价钱;\n就送我上车。他给我拣定了靠车门的一张椅子;我将他给我做的紫毛大衣铺好座位。他嘱我路上小心,夜里要警醒些,不要受凉。\n又嘱托茶房好好照应我。我心里***暗笑他的迂;他们只认得钱,托他们****只是白托!而且我这样大年纪的人,难道还不能料理自**己么?\n唉,我现在想想,那时真是太聪明了!",
        "sensitiveWords": [
            "🐷",
            "taidu",
            "SB",
            "falungong",
            "法轮功",
            "1450",
            "🔪"
        ]
    }
}
Tags: DFA
~ belongs to alamide@163.com