可能不止在天朝,絕大多數網站都會須要違禁詞過濾模塊,用於對不雅言論進行屏蔽;因此這個應該算是網站的基礎功能。大概在去年的時候我開發過這個功能,當時用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; } }
主要的思路和實現代碼都已經講明瞭,若你們有更好的過濾違禁詞的算法,但願分享,週末愉快。