如何优雅地过滤敏感词
敏感詞過(guò)濾功能在很多地方都會(huì)用到,理論上在Web應(yīng)用中,只要涉及用戶輸入的地方,都需要進(jìn)行文本校驗(yàn),如:XSS校驗(yàn)、SQL注入檢驗(yàn)、敏感詞過(guò)濾等。今天著重講講如何優(yōu)雅高效地實(shí)現(xiàn)敏感詞過(guò)濾。
敏感詞過(guò)濾方案一
先講講筆者在上家公司是如何實(shí)現(xiàn)敏感詞過(guò)濾的。當(dāng)時(shí)畢竟還年輕,所以使用的是最簡(jiǎn)單的過(guò)濾方案。簡(jiǎn)單來(lái)說(shuō)就是對(duì)于要進(jìn)行檢測(cè)的文本,遍歷所有敏感詞,逐個(gè)檢測(cè)輸入的文本中是否含有指定的敏感詞。這種方式是最簡(jiǎn)單的敏感詞過(guò)濾方案了,實(shí)現(xiàn)起來(lái)不難,示例代碼如下:
@Testpublic void test1(){Set<String> sensitiveWords=new HashSet<>();sensitiveWords.add("shit");sensitiveWords.add("傻逼");sensitiveWords.add("笨蛋");String text="你是傻逼啊";for(String sensitiveWord:sensitiveWords){if(text.contains(sensitiveWord)){System.out.println("輸入的文本存在敏感詞。——"+sensitiveWord);break;}}}代碼十分簡(jiǎn)單,也確實(shí)能夠滿足要求。但是這個(gè)方案有一個(gè)很大的問(wèn)題是,隨著敏感詞數(shù)量的增多,敏感詞檢測(cè)的時(shí)間會(huì)呈線性增長(zhǎng)。由于之前的項(xiàng)目的敏感詞數(shù)量只有幾十個(gè),所以使用這種方案不會(huì)存在太大的性能問(wèn)題。但是如果項(xiàng)目中有成千上萬(wàn)個(gè)敏感詞,使用這種方案就會(huì)很耗CPU了。
敏感詞過(guò)濾方案二
在網(wǎng)上查了下敏感詞過(guò)濾方案,找到了一種名為DFA的算法,即Deterministic Finite Automaton算法,翻譯成中文就是確定有窮自動(dòng)機(jī)算法。它的基本思想是基于狀態(tài)轉(zhuǎn)移來(lái)檢索敏感詞,只需要掃描一次待檢測(cè)文本,就能對(duì)所有敏感詞進(jìn)行檢測(cè),所以效率比方案一高不少。
假設(shè)我們有以下5個(gè)敏感詞需要檢測(cè):傻逼、傻子、傻大個(gè)、壞蛋、壞人。那么我們可以先把敏感詞中有相同前綴的詞組合成一個(gè)樹形結(jié)構(gòu),不同前綴的詞分屬不同樹形分支,以上述5個(gè)敏感詞為例,可以初始化成如下2棵樹:
?
把敏感詞組成成樹形結(jié)構(gòu)有什么好處呢?最大的好處就是可以減少檢索次數(shù),我們只需要遍歷一次待檢測(cè)文本,然后在敏感詞庫(kù)中檢索出有沒(méi)有該字符對(duì)應(yīng)的子樹就行了,如果沒(méi)有相應(yīng)的子樹,說(shuō)明當(dāng)前檢測(cè)的字符不在敏感詞庫(kù)中,則直接跳過(guò)繼續(xù)檢測(cè)下一個(gè)字符;如果有相應(yīng)的子樹,則接著檢查下一個(gè)字符是不是前一個(gè)字符對(duì)應(yīng)的子樹的子節(jié)點(diǎn),這樣迭代下去,就能找出待檢測(cè)文本中是否包含敏感詞了。
我們以文本“你是不是傻逼”為例,我們依次檢測(cè)每個(gè)字符,因?yàn)榍?個(gè)字符都不在敏感詞庫(kù)里,找不到相應(yīng)的子樹,所以直接跳過(guò)。當(dāng)檢測(cè)到“傻”字時(shí),發(fā)現(xiàn)敏感詞庫(kù)中有相應(yīng)的子樹,我們把他記為tree-1,接著再搜索下一個(gè)字符“逼”是不是子樹tree-1的子節(jié)點(diǎn),發(fā)現(xiàn)恰好是,接下來(lái)再判斷“逼”這個(gè)字符是不是葉子節(jié)點(diǎn),如果是,則說(shuō)明匹配到了一個(gè)敏感詞了,在這里“逼”這個(gè)字符剛好是tree-1的葉子節(jié)點(diǎn),所以成功檢索到了敏感詞:“傻逼”。大家發(fā)現(xiàn)了沒(méi)有,在我們的搜索過(guò)程中,我們只需要掃描一次被檢測(cè)文本就行了,而且對(duì)于被檢測(cè)文本中不存在的敏感詞,如這個(gè)例子中的“壞蛋”和“壞人”,我們完全不會(huì)掃描到,因此相比方案一效率大大提升了。
在Java中,我們可以用HashMap來(lái)存儲(chǔ)上述的樹形結(jié)構(gòu),還是以上述敏感詞為例,我們把每個(gè)敏感詞字符串拆散成字符,再存儲(chǔ)到HashMap中,可以這樣存:
{"傻": {"逼": {"isEnd": "Y"},"子": {"isEnd": "Y"},"大": {"個(gè)": {"isEnd": "Y"}}} }首先將每個(gè)詞的第一個(gè)字符作為key,value則是另一個(gè)HashMap,value對(duì)應(yīng)的HashMap的key為第二個(gè)字符,如果還有第三個(gè)字符,則存儲(chǔ)到以第二個(gè)字符為key的value中,當(dāng)然這個(gè)value還是一個(gè)HashMap,以此類推下去,直到最后一個(gè)字符,當(dāng)然最后一個(gè)字符對(duì)應(yīng)的value也是HashMap,只不過(guò)這個(gè)HashMap只需要存儲(chǔ)一個(gè)結(jié)束標(biāo)志就行了,像上述的例子中,我們就存了一個(gè){"isEnd","Y"}的HashMap,來(lái)表示這個(gè)value對(duì)應(yīng)的key是敏感詞的最后一個(gè)字符。
同理,“壞人”和“壞蛋”這2個(gè)敏感詞也是按這樣的方式存儲(chǔ)起來(lái),這里就不羅列出來(lái)了。
用HashMap存儲(chǔ)有什么好處呢?我們知道HashMap在理想情況下可以以O(shè)(1)的時(shí)間復(fù)雜度進(jìn)行查詢,所以我們?cè)诒闅v待檢測(cè)字符串的過(guò)程中,可以以O(shè)(1)的時(shí)間復(fù)雜度檢索出當(dāng)前字符是否在敏感詞庫(kù)中,效率比方案一提升太多了。
接下來(lái)上代碼。
首先是初始化敏感詞庫(kù):
private Map<Object,Object> sensitiveWordsMap;private static final String END_FLAG="end";private void initSensitiveWordsMap(Set<String> sensitiveWords){if(sensitiveWords==null||sensitiveWords.isEmpty()){throw new IllegalArgumentException("Senditive words must not be empty!");}sensitiveWordsMap=new HashMap<>(sensitiveWords.size());String currentWord;Map<Object,Object> currentMap;Map<Object,Object> subMap;Iterator<String> iterator = sensitiveWords.iterator();while (iterator.hasNext()){currentWord=iterator.next();if(currentWord==null||currentWord.trim().length()<2){ //敏感詞長(zhǎng)度必須大于等于2continue;}currentMap=sensitiveWordsMap;for(int i=0;i<currentWord.length();i++){char c=currentWord.charAt(i);subMap=(Map<Object, Object>) currentMap.get(c);if(subMap==null){subMap=new HashMap<>();currentMap.put(c,subMap);currentMap=subMap;}else {currentMap= subMap;}if(i==currentWord.length()-1){//如果是最后一個(gè)字符,則put一個(gè)結(jié)束標(biāo)志,這里只需要保存key就行了,value為null可以節(jié)省空間。//如果不是最后一個(gè)字符,則不需要存這個(gè)結(jié)束標(biāo)志,同樣也是為了節(jié)省空間。currentMap.put(END_FLAG,null);}}}}代碼的邏輯上面已經(jīng)說(shuō)過(guò)了,就是循環(huán)敏感詞集合,將他們放到HashMap中,這里不再贅述。
接下來(lái)是敏感詞的掃描:
public enum MatchType {MIN_MATCH("最小匹配規(guī)則"),MAX_MATCH("最大匹配規(guī)則");String desc;MatchType(String desc) {this.desc = desc;} }public Set<String> getSensitiveWords(String text,MatchType matchType){if(text==null||text.trim().length()==0){throw new IllegalArgumentException("The input text must not be empty.");}Set<String> sensitiveWords=new HashSet<>();for(int i=0;i<text.length();i++){int sensitiveWordLength = getSensitiveWordLength(text, i, matchType);if(sensitiveWordLength>0){String sensitiveWord = text.substring(i, i + sensitiveWordLength);sensitiveWords.add(sensitiveWord);if(matchType==MatchType.MIN_MATCH){break;}i=i+sensitiveWordLength-1;}}return sensitiveWords;}public int getSensitiveWordLength(String text,int startIndex,MatchType matchType){if(text==null||text.trim().length()==0){throw new IllegalArgumentException("The input text must not be empty.");}char currentChar;Map<Object,Object> currentMap=sensitiveWordsMap;int wordLength=0;boolean endFlag=false;for(int i=startIndex;i<text.length();i++){currentChar=text.charAt(i);Map<Object,Object> subMap=(Map<Object,Object>) currentMap.get(currentChar);if(subMap==null){break;}else {wordLength++;if(subMap.containsKey(END_FLAG)){endFlag=true;if(matchType==MatchType.MIN_MATCH){break;}else {currentMap=subMap;}}else {currentMap=subMap;}}}if(!endFlag){wordLength=0;}return wordLength;}其中,MatchType表示匹配規(guī)則,有時(shí)候我們只需要找到一個(gè)敏感詞就可以了,有時(shí)候則需要知道待檢測(cè)文本中到底包含多少個(gè)敏感詞,前者對(duì)應(yīng)的是最小匹配原則,后者則是最大匹配原則。
getSensitiveWordLength方法的作用是根據(jù)給定的待檢測(cè)文本及起始下標(biāo),還有匹配規(guī)則,計(jì)算出待檢測(cè)文本中的敏感詞長(zhǎng)度,如果不存在,則返回0,存在則返回匹配到的敏感詞長(zhǎng)度。
getSensitiveWords方法則是掃描一遍待檢測(cè)文本,逐個(gè)檢測(cè)每個(gè)字符是否在敏感詞庫(kù)中,然后將檢測(cè)到的敏感詞截取出來(lái)放到集合中返回給客戶端。
最后寫個(gè)測(cè)試用例測(cè)一下:
public static void main(String[] args) {Set<String> sensitiveWords=new HashSet<>();sensitiveWords.add("你是傻逼");sensitiveWords.add("你是傻逼啊");sensitiveWords.add("你是壞蛋");sensitiveWords.add("你個(gè)大笨蛋");sensitiveWords.add("我去年買了個(gè)表");sensitiveWords.add("shit");TextFilter textFilter=new TextFilter();textFilter.initSensitiveWordsMap(sensitiveWords);String text="你你你你是傻逼啊你,說(shuō)你呢,你個(gè)大笨蛋。";System.out.println(textFilter.getSensitiveWords(text,MatchType.MAX_MATCH));}結(jié)果輸出如下:
可以看到,我們成功地過(guò)濾出了敏感詞。
敏感詞過(guò)濾方案三
方案二在性能上已經(jīng)可以滿足需求了,但是卻很容易被破解,比如說(shuō),我在待檢測(cè)文本中的敏感詞中間加個(gè)空格,就可以成功繞過(guò)了。要解決這個(gè)問(wèn)題也不難,有一個(gè)簡(jiǎn)單的方法是初始化一個(gè)無(wú)效字符庫(kù),比如:空格、*、#、@等字符,然后在檢測(cè)文本前,先將待檢測(cè)文本中的無(wú)效字符去除,這樣的話被檢測(cè)字符就不存在這些無(wú)效字符了,因此還是可以繼續(xù)用方案二進(jìn)行過(guò)濾。只要被檢測(cè)文本不要太長(zhǎng),那么我們只要在方案二的基礎(chǔ)上再多掃描一次被檢測(cè)文本去除無(wú)效字符就行了,這個(gè)性能損耗也還是可以接受的。
如果敏感詞是英文,則還要考慮大小寫的問(wèn)題。有一個(gè)比較簡(jiǎn)單的解決方案是在初始化敏感詞時(shí),將敏感詞都以小寫形式存儲(chǔ)。同時(shí),在檢測(cè)文本時(shí),也統(tǒng)一將待檢測(cè)文本轉(zhuǎn)化為小寫,這樣就能解決大小寫的問(wèn)題了。
比較棘手的是中文跟拼音混合的情況,比如“傻逼”這個(gè)敏感詞,可以通過(guò)“sha逼”這種中文跟拼音混合的方式輕松繞過(guò),對(duì)于這種情況我目前還沒(méi)想到比較好的解決方案,有想法的讀者可以在文末留言。
完整代碼:https://github.com/hzjjames/TextFilter?
?
總結(jié)
以上是生活随笔為你收集整理的如何优雅地过滤敏感词的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 视觉统计计数方案
- 下一篇: 弯管机编程软件电脑版_乐高Wedo2.0