可能不止在天朝,绝大多数网站都会须要违禁词过滤模块,用于对不雅言论进行屏蔽;因此这个应该算是网站的基础功能。大概在去年的时候我开发过这个功能,当时用6600+(词数)的违禁词库,过滤2000+(字数)的文章,耗时大概20ms左右,当时自我感受还挺良好。过了这一段时间,回想一下,其实有很多地方能够作优化、能够总结;因而从头至尾捋了一遍。java
/** * 过滤违禁词 * @param sentence:待过滤字符串 * @return */ private BadInfo findBadWord(String sentence) { CharType[] charTypeArray = getCharTypes(sentence);//获取出每一个字符的类型 BadInfo result = new BadInfo(sentence); BadWordToken token; int i = 0, j; int length = sentence.length(); int foundIndex; char[] charArray; StringBuffer wordBuf = new StringBuffer(); while (i < length) { // 只处理汉字和字母 if (CharType.HANZI == charTypeArray[i] || CharType.LETTER == charTypeArray[i]) { j = i + 1; wordBuf.delete(0, wordBuf.length());//新的一轮匹配,清除掉原来的 wordBuf.append(sentence.charAt(i)); charArray = new char[] { sentence.charAt(i) }; foundIndex = wordDict.getPrefixMatch(charArray);//前缀匹配违禁词 //foundIndex表示记录了前缀匹配的位置 while (j <= length && foundIndex != -1) { // 表示找到了 if (wordDict.isEqual(charArray, foundIndex) && charArray.length > 1) { token = new BadWordToken(new String(charArray), i, j); result.addToken(token);//记录下来 i = j - 1; // j在匹配成功时已经自加了,这里是验证确实是违禁词,因此须要将j前一个位置给i } // 去掉空格 while (j < length && charTypeArray[j] == CharType.SPACE_LIKE) j++; if (j < length && (charTypeArray[j] == CharType.HANZI || CharType.LETTER == charTypeArray[j])) { //将下个字符和前面的组合起来, 继续前缀匹配 wordBuf.append(sentence.charAt(j)); charArray = new char[wordBuf.length()]; wordBuf.getChars(0, charArray.length, charArray, 0); foundIndex = wordDict.getPrefixMatch(charArray, foundIndex);//前缀匹配违禁词 j++; } else { break; } } } i++; } return result; }
上面的逻辑和代码实现只是过滤违禁词外层实现,具体如何在违禁词库中,查询指定字符串,是最为关键的,即:词典WordDict的数据结构,和它的算法getPrefixMatch() 方法,也是涉及到性能优化的地方。
private char[][][] wordItem_real;
第一维 wordItem_real[i] 其含义是:具备相同开头汉字X,的全部违禁词(一组)。其中下标 i 为 X 的 GB2312 码,这样只要对文档中的某一个汉字一转码,就能立刻找到以此汉字开头的全部违禁词,算是一种散列吧;
/** * * * @see{getPrefixMatch(char[] charArray)} * @param charArray * 前缀单词 * @param knownStart * 已知的起始位置 * @return 知足前缀条件的第一个单词的位置 */ public int getPrefixMatch(char[] charArray, int knownStart) { int index = Utility.getGB2312Id(charArray[0]); if (index == -1) return -1; char[][] items = wordItem_real[index]; if(items == null){ return -1; //没有以此字开头的违禁词 } int start = knownStart, end = items.length - 1; int mid = (start + end) / 2, cmpResult; // 二分查找法 while (start <= end) { cmpResult = Utility.compareArrayByPrefix(charArray, 1, items[mid], 0); if (cmpResult == 0) { // 获取第一个匹配到的(短的优先) while (mid >= 0 && Utility.compareArrayByPrefix(charArray, 1, items[mid], 0) == 0) mid--; mid++; return mid;// 找到第一个以charArray为前缀的单词 } else if (cmpResult < 0) end = mid - 1; else start = mid + 1; mid = (start + end) / 2; } return -1; }
public static int compareArrayByPrefix(char[] shortArray, int shortIndex, char[] longArray, int longIndex) { // 空数组是全部数组的前缀,不考虑index if (shortArray == null) return 0; else if (longArray == null) return (shortIndex < shortArray.length) ? 1 : 0; int si = shortIndex, li = longIndex; while (si < shortArray.length && li < longArray.length && shortArray[si] == longArray[li]) { si++; li++; } if (si == shortArray.length) { // shortArray 是 longArray的prefix return 0; } else { // 此时不可能si>shortArray.length所以只有si < // shortArray.length,表示si没有到达shortArray末尾 // shortArray没有结束,可是longArray已经结束,所以shortArray > longArray if (li == longArray.length) return 1; else // 此时不可能li>longArray.length所以只有li < longArray.length // 表示shortArray和longArray都没有结束,所以按下一个数的大小判断 return (shortArray[si] > longArray[li]) ? 1 : -1; } }
主要的思路和实现代码都已经讲明了,若你们有更好的过滤违禁词的算法,但愿分享,周末愉快。