敏感词过滤
网站在输入问题或者问题内容的时候为了避免有些人打广告,或者发表一些不当言论,我们需要增加敏感词过滤这个功能。
现在一般的网站的敏感词过滤都是用前缀树实现的,那么前缀树是什么呢?(自问自答- -)
字典树又称前缀树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高
下面是我画的前缀树的一个图
可以看到一个节点中需要包含一个字符和下一个节点的连接,这样的话我们就可以用一个hashmap来保存我们需要的信息,当然,既然是敏感词的前缀树,就需要添加字符、设置敏感词结尾、判断结尾、获取下一个节点的功能
一个敏感词过滤器的组成:
1、定义前缀树
2、根据敏感词,初始化前缀树
3、编写过滤敏感词的方法
1、定义前缀树
private class TrieNode { //关键词结束标志 private boolean isKeywordEnd = false; //子节点(key是下级字符,value是下级节点) private Map<Character, TrieNode> subNodes = new HashMap<>(); public boolean isKeywordEnd() { return isKeywordEnd; } public void setKeywordEnd(boolean keywordEnd) { isKeywordEnd = keywordEnd; } //添加子节点 public void addSubNode(Character c, TrieNode node) { subNodes.put(c, node); } //获取子节点 public TrieNode getSubNode(Character c) { return subNodes.get(c); } }
2、初始化前缀树
//被@PostConstruct修饰的方***在服务器加载Servlet的时候运行,并且只会被服务器执行一次。 //@PostConstruct在构造函数之后执行,init()方法之前执行 @PostConstruct public void init() { try ( InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); ) { String keyword; while ((keyword = reader.readLine()) != null) { //添加到前缀树 this.addKeyword(keyword); } } catch (Exception e) { logger.error("加载敏感词文件失败:" + e.getMessage()); } } //将敏感词添加到前缀树中 private void addKeyword(String keyword) { TrieNode tempNode = rootNode; for (int i = 0; i < keyword.length(); i++) { char c = keyword.charAt(i); TrieNode subNode = tempNode.getSubNode(c); if (subNode == null) { //初始化子节点 subNode = new TrieNode(); tempNode.addSubNode(c, subNode); } //指向子节点,进入下一轮循环 tempNode = subNode; //设置结束标识 if (i == keyword.length() - 1) { tempNode.setKeywordEnd(true); } } }
关键功能:敏感词过滤
关键的部分来了:
定义三个指针,将两个指针a、p指向文本开头,另一个指针temp指向前缀树的根节点
获取p指针指向的字符,在前缀树中获取到该节点,
1)并判断其是否为空,如果为空则不包含这个字符,p后移、a也后移
2)如果不为空且不是敏感词结尾,p++,继续上面的判断
3)如果为空且是敏感词结尾,将这一段的文本替换掉,p后移,a随着p后移继续判断,直到文本结束
3、实现过滤算法
public String filter(String text) { if (StringUtils.isBlank(text)) { return null; } //指针1 TrieNode tempNode = rootNode; //指针2 int begin = 0; //指针3 int position = 0; //结果 StringBuilder sb = new StringBuilder(); while (position < text.length()) { char c = text.charAt(position); //跳过符号 if (isSymbol(c)) { //若指针1处于根节点,将此符号计入计入结果,让指针2向下走一步 if (tempNode == rootNode) { sb.append(c); begin++; } //无论符号在开头还是中间,指针3都向下走一步 position++; continue; } //检查下级节点 tempNode = tempNode.getSubNode(c); if (tempNode == null) { //以begin开头的字符串不是敏感词 sb.append(text.charAt(begin)); //进入下一位置 position = ++begin; //重新指向根节点 tempNode = rootNode; } else if (tempNode.isKeywordEnd()) { //发现敏感词,将begin-position字符串替换掉 sb.append(REPLACEMENT); //进入下一位置 begin = ++position; //重新指向根节点 tempNode = rootNode; } else { //检查下一字符 position++; } } //最后一批字符计入结果 sb.append(text.substring(begin)); return sb.toString(); } //判断是否为符号 private boolean isSymbol(Character c) { return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF); }