作者 Eluli Simpray

v1.0.2版本

1 -# 高效敏感词过滤 1 +# 快速敏感词过滤
2 2
3 3
4 -## 性能概述 4 +### 性能概述
5 5
6 -在共60M穿越小说上测试,单核性能为80M字符每秒(i7 2.3GHz)。  
7 -相比类似原理的正向最大匹配分词,性能一般在1M字节每秒左右有很大提升,类似的优化方式可以用在分词器上。 6 +使用60MB大小的小说测试,单核性能超过50M字符每秒(i7 2.3GHz)。
8 7
9 ``` 8 ```
10 -敏感词 14580 条  
11 -共加载 599254 行,30613005 字符。  
12 -共耗时 0.381 秒, 速度为 80349.1字符/毫秒 9 +敏感词 14553 条
  10 +待过滤文本共 599254 行,30613005 字符。
  11 +过滤耗时 0.535 秒, 速度为 57220.6字符/毫秒
  12 +其中 39691 行有替换
13 ``` 13 ```
14 14
15 -## 优化方式 15 +### 优化方式
16 16
17 主要的优化目标是速度,从以下方面优化: 17 主要的优化目标是速度,从以下方面优化:
18 18
@@ -22,471 +22,37 @@ @@ -22,471 +22,37 @@
22 4. StringPointer,在不生成新实例的情况下计算任意位置2个字符的hash和mix 22 4. StringPointer,在不生成新实例的情况下计算任意位置2个字符的hash和mix
23 5. StringPointer,尽量减少实例生成和char数组的拷贝。 23 5. StringPointer,尽量减少实例生成和char数组的拷贝。
24 24
25 -## 敏感词库  
26 -  
27 -自带敏感词库拷贝自 https://github.com/observerss/textfilter ,并删除如`女人`、`然后`这样的几个常用词。  
28 -如果需要自带敏感词的实例,可以直接使用下面的方式: 25 +### 敏感词库
29 26
  27 +默认敏感词库拷贝自 https://github.com/observerss/textfilter ,并删除如`女人`、`然后`这样的几个常用词。
  28 +使用默认敏感词库的示例如下
30 29
31 ```java 30 ```java
32 -// 使用默认的单例(即加载了自带敏感词库的 31 +// 使用默认单例(加载默认敏感词库
33 SensitiveFilter filter = SensitiveFilter.DEFAULT; 32 SensitiveFilter filter = SensitiveFilter.DEFAULT;
34 -// 对一个句子过滤  
35 -System.out.println(filter.filter("会上,主席进行了发言。", '*')); 33 +// 向过滤器增加一个词
  34 +filter.put("婚礼上唱春天在哪里");
  35 +
  36 +// 待过滤的句子
  37 +String sentence = "然后,市长在婚礼上唱春天在哪里。";
  38 +// 进行过滤
  39 +String filted = filter.filter(sentence, '*');
  40 +
  41 +// 如果未过滤,则返回输入的String引用
  42 +if(sentence != filted){
  43 + // 句子中有敏感词
  44 + System.out.println(filted);
  45 +}
36 ``` 46 ```
37 47
38 打印结果 48 打印结果
39 49
40 ``` 50 ```
41 -会上,**进行了发言。  
42 -```  
43 -  
44 -## 代码只有3个类直接贴上  
45 -  
46 -  
47 -### SensitiveFilter.java  
48 -```java  
49 -package com.odianyun.util.sensi;  
50 -  
51 -import java.io.BufferedReader;  
52 -import java.io.IOException;  
53 -import java.io.InputStreamReader;  
54 -import java.io.Serializable;  
55 -import java.nio.charset.StandardCharsets;  
56 -  
57 -/**  
58 - * 敏感词过滤器,以过滤速度优化为主。<br/>  
59 - * * 增加一个敏感词:{@link #put(String)} <br/>  
60 - * * 过滤一个句子:{@link #filter(String, char)} <br/>  
61 - * * 获取默认的单例:{@link #DEFAULT}  
62 - *  
63 - * @author ZhangXiaoye  
64 - * @date 2017年1月5日 下午4:18:38  
65 - */  
66 -public class SensitiveFilter implements Serializable{  
67 -  
68 - private static final long serialVersionUID = 1L;  
69 -  
70 - /**  
71 - * 默认的单例,使用自带的敏感词库  
72 - */  
73 - public static final SensitiveFilter DEFAULT = new SensitiveFilter(  
74 - new BufferedReader(new InputStreamReader(  
75 - ClassLoader.getSystemResourceAsStream("sensi_words.txt")  
76 - , StandardCharsets.UTF_8)));  
77 -  
78 - /**  
79 - * 为2的n次方,考虑到敏感词大概在10k左右,  
80 - * 这个数量应为词数的数倍,使得桶很稀疏  
81 - * 提高不命中时hash指向null的概率,  
82 - * 加快访问速度。  
83 - */  
84 - static final int DEFAULT_INITIAL_CAPACITY = 131072;  
85 -  
86 - /**  
87 - * 类似HashMap的桶,比较稀疏。  
88 - * 使用2个字符的hash定位。  
89 - */  
90 - protected SensitiveNode[] nodes = new SensitiveNode[DEFAULT_INITIAL_CAPACITY];  
91 -  
92 - /**  
93 - * 构建一个空的filter  
94 - *  
95 - * @author ZhangXiaoye  
96 - * @date 2017年1月5日 下午4:18:07  
97 - */  
98 - public SensitiveFilter(){  
99 -  
100 - }  
101 -  
102 - /**  
103 - * 加载一个文件中的词典,并构建filter<br/>  
104 - * 文件中,每行一个敏感词条<br/>  
105 - * <b>注意:</b>读取完成后会调用{@link BufferedReader#close()}方法。<br/>  
106 - * <b>注意:</b>读取中的{@link IOException}不会抛出  
107 - *  
108 - * @param reader  
109 - * @author ZhangXiaoye  
110 - * @date 2017年1月5日 下午4:21:06  
111 - */  
112 - public SensitiveFilter(BufferedReader reader){  
113 - try{  
114 - for(String line = reader.readLine(); line != null; line = reader.readLine()){  
115 - put(line);  
116 - }  
117 - reader.close();  
118 - }catch(IOException e){  
119 - e.printStackTrace();  
120 - }  
121 - }  
122 -  
123 - /**  
124 - * 增加一个敏感词,如果词的长度(trim后)小于2,则丢弃<br/>  
125 - * 此方法(构建)并不是主要的性能优化点。  
126 - *  
127 - * @param word  
128 - * @author ZhangXiaoye  
129 - * @date 2017年1月5日 下午2:35:21  
130 - */  
131 - public void put(String word){  
132 - if(word == null || word.trim().length() < 2){  
133 - return;  
134 - }  
135 - StringPointer sp = new StringPointer(word.trim());  
136 - // 计算头两个字符的hash  
137 - int hash = sp.nextTwoCharHash(0);  
138 - // 计算头两个字符的mix表示(mix相同,两个字符相同)  
139 - int mix = sp.nextTwoCharMix(0);  
140 - // 转为在hash桶中的位置  
141 - int index = hash & (nodes.length - 1);  
142 -  
143 - // 从桶里拿第一个节点  
144 - SensitiveNode node = nodes[index];  
145 - if(node == null){  
146 - // 如果没有节点,则放进去一个  
147 - node = new SensitiveNode(mix);  
148 - // 并添加词  
149 - node.words.add(sp);  
150 - // 放入桶里  
151 - nodes[index] = node;  
152 - }else{  
153 - // 如果已经有节点(1个或多个),找到正确的节点  
154 - for(;node != null; node = node.next){  
155 - // 匹配节点  
156 - if(node.headTwoCharMix == mix){  
157 - node.words.add(sp);  
158 - return;  
159 - }  
160 - // 如果匹配到最后仍然不成功,则追加一个节点  
161 - if(node.next == null){  
162 - new SensitiveNode(mix, node).words.add(sp);  
163 - return;  
164 - }  
165 - }  
166 - }  
167 - }  
168 -  
169 - /**  
170 - * 对句子进行敏感词过滤  
171 - *  
172 - * @param sentence 句子  
173 - * @param replace 敏感词的替换字符  
174 - * @return 过滤后的句子  
175 - * @author ZhangXiaoye  
176 - * @date 2017年1月5日 下午4:16:31  
177 - */  
178 - public String filter(String sentence, char replace){  
179 - // 先转换为StringPointer  
180 - StringPointer sp = new StringPointer(sentence);  
181 -  
182 - // 标示是否替换  
183 - boolean replaced = false;  
184 -  
185 - // 匹配的起始位置  
186 - int i = 0;  
187 - while(i < sp.length - 2){  
188 - /*  
189 - * 移动到下一个匹配位置的步进:  
190 - * 如果未匹配为1,如果匹配是匹配的词长度  
191 - */  
192 - int step = 1;  
193 - // 计算此位置开始2个字符的hash  
194 - int hash = sp.nextTwoCharHash(i);  
195 - /*  
196 - * 根据hash获取第一个节点,  
197 - * 真正匹配的节点可能不是第一个,  
198 - * 所以有后面的for循环。  
199 - */  
200 - SensitiveNode node = nodes[hash & (nodes.length - 1)];  
201 - /*  
202 - * 如果非敏感词,node基本为null。  
203 - * 这一步大幅提升效率  
204 - */  
205 - if(node != null){  
206 - /*  
207 - * 如果能拿到第一个节点,  
208 - * 才计算mix(mix相同表示2个字符相同)。  
209 - * mix的意义和HashMap先hash再equals的equals部分类似。  
210 - */  
211 - int mix = sp.nextTwoCharMix(i);  
212 - /*  
213 - * 循环所有的节点,如果非敏感词,  
214 - * mix相同的概率非常低,提高效率  
215 - */  
216 - for(; node != null; node = node.next){  
217 - /*  
218 - * 对于一个节点,先根据头2个字符判断是否属于这个节点。  
219 - * 如果属于这个节点,看这个节点的词库是否命中。  
220 - * 此代码块中访问次数已经很少,不是优化重点  
221 - */  
222 - if(node.headTwoCharMix == mix){  
223 - /*  
224 - * 查出比剩余sentence小的最大的词。  
225 - * 例如剩余sentence为"色情电影哪家强?",  
226 - * 这个节点含三个词从小到大为:“色情”、“色情电影”、“色情信息”。  
227 - * 则取到的word为“色情电影”  
228 - */  
229 - StringPointer word = node.words.floor(sp.substring(i));  
230 - /*  
231 - * 仍然需要再判断一次,例如“色情信息哪里有?”,  
232 - * 如果节点只包含“色情电影”一个词,  
233 - * 仍然能够取到word为“色情电影”,但是不该匹配。  
234 - */  
235 - if(word != null && sp.nextStartsWith(i, word)){  
236 - // 匹配成功,将匹配的部分,用replace制定的内容替代  
237 - sp.fill(i, i + word.length, replace);  
238 - // 跳过已经替代的部分  
239 - step = word.length;  
240 - // 标示有替换  
241 - replaced = true;  
242 - // 跳出for循环(然后是while循环的下一个位置)  
243 - break;  
244 - }  
245 - }  
246 - }  
247 - }  
248 -  
249 - // 移动到下一个匹配位置  
250 - i += step;  
251 - }  
252 -  
253 - // 如果没有替换,直接返回入参(节约String的构造copy)  
254 - if(replaced){  
255 - return sp.toString();  
256 - }else{  
257 - return sentence;  
258 - }  
259 - }  
260 -  
261 -}  
262 -```  
263 -  
264 -### SensitiveNode.java  
265 -  
266 -  
267 -```java  
268 -package com.odianyun.util.sensi;  
269 -  
270 -import java.io.Serializable;  
271 -import java.util.TreeSet;  
272 -  
273 -/**  
274 - * 敏感词节点,每个节点包含了以相同的2个字符开头的所有词  
275 - *  
276 - * @author ZhangXiaoye  
277 - * @date 2017年1月5日 下午5:06:26  
278 - */  
279 -public class SensitiveNode implements Serializable{  
280 -  
281 - private static final long serialVersionUID = 1L;  
282 -  
283 - /**  
284 - * 头两个字符的mix,mix相同,两个字符相同  
285 - */  
286 - protected final int headTwoCharMix;  
287 -  
288 - /**  
289 - * 所有以这两个字符开头的词表  
290 - */  
291 - protected final TreeSet<StringPointer> words = new TreeSet<StringPointer>();  
292 -  
293 - /**  
294 - * 下一个节点  
295 - */  
296 - protected SensitiveNode next;  
297 -  
298 - public SensitiveNode(int headTwoCharMix){  
299 - this.headTwoCharMix = headTwoCharMix;  
300 - }  
301 -  
302 - public SensitiveNode(int headTwoCharMix, SensitiveNode parent){  
303 - this.headTwoCharMix = headTwoCharMix;  
304 - parent.next = this;  
305 - }  
306 -  
307 -} 51 +然后,**在*********。
308 ``` 52 ```
309 53
310 -### StringPointer.java  
311 -  
312 -```java  
313 -package com.odianyun.util.sensi;  
314 -  
315 -import java.io.Serializable;  
316 -import java.util.HashMap;  
317 -import java.util.TreeMap;  
318 -  
319 -/**  
320 - * 没有注释的方法与{@link String}类似<br/>  
321 - * <b>注意:</b>没有(数组越界等的)安全检查<br/>  
322 - * 可以作为{@link HashMap}和{@link TreeMap}的key  
323 - *  
324 - * @author ZhangXiaoye  
325 - * @date 2017年1月5日 下午2:11:56  
326 - */  
327 -public class StringPointer implements Serializable, CharSequence, Comparable<StringPointer>{  
328 -  
329 - private static final long serialVersionUID = 1L;  
330 -  
331 - protected final char[] value;  
332 -  
333 - protected final int offset;  
334 -  
335 - protected final int length;  
336 -  
337 - private int hash = 0;  
338 -  
339 - public StringPointer(String str){  
340 - value = str.toCharArray();  
341 - offset = 0;  
342 - length = value.length;  
343 - } 54 +### 依赖
344 55
345 - public StringPointer(char[] value, int offset, int length){  
346 - this.value = value;  
347 - this.offset = offset;  
348 - this.length = length;  
349 - } 56 +JDK 1.7版本及以上
350 57
351 - /**  
352 - * 计算该位置后(包含)2个字符的hash值  
353 - *  
354 - * @param i 从 0 到 length - 2  
355 - * @return hash值  
356 - * @author ZhangXiaoye  
357 - * @date 2017年1月5日 下午2:23:02  
358 - */  
359 - public int nextTwoCharHash(int i){  
360 - return 31 * value[offset + i] + value[offset + i + 1];  
361 - }  
362 -  
363 - /**  
364 - * 计算该位置后(包含)2个字符和为1个int型的值<br/>  
365 - * int值相同表示2个字符相同  
366 - *  
367 - * @param i 从 0 到 length - 2  
368 - * @return int值  
369 - * @author ZhangXiaoye  
370 - * @date 2017年1月5日 下午2:46:58  
371 - */  
372 - public int nextTwoCharMix(int i){  
373 - return (value[offset + i] << 16) | value[offset + i + 1];  
374 - }  
375 -  
376 - /**  
377 - * 该位置后(包含)的字符串,是否以某个词(word)开头  
378 - *  
379 - * @param i 从 0 到 length - 2  
380 - * @param word 词  
381 - * @return 是否?  
382 - * @author ZhangXiaoye  
383 - * @date 2017年1月5日 下午3:13:49  
384 - */  
385 - public boolean nextStartsWith(int i, StringPointer word){  
386 - // 是否长度超出  
387 - if(word.length > length - i){  
388 - return false;  
389 - }  
390 - // 从尾开始判断  
391 - for(int c = word.length - 1; c >= 0; c --){  
392 - if(value[offset + i + c] != word.value[word.offset + c]){  
393 - return false;  
394 - }  
395 - }  
396 - return true;  
397 - }  
398 -  
399 - /**  
400 - * 填充(替换)  
401 - *  
402 - * @param begin 从此位置开始(含)  
403 - * @param end 到此位置结束(不含)  
404 - * @param fillWith 以此字符填充(替换)  
405 - * @author ZhangXiaoye  
406 - * @date 2017年1月5日 下午3:29:21  
407 - */  
408 - public void fill(int begin, int end, char fillWith){  
409 - for(int i = begin; i < end; i ++){  
410 - value[offset + i] = fillWith;  
411 - }  
412 - }  
413 -  
414 - public int length(){  
415 - return length;  
416 - }  
417 -  
418 - public char charAt(int i){  
419 - return value[offset + i];  
420 - }  
421 -  
422 - public StringPointer substring(int begin){  
423 - return new StringPointer(value, offset + begin, length - begin);  
424 - }  
425 -  
426 - public StringPointer substring(int begin, int end){  
427 - return new StringPointer(value, offset + begin, end - begin);  
428 - }  
429 -  
430 - @Override  
431 - public CharSequence subSequence(int start, int end) {  
432 - return substring(start, end);  
433 - }  
434 -  
435 - public String toString(){  
436 - return new String(value, offset, length);  
437 - }  
438 -  
439 - public int hashCode() {  
440 - int h = hash;  
441 - if (h == 0 && length > 0) {  
442 - for (int i = 0; i < length; i++) {  
443 - h = 31 * h + value[offset + i];  
444 - }  
445 - hash = h;  
446 - }  
447 - return h;  
448 - }  
449 -  
450 - public boolean equals(Object anObject) {  
451 - if (this == anObject) {  
452 - return true;  
453 - }  
454 - if (anObject instanceof StringPointer) {  
455 - StringPointer that = (StringPointer)anObject;  
456 - if (length == that.length) {  
457 - char v1[] = this.value;  
458 - char v2[] = that.value;  
459 - for(int i = 0; i < this.length; i ++){  
460 - if(v1[this.offset + i] != v2[that.offset + i]){  
461 - return false;  
462 - }  
463 - }  
464 - return true;  
465 - }  
466 - }  
467 - return false;  
468 - }  
469 -  
470 - @Override  
471 - public int compareTo(StringPointer that) {  
472 - int len1 = this.length;  
473 - int len2 = that.length;  
474 - int lim = Math.min(len1, len2);  
475 - char v1[] = this.value;  
476 - char v2[] = that.value;  
477 -  
478 - int k = 0;  
479 - while (k < lim) {  
480 - char c1 = v1[this.offset + k];  
481 - char c2 = v2[that.offset + k];  
482 - if (c1 != c2) {  
483 - return c1 - c2;  
484 - }  
485 - k++;  
486 - }  
487 - return len1 - len2;  
488 - }  
489 -  
490 -}  
491 -```  
492 58
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 4
5 <groupId>com.odianyun.util</groupId> 5 <groupId>com.odianyun.util</groupId>
6 <artifactId>sensitive-words</artifactId> 6 <artifactId>sensitive-words</artifactId>
7 - <version>1.0.1</version> 7 + <version>1.0.2</version>
8 <packaging>jar</packaging> 8 <packaging>jar</packaging>
9 9
10 <name>sensitive-words</name> 10 <name>sensitive-words</name>
@@ -5,6 +5,7 @@ import java.io.IOException; @@ -5,6 +5,7 @@ import java.io.IOException;
5 import java.io.InputStreamReader; 5 import java.io.InputStreamReader;
6 import java.io.Serializable; 6 import java.io.Serializable;
7 import java.nio.charset.StandardCharsets; 7 import java.nio.charset.StandardCharsets;
  8 +import java.util.NavigableSet;
8 9
9 /** 10 /**
10 * 敏感词过滤器,以过滤速度优化为主。<br/> 11 * 敏感词过滤器,以过滤速度优化为主。<br/>
@@ -80,9 +81,14 @@ public class SensitiveFilter implements Serializable{ @@ -80,9 +81,14 @@ public class SensitiveFilter implements Serializable{
80 * @author ZhangXiaoye 81 * @author ZhangXiaoye
81 * @date 2017年1月5日 下午2:35:21 82 * @date 2017年1月5日 下午2:35:21
82 */ 83 */
83 - public void put(String word){ 84 + public boolean put(String word){
  85 + // 长度小于2的不加入
84 if(word == null || word.trim().length() < 2){ 86 if(word == null || word.trim().length() < 2){
85 - return; 87 + return false;
  88 + }
  89 + // 两个字符的不考虑
  90 + if(word.length() == 2 && word.matches("\\w\\w")){
  91 + return false;
86 } 92 }
87 StringPointer sp = new StringPointer(word.trim()); 93 StringPointer sp = new StringPointer(word.trim());
88 // 计算头两个字符的hash 94 // 计算头两个字符的hash
@@ -107,19 +113,26 @@ public class SensitiveFilter implements Serializable{ @@ -107,19 +113,26 @@ public class SensitiveFilter implements Serializable{
107 // 匹配节点 113 // 匹配节点
108 if(node.headTwoCharMix == mix){ 114 if(node.headTwoCharMix == mix){
109 node.words.add(sp); 115 node.words.add(sp);
110 - return; 116 + return true;
111 } 117 }
112 // 如果匹配到最后仍然不成功,则追加一个节点 118 // 如果匹配到最后仍然不成功,则追加一个节点
113 if(node.next == null){ 119 if(node.next == null){
114 new SensitiveNode(mix, node).words.add(sp); 120 new SensitiveNode(mix, node).words.add(sp);
115 - return; 121 + return true;
116 } 122 }
117 } 123 }
118 } 124 }
  125 + return true;
119 } 126 }
120 127
121 /** 128 /**
122 - * 对句子进行敏感词过滤 129 + * 对句子进行敏感词过滤<br/>
  130 + * 如果无敏感词返回输入的sentence对象,即可以用下面的方式判断是否有敏感词:<br/><code>
  131 + * String result = filter.filter(sentence, '*');<br/>
  132 + * if(result != sentence){<br/>
  133 + * &nbsp;&nbsp;// 有敏感词<br/>
  134 + * }
  135 + * </code>
123 * 136 *
124 * @param sentence 句子 137 * @param sentence 句子
125 * @param replace 敏感词的替换字符 138 * @param replace 敏感词的替换字符
@@ -165,6 +178,7 @@ public class SensitiveFilter implements Serializable{ @@ -165,6 +178,7 @@ public class SensitiveFilter implements Serializable{
165 * 循环所有的节点,如果非敏感词, 178 * 循环所有的节点,如果非敏感词,
166 * mix相同的概率非常低,提高效率 179 * mix相同的概率非常低,提高效率
167 */ 180 */
  181 + outer:
168 for(; node != null; node = node.next){ 182 for(; node != null; node = node.next){
169 /* 183 /*
170 * 对于一个节点,先根据头2个字符判断是否属于这个节点。 184 * 对于一个节点,先根据头2个字符判断是否属于这个节点。
@@ -175,26 +189,31 @@ public class SensitiveFilter implements Serializable{ @@ -175,26 +189,31 @@ public class SensitiveFilter implements Serializable{
175 /* 189 /*
176 * 查出比剩余sentence小的最大的词。 190 * 查出比剩余sentence小的最大的词。
177 * 例如剩余sentence为"色情电影哪家强?", 191 * 例如剩余sentence为"色情电影哪家强?",
178 - * 这个节点含三个词从小到大为:“色情”、“色情电影”、“色情信息”。  
179 - * 则取到的word为“色情电影” 192 + * 这个节点含三个词从小到大为:"色情"、"色情电影"、"色情信息"。
  193 + * 则从“色情电影”开始向前匹配
180 */ 194 */
181 - StringPointer word = node.words.floor(sp.substring(i)); 195 + NavigableSet<StringPointer> desSet = node.words.headSet(sp.substring(i), true);
  196 + if(desSet != null){
  197 + for(StringPointer word: desSet){
182 /* 198 /*
183 - * 仍然需要再判断一次,例如“色情信息哪里有?”,  
184 - * 如果节点只包含“色情电影”一个词,  
185 - * 仍然能够取到word为“色情电影”,但是不该匹配。 199 + * 仍然需要再判断一次,例如"色情信息哪里有?",
  200 + * 如果节点只包含"色情电影"一个词,
  201 + * 仍然能够取到word为"色情电影",但是不该匹配。
186 */ 202 */
187 - if(word != null && sp.nextStartsWith(i, word)){ 203 + if(sp.nextStartsWith(i, word)){
188 // 匹配成功,将匹配的部分,用replace制定的内容替代 204 // 匹配成功,将匹配的部分,用replace制定的内容替代
189 sp.fill(i, i + word.length, replace); 205 sp.fill(i, i + word.length, replace);
190 // 跳过已经替代的部分 206 // 跳过已经替代的部分
191 step = word.length; 207 step = word.length;
192 // 标示有替换 208 // 标示有替换
193 replaced = true; 209 replaced = true;
194 - // 跳出for循环(然后是while循环的下一个位置)  
195 - break; 210 + // 跳出循环(然后是while循环的下一个位置)
  211 + break outer;
  212 + }
196 } 213 }
197 } 214 }
  215 +
  216 + }
198 } 217 }
199 } 218 }
200 219
@@ -529,7 +529,6 @@ pussy @@ -529,7 +529,6 @@ pussy
529 遊行 529 遊行
530 偷电 530 偷电
531 龟头粗壮赤红 531 龟头粗壮赤红
532 -管理  
533 淫荡女医生穴 532 淫荡女医生穴
534 官商勾结 533 官商勾结
535 小鸡巴 534 小鸡巴
@@ -1597,7 +1596,6 @@ orgasmus @@ -1597,7 +1596,6 @@ orgasmus
1597 黄蓉舔小龙女的乳头 1596 黄蓉舔小龙女的乳头
1598 色情網站 1597 色情網站
1599 叔嫂肉欲 1598 叔嫂肉欲
1600 -飘飘  
1601 塞白 1599 塞白
1602 眯眯爱大型黄站 1600 眯眯爱大型黄站
1603 妹妹陰毛 1601 妹妹陰毛
@@ -1724,7 +1722,6 @@ sb @@ -1724,7 +1722,6 @@ sb
1724 日本素人 1722 日本素人
1725 午夜激情美穴贴台湾性福联盟 1723 午夜激情美穴贴台湾性福联盟
1726 手记注册日韩5006部激情电影 1724 手记注册日韩5006部激情电影
1727 -ro  
1728 全國人大 1725 全國人大
1729 蘇秀文 1726 蘇秀文
1730 诛仙 1727 诛仙
@@ -2148,7 +2145,6 @@ fuck外专局 @@ -2148,7 +2145,6 @@ fuck外专局
2148 教你怎样延迟射精34岁以下禁免费淫电影a片 2145 教你怎样延迟射精34岁以下禁免费淫电影a片
2149 干他 2146 干他
2150 女幹三虧 2147 女幹三虧
2151 -妹妹  
2152 中国之春 2148 中国之春
2153 超美女优MUGEN EX Vol。 7~浜崎ひめ 2149 超美女优MUGEN EX Vol。 7~浜崎ひめ
2154 transsexuell 2150 transsexuell
@@ -2220,7 +2216,6 @@ fuck私服 @@ -2220,7 +2216,6 @@ fuck私服
2220 g。m 2216 g。m
2221 毛叫小姐 2217 毛叫小姐
2222 meinv嫩穴 妹穴 亮穴 女人穴 2218 meinv嫩穴 妹穴 亮穴 女人穴
2223 -母亲  
2224 北极冰 2219 北极冰
2225 誘姦 2220 誘姦
2226 mabinogi 2221 mabinogi
@@ -2400,7 +2395,6 @@ yifeng @@ -2400,7 +2395,6 @@ yifeng
2400 巨奶 2395 巨奶
2401 好嫩 2396 好嫩
2402 女尔 2397 女尔
2403 -妈妈  
2404 2398
2405 SM援交 2399 SM援交
2406 www。ccwcc。cn 2400 www。ccwcc。cn
@@ -6018,8 +6012,6 @@ meimei的嫩阴户 @@ -6018,8 +6012,6 @@ meimei的嫩阴户
6018 新手輔導員 6012 新手輔導員
6019 Tora Tora Gold Vol。15 香山圣 6013 Tora Tora Gold Vol。15 香山圣
6020 海洛因 6014 海洛因
6021 -杨帆  
6022 -温馨  
6023 毛他母亲 6015 毛他母亲
6024 meinv极品嫩穴 6016 meinv极品嫩穴
6025 龟头顶破了处女膜 6017 龟头顶破了处女膜
@@ -6633,7 +6625,6 @@ cdjp李鹏* @@ -6633,7 +6625,6 @@ cdjp李鹏*
6633 色情 导航 6625 色情 导航
6634 三民主义 6626 三民主义
6635 毛阴茎 6627 毛阴茎
6636 -公告消息  
6637 私¥服 6628 私¥服
6638 毛一国两制 6629 毛一国两制
6639 和两勇男玩15P 6630 和两勇男玩15P
@@ -6713,7 +6704,6 @@ mland @@ -6713,7 +6704,6 @@ mland
6713 你爺 6704 你爺
6714 fuck尉健行 6705 fuck尉健行
6715 迷魂药 6706 迷魂药
6716 -你爸  
6717 mai骚 6707 mai骚
6718 科洛斯 6708 科洛斯
6719 淫色俱乐部 6709 淫色俱乐部
@@ -7240,7 +7230,6 @@ Red Hot Fetish 纹舞らん--スケパン刑事 @@ -7240,7 +7230,6 @@ Red Hot Fetish 纹舞らん--スケパン刑事
7240 援助交易 7230 援助交易
7241 23dy淫色贴图 7231 23dy淫色贴图
7242 吳邦囯 7232 吳邦囯
7243 -爷爷  
7244 自拍 7233 自拍
7245 法~~輪 7234 法~~輪
7246 BlowJobs 7235 BlowJobs
@@ -7259,7 +7248,6 @@ fuck投毒杀人 @@ -7259,7 +7248,6 @@ fuck投毒杀人
7259 激情玩穴网 7248 激情玩穴网
7260 妹妹的嫩阴唇 7249 妹妹的嫩阴唇
7261 butthole 7250 butthole
7262 -爸爸  
7263 毛妳老母的 7251 毛妳老母的
7264 管理员 7252 管理员
7265 金酷 7253 金酷
@@ -8048,7 +8036,6 @@ hardcore @@ -8048,7 +8036,6 @@ hardcore
8048 中功 8036 中功
8049 精品性交图 8037 精品性交图
8050 瑞士金融大学 8038 瑞士金融大学
8051 -系统消息  
8052 邻居美少妇 操淫荡少妇 8039 邻居美少妇 操淫荡少妇
8053 成人色情导航 8040 成人色情导航
8054 完全免费在线淫电影 看完保证你打飞机 8041 完全免费在线淫电影 看完保证你打飞机
@@ -9973,7 +9960,6 @@ tranny @@ -9973,7 +9960,6 @@ tranny
9973 我讓朋友強姦我的小姨子 9960 我讓朋友強姦我的小姨子
9974 大城舞衣子(有碼無碼)大合輯~~~超優!!(可分開下載哦!!!) 9961 大城舞衣子(有碼無碼)大合輯~~~超優!!(可分開下載哦!!!)
9975 操妳娘 9962 操妳娘
9976 -通知  
9977 合成淫色贴图 9963 合成淫色贴图
9978 Qqyinsu 9964 Qqyinsu
9979 fuck晚年周恩来 9965 fuck晚年周恩来
@@ -10319,7 +10305,6 @@ fuck统计局 @@ -10319,7 +10305,6 @@ fuck统计局
10319 欲仙欲死 10305 欲仙欲死
10320 干你媽逼 10306 干你媽逼
10321 淫奸护士穴论坛 10307 淫奸护士穴论坛
10322 -好朋友  
10323 身份生成器 10308 身份生成器
10324 法@@倫 10309 法@@倫
10325 欧洲男女激情淫乱穴 mm贴图网 10310 欧洲男女激情淫乱穴 mm贴图网
@@ -10714,7 +10699,6 @@ qi shi @@ -10714,7 +10699,6 @@ qi shi
10714 新人调教 Red Hot Jam Vol。9 Myuu 10699 新人调教 Red Hot Jam Vol。9 Myuu
10715 毛奸你 10700 毛奸你
10716 恐怖主义 10701 恐怖主义
10717 -领导  
10718 勁樂 10702 勁樂
10719 性爱插穴 10703 性爱插穴
10720 你呀的鸡巴长歪了吧 10704 你呀的鸡巴长歪了吧
@@ -11226,7 +11210,6 @@ a 片成人免费激情小电影 @@ -11226,7 +11210,6 @@ a 片成人免费激情小电影
11226 真三国无双OL 11210 真三国无双OL
11227 陈慕华 11211 陈慕华
11228 真孔雀王 11212 真孔雀王
11229 -消息  
11230 yeeplay 11213 yeeplay
11231 11214
11232 东突 11215 东突
@@ -11405,7 +11388,6 @@ meimei的嫩屄 @@ -11405,7 +11388,6 @@ meimei的嫩屄
11405 jiejie被轮奸 11388 jiejie被轮奸
11406 18dy免费观看 11389 18dy免费观看
11407 免费放淫电影**日本高校女生性交 11390 免费放淫电影**日本高校女生性交
11408 -销售  
11409 好紧的屄啊 11391 好紧的屄啊
11410 祁建 11392 祁建
11411 18岁少女处夜激情性爱电影下载中心 11393 18岁少女处夜激情性爱电影下载中心
@@ -12004,7 +11986,6 @@ fofg @@ -12004,7 +11986,6 @@ fofg
12004 鷄巴 11986 鷄巴
12005 手记注册日韩5002部激情电影 11987 手记注册日韩5002部激情电影
12006 干死你 11988 干死你
12007 -服务管理  
12008 渡口 11989 渡口
12009 毛回良玉 11990 毛回良玉
12010 又黑又粗长的鸡巴 11991 又黑又粗长的鸡巴
@@ -12102,7 +12083,6 @@ EVENT @@ -12102,7 +12083,6 @@ EVENT
12102 淫奴 12083 淫奴
12103 法陯功 12084 法陯功
12104 淫女 12085 淫女
12105 -锤子  
12106 淫奸 12086 淫奸
12107 钱 其 琛 12087 钱 其 琛
12108 龟头插进 12088 龟头插进
@@ -12304,7 +12284,6 @@ roiworld @@ -12304,7 +12284,6 @@ roiworld
12304 12284
12305 肏妹妹的小骚屄 12285 肏妹妹的小骚屄
12306 强*十八岁meinv护士看完保证你打飞机免费淫电影a片 12286 强*十八岁meinv护士看完保证你打飞机免费淫电影a片
12307 -系统管理员  
12308 在线淫色电影 12287 在线淫色电影
12309 天安门英雄纪念碑 12288 天安门英雄纪念碑
12310 儿子添着妈妈肥大湿润阴唇 12289 儿子添着妈妈肥大湿润阴唇
@@ -12694,7 +12673,6 @@ fuck私/服 @@ -12694,7 +12673,6 @@ fuck私/服
12694 Tokyo Hot n0232 : 真宫梨沙子 12673 Tokyo Hot n0232 : 真宫梨沙子
12695 西哈努克 12674 西哈努克
12696 莎士比亚 12675 莎士比亚
12697 -明慧  
12698 台灣民国 12676 台灣民国
12699 硕大的龟头 12677 硕大的龟头
12700 黄 菊 12678 黄 菊
@@ -12734,7 +12712,6 @@ Lateinos @@ -12734,7 +12712,6 @@ Lateinos
12734 街上钓个漂亮马子回家玩 12712 街上钓个漂亮马子回家玩
12735 meinv主持25小时全天激情热舞 12713 meinv主持25小时全天激情热舞
12736 輿論鉗制 12714 輿論鉗制
12737 -服務管理  
12738 蒋正华 12715 蒋正华
12739 遭警察殴打 12716 遭警察殴打
12740 本拉丹 12717 本拉丹
@@ -13340,7 +13317,6 @@ sex大型色站 @@ -13340,7 +13317,6 @@ sex大型色站
13340 抽插38岁处女妹妹淫叫不断 13317 抽插38岁处女妹妹淫叫不断
13341 激爱贴图 加西激情中文网 13318 激爱贴图 加西激情中文网
13342 女性私处锻炼法 13319 女性私处锻炼法
13343 -哥哥  
13344 13320
13345 我的穴 13321 我的穴
13346 税力 13322 税力
@@ -14270,8 +14246,6 @@ ur靠 @@ -14270,8 +14246,6 @@ ur靠
14270 劉曉竹 14246 劉曉竹
14271 免费淫奸女A片段 免费在线观看 14247 免费淫奸女A片段 免费在线观看
14272 习近平 14248 习近平
14273 -姐姐  
14274 -  
14275 嫩穴 14249 嫩穴
14276 淫液 14250 淫液
14277 操大波波 14251 操大波波
@@ -14353,7 +14327,6 @@ fuck特别公告 @@ -14353,7 +14327,6 @@ fuck特别公告
14353 鹿城色meimei大型黄站 14327 鹿城色meimei大型黄站
14354 揭批书 14328 揭批书
14355 互淫 14329 互淫
14356 -管理人员  
14357 少妇白洁的淫乱生活 14330 少妇白洁的淫乱生活
14358 真理組織 14331 真理組織
14359 丁石孫 14332 丁石孫
@@ -14,9 +14,21 @@ public class SensitiveFilterTest extends TestCase{ @@ -14,9 +14,21 @@ public class SensitiveFilterTest extends TestCase{
14 14
15 public void test() throws Exception{ 15 public void test() throws Exception{
16 16
  17 + // 使用默认单例(加载默认词典)
17 SensitiveFilter filter = SensitiveFilter.DEFAULT; 18 SensitiveFilter filter = SensitiveFilter.DEFAULT;
18 -  
19 - System.out.println(filter.filter("会上,主席进行了发言。", '*')); 19 + // 向过滤器增加一个词
  20 + filter.put("婚礼上唱春天在哪里");
  21 +
  22 + // 待过滤的句子
  23 + String sentence = "然后,市长在婚礼上唱春天在哪里。";
  24 + // 进行过滤
  25 + String filted = filter.filter(sentence, '*');
  26 +
  27 + // 如果未过滤,则返回输入的String引用
  28 + if(sentence != filted){
  29 + // 句子中有敏感词
  30 + System.out.println(filted);
  31 + }
20 32
21 } 33 }
22 34
@@ -42,24 +54,22 @@ public class SensitiveFilterTest extends TestCase{ @@ -42,24 +54,22 @@ public class SensitiveFilterTest extends TestCase{
42 } 54 }
43 } 55 }
44 56
45 - System.out.println(String.format("共加载 %d 行,%d 字符。", testSuit.size(), length)); 57 + System.out.println(String.format("待过滤文本共 %d 行,%d 字符。", testSuit.size(), length));
46 58
47 59
48 SensitiveFilter filter = SensitiveFilter.DEFAULT; 60 SensitiveFilter filter = SensitiveFilter.DEFAULT;
49 61
50 int replaced = 0; 62 int replaced = 0;
51 63
52 - for(String line: testSuit){  
53 - if(! line.contains("`")){  
54 - String result = filter.filter(line, '`');  
55 - if(result.contains("`")){  
56 - ps.println(line); 64 + for(String sentence: testSuit){
  65 + String result = filter.filter(sentence, '*');
  66 + if(result != sentence){
  67 + ps.println(sentence);
57 ps.println(result); 68 ps.println(result);
58 ps.println(); 69 ps.println();
59 replaced ++; 70 replaced ++;
60 } 71 }
61 } 72 }
62 - }  
63 ps.close(); 73 ps.close();
64 74
65 long timer = System.currentTimeMillis(); 75 long timer = System.currentTimeMillis();
@@ -67,7 +77,7 @@ public class SensitiveFilterTest extends TestCase{ @@ -67,7 +77,7 @@ public class SensitiveFilterTest extends TestCase{
67 filter.filter(line, '*'); 77 filter.filter(line, '*');
68 } 78 }
69 timer = System.currentTimeMillis() - timer; 79 timer = System.currentTimeMillis() - timer;
70 - System.out.println(String.format("耗时 %1.3f 秒, 速度为 %1.1f字符/毫秒", timer * 1E-3, length / (double) timer)); 80 + System.out.println(String.format("过滤耗时 %1.3f 秒, 速度为 %1.1f字符/毫秒", timer * 1E-3, length / (double) timer));
71 System.out.println(String.format("其中 %d 行有替换", replaced)); 81 System.out.println(String.format("其中 %d 行有替换", replaced));
72 82
73 } 83 }