最近在开发过程中遇到了需要添加敏感词的地方,我这个方法是扫描数据库的数据完成敏感词过滤。
我的文章中也有关于txt敏感词过滤的方法。
该方法只能过滤两个字或者两个字以上的敏感词。
首先,你可以去网上找一些关于敏感词的内容,放到数据库中,可以用个字段来进行判断这个敏感词是黑名单还是白名单内的。
黑名单中盛放的就是你所要用的敏感词,白名单就是对黑名单中的词的释放,如果你黑名单中有这个敏感词,你在白名单中也添加了这个词,这个词会被释放,不会当做敏感词进行过滤。
其次是pom.xml文件
<groupId>com.odianyun.util</groupId>
<artifactId>sensitive-words</artifactId>
<version>1.0.2</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
然后自己可以创建一个utils,在其中存放四个工具类
第一个工具类SensitiveNode.java:
import java.io.Serializable;
import java.util.TreeSet;
/**
* 敏感词节点,每个节点包含了以相同的2个字符开头的所有词
*/
public class SensitiveNode implements Serializable{
private static final long serialVersionUID = 1L;
/**
* 头两个字符的mix,mix相同,两个字符相同
*/
protected final int headTwoCharMix;
/**
* 所有以这两个字符开头的词表
*/
protected final TreeSet<StringPointer> words = new TreeSet<StringPointer>();
/**
* 下一个节点
*/
protected SensitiveNode next;
public SensitiveNode(int headTwoCharMix){
this.headTwoCharMix = headTwoCharMix;
}
public SensitiveNode(int headTwoCharMix, SensitiveNode parent){
this.headTwoCharMix = headTwoCharMix;
parent.next = this;
}
}
第二个工具类StringPointer.java:
import java.io.Serializable;
import java.util.HashMap;
import java.util.TreeMap;
/**
* 没有注释的方法与{@link String}类似<br/>
* <b>注意:</b>没有(数组越界等的)安全检查<br/>
* 可以作为{@link HashMap}和{@link TreeMap}的key
*/
public class StringPointer implements Serializable, CharSequence, Comparable<StringPointer>{
private static final long serialVersionUID = 1L;
protected final char[] value;
protected final int offset;
protected final int length;
private int hash = 0;
public StringPointer(String str){
value = str.toCharArray();
offset = 0;
length = value.length;
}
public StringPointer(char[] value, int offset, int length){
this.value = value;
this.offset = offset;
this.length = length;
}
/**
* 计算该位置后(包含)2个字符的hash值
*
* @param i 从 0 到 length - 2
* @return hash值
*/
public int nextTwoCharHash(int i){
return 31 * value[offset + i] + value[offset + i + 1];
}
/**
* 计算该位置后(包含)2个字符和为1个int型的值<br/>
* int值相同表示2个字符相同
*
* @param i 从 0 到 length - 2
* @return int值
*/
public int nextTwoCharMix(int i){
return (value[offset + i] << 16) | value[offset + i + 1];
}
/**
* 该位置后(包含)的字符串,是否以某个词(word)开头
*
* @param i 从 0 到 length - 2
* @param word 词
* @return 是否?
*/
public boolean nextStartsWith(int i, StringPointer word){
// 是否长度超出
if(word.length > length - i){
return false;
}
// 从尾开始判断
for(int c = word.length - 1; c >= 0; c --){
if(value[offset + i + c] != word.value[word.offset + c]){
return false;
}
}
return true;
}
/**
* 填充(替换)
*
* @param begin 从此位置开始(含)
* @param end 到此位置结束(不含)
* @param fillWith 以此字符填充(替换)
*/
public void fill(int begin, int end, char fillWith){
for(int i = begin; i < end; i ++){
value[offset + i] = fillWith;
}
}
public int length(){
return length;
}
public char charAt(int i){
return value[offset + i];
}
public StringPointer substring(int begin){
return new StringPointer(value, offset + begin, length - begin);
}
public StringPointer substring(int begin, int end){
return new StringPointer(value, offset + begin, end - begin);
}
@Override
public CharSequence subSequence(int start, int end) {
return substring(start, end);
}
public String toString(){
return new String(value, offset, length);
}
public int hashCode() {
int h = hash;
if (h == 0 && length > 0) {
for (int i = 0; i < length; i++) {
h = 31 * h + value[offset + i];
}
hash = h;
}
return h;
}
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof StringPointer) {
StringPointer that = (StringPointer)anObject;
if (length == that.length) {
char v1[] = this.value;
char v2[] = that.value;
for(int i = 0; i < this.length; i ++){
if(v1[this.offset + i] != v2[that.offset + i]){
return false;
}
}
return true;
}
}
return false;
}
@Override
public int compareTo(StringPointer that) {
int len1 = this.length;
int len2 = that.length;
int lim = Math.min(len1, len2);
char v1[] = this.value;
char v2[] = that.value;
int k = 0;
while (k < lim) {
char c1 = v1[this.offset + k];
char c2 = v2[that.offset + k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
}
第三个工具类SensitiveFilter.java:
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.NavigableSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 敏感词过滤器,以过滤速度优化为主。<br/>
* * 增加一个敏感词:{@link #put(String)} <br/>
* * 过滤一个句子:{@link #filter(String, char)} <br/>
*/
public class SensitiveFilter implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 为2的n次方,考虑到敏感词大概在10k左右,
* 这个数量应为词数的数倍,使得桶很稀疏
* 提高不命中时hash指向null的概率,
* 加快访问速度。
*/
static final int DEFAULT_INITIAL_CAPACITY = 131072;
/**
* 类似HashMap的桶,比较稀疏。
* 使用2个字符的hash定位。
*/
protected SensitiveNode[] nodes = new SensitiveNode[DEFAULT_INITIAL_CAPACITY];
/**
* 构建一个空的filter
*a
* @author ZhangXiaoye
* @date 2017年1月5日 下午4:18:07
*/
public SensitiveFilter() {
}
/**
* 加载一个文件中的词典,并构建filter<br/>
* 文件中,每行一个敏感词条<br/>
* <b>注意:</b>读取完成后会调用{@link BufferedReader#close()}方法。<br/>
* <b>注意:</b>读取中的{@link IOException}不会抛出
*
* @param reader
*/
public SensitiveFilter(BufferedReader reader) {
// File f2 = new File(this.getClass().getResource("").getPath());
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
ClassLoader.getSystemResourceAsStream("test.txt")));
List<String> strings = loadKeywords(bufferedReader);
try {
for (String line = reader.readLine(); line != null; line = reader.readLine()) {
if (strings.indexOf(line)==-1) {
put(line);
}
}
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public SensitiveFilter(List<String> blackList,List<String> whiteList) {
for (String blackStr: blackList) {
if (whiteList.indexOf(blackStr)==-1) {
put(blackStr);
}
}
}
public static List<String> loadKeywords(BufferedReader br){
List<String> keyArray=new ArrayList<String>();
try{
String s = null;
while((s = br.readLine())!=null){//使用readLine方法,一次读一行
keyArray.add(s);
}
br.close();
}catch(Exception e){
e.printStackTrace();
}
return keyArray;
}
/**
* 去除空格 回车 换行 水平制表符
* @param str
* @return
*/
public static String replaceAllBlank(String str) {
String s = "";
if (str != null) {
Pattern p = Pattern.compile("\\s*|\t|\r|\n");
/* \n 回车(\u000a) \t 水平制表符(\u0009) \s 空格(\u0008) \r 换行(\u000d) */
Matcher m = p.matcher(str);
s = m.replaceAll("");
}
return s;
}
/**
* 增加一个敏感词,如果词的长度(trim后)小于2,则丢弃<br/>
* 此方法(构建)并不是主要的性能优化点。
* @param word
*/
public boolean put(String word) {
// 长度小于2的不加入
if (word == null || word.trim().length() < 2) {
return false;
}
// 两个字符的不考虑
if (word.length() == 2 && word.matches("\\w\\w")) {
return false;
}
StringPointer sp = new StringPointer(word.trim());
// 计算头两个字符的hash
int hash = sp.nextTwoCharHash(0);
// 计算头两个字符的mix表示(mix相同,两个字符相同)
int mix = sp.nextTwoCharMix(0);
// 转为在hash桶中的位置
int index = hash & (nodes.length - 1);
// 从桶里拿第一个节点
SensitiveNode node = nodes[index];
if (node == null) {
// 如果没有节点,则放进去一个
node = new SensitiveNode(mix);
// 并添加词
node.words.add(sp);
// 放入桶里
nodes[index] = node;
} else {
// 如果已经有节点(1个或多个),找到正确的节点
for (; node != null; node = node.next) {
// 匹配节点
if (node.headTwoCharMix == mix) {
node.words.add(sp);
return true;
}
// 如果匹配到最后仍然不成功,则追加一个节点
if (node.next == null) {
new SensitiveNode(mix, node).words.add(sp);
return true;
}
}
}
return true;
}
/**
* 对句子进行敏感词过滤<br/>
* 如果无敏感词返回输入的sentence对象,即可以用下面的方式判断是否有敏感词:<br/><code>
* String result = filter.filter(sentence, '*');<br/>
* if(result != sentence){<br/>
* // 有敏感词<br/>
* }
* </code>
* @param sentence 句子
* @param replace 敏感词的替换字符
* @return 过滤后的句子
*/
public String filter(String sentence, char replace) {
// 先转换为StringPointer
StringPointer sp = new StringPointer(sentence);
// 标示是否替换
boolean replaced = false;
// 匹配的起始位置
int i = 0;
while (i < sp.length - 2) {
/*
* 移动到下一个匹配位置的步进:
* 如果未匹配为1,如果匹配是匹配的词长度
*/
int step = 1;
// 计算此位置开始2个字符的hash
int hash = sp.nextTwoCharHash(i);
/*
* 根据hash获取第一个节点,
* 真正匹配的节点可能不是第一个,
* 所以有后面的for循环。
*/
SensitiveNode node = nodes[hash & (nodes.length - 1)];
/*
* 如果非敏感词,node基本为null。
* 这一步大幅提升效率
*/
if (node != null) {
/*
* 如果能拿到第一个节点,
* 才计算mix(mix相同表示2个字符相同)。
* mix的意义和HashMap先hash再equals的equals部分类似。
*/
int mix = sp.nextTwoCharMix(i);
/*
* 循环所有的节点,如果非敏感词,
* mix相同的概率非常低,提高效率
*/
outer:
for (; node != null; node = node.next) {
/*
* 对于一个节点,先根据头2个字符判断是否属于这个节点。
* 如果属于这个节点,看这个节点的词库是否命中。
* 此代码块中访问次数已经很少,不是优化重点
*/
if (node.headTwoCharMix == mix) {
/*
* 查出比剩余sentence小的最大的词。
* 例如剩余sentence为"色情电影哪家强?",
* 这个节点含三个词从小到大为:"色情"、"色情电影"、"色情信息"。
* 则从“色情电影”开始向前匹配
*/
NavigableSet<StringPointer> desSet = node.words.headSet(sp.substring(i), true);
if (desSet != null) {
for (StringPointer word : desSet.descendingSet()) {
/*
* 仍然需要再判断一次,例如"色情信息哪里有?",
* 如果节点只包含"色情电影"一个词,
* 仍然能够取到word为"色情电影",但是不该匹配。
*/
if (sp.nextStartsWith(i, word)) {
// 匹配成功,将匹配的部分,用replace制定的内容替代
sp.fill(i, i + word.length, replace);
// 跳过已经替代的部分
step = word.length;
// 标示有替换
replaced = true;
// 跳出循环(然后是while循环的下一个位置)
break outer;
}
}
}
}
}
}
// 移动到下一个匹配位置
i += step;
}
// 如果没有替换,直接返回入参(节约String的构造copy)
if (replaced) {
return sp.toString();
} else {
return sentence;
}
}
/**
* 对句子进行敏感词过滤<br/>
* 如果无敏感词返回输入的sentence对象,即可以用下面的方式判断是否有敏感词:<br/><code>
* String result = filter.filter(sentence, '*');<br/>
* if(result != sentence){<br/>
* // 有敏感词<br/>
* }
* </code>
*
* @param sentence 句子
* @return 敏感词
* @author ZhangXiaoye
* @date 2017年1月5日 下午4:16:31
*/
public List<String> CheckSensitiveWord(String sentence) {
List<String> sensitiveList = new ArrayList<>();
// 先转换为StringPointer
StringPointer sp = new StringPointer(sentence);
// 标示是否替换
boolean replaced = false;
// 匹配的起始位置
int i = 0;
while (i < sp.length -1) {
/*
* 移动到下一个匹配位置的步进:
* 如果未匹配为1,如果匹配是匹配的词长度
*/
int step = 1;
// 计算此位置开始2个字符的hash
int hash = sp.nextTwoCharHash(i);
/*
* 根据hash获取第一个节点,
* 真正匹配的节点可能不是第一个,
* 所以有后面的for循环。
*/
SensitiveNode node = nodes[hash & (nodes.length - 1)];
/*
* 如果非敏感词,node基本为null。
* 这一步大幅提升效率
*/
if (node != null) {
/*
* 如果能拿到第一个节点,
* 才计算mix(mix相同表示2个字符相同)。
* mix的意义和HashMap先hash再equals的equals部分类似。
*/
int mix = sp.nextTwoCharMix(i);
/*
* 循环所有的节点,如果非敏感词,
* mix相同的概率非常低,提高效率
*/
outer:
for (; node != null; node = node.next) {
/*
* 对于一个节点,先根据头2个字符判断是否属于这个节点。
* 如果属于这个节点,看这个节点的词库是否命中。
* 此代码块中访问次数已经很少,不是优化重点
*/
if (node.headTwoCharMix == mix) {
/*
* 查出比剩余sentence小的最大的词。
* 例如剩余sentence为"色情电影哪家强?",
* 这个节点含三个词从小到大为:"色情"、"色情电影"、"色情信息"。
* 则从“色情电影”开始向前匹配
*/
NavigableSet<StringPointer> desSet = node.words.headSet(sp.substring(i), true);
if (desSet != null) {
for (StringPointer word : desSet.descendingSet()) {
/*
* 仍然需要再判断一次,例如"色情信息哪里有?",
* 如果节点只包含"色情电影"一个词,
* 仍然能够取到word为"色情电影",但是不该匹配。
*/
if (sp.nextStartsWith(i, word)) {
sensitiveList.add(word.toString());
// 匹配成功,将匹配的部分,用replace制定的内容替代
// sp.fill(i, i + word.length, replace);
// // 跳过已经替代的部分
// step = word.length;
// 标示有替换
replaced = true;
// 跳出循环(然后是while循环的下一个位置)
break outer;
}
}
}
}
}
}
// 移动到下一个匹配位置
i += step;
}
/* // 如果没有替换,直接返回入参(节约String的构造copy)
if (replaced) {
return sp.toString();
} else {
return sentence;
}*/
return sensitiveList;
}
}
第四个工具类SensitiveUtils:
import com.sun.deploy.util.StringUtils;
import java.util.List;
/**
* Created with IntelliJ IDEA.
* @Description:
*/
public class SensitiveUtils {
/**
* 敏感词识别
* @param sentence 需要识别的词或语句
* @param blackList 黑名单集合
* @param whiteList 白名单集合
* @return true无/false有
*/
public static boolean CheckSensitiveWord(String sentence,List<String> blackList,List<String> whiteList){
//构建敏感词DFA模型
SensitiveFilter filter = new SensitiveFilter(blackList,whiteList);
List<String> sentenceList = filter.CheckSensitiveWord(sentence);
if(sentenceList.size()>0){
//转成拼接字符串
String sentenceStr = StringUtils.join(sentenceList, ",");
System.out.println(sentenceStr);
return false;
}
return true;
}
/**
* 将词或语句的敏感内容替换成指定符号
* @param sentence 词或语句
* @param replace 指定符号
* @param blackList 黑名单集合
* @param whiteList 白名单集合
* @return 替换完的语句
*/
public static String FilterSensitiveWord(String sentence, char replace,List<String> blackList,List<String> whiteList){
//构建敏感词DFA模型
SensitiveFilter filter = new SensitiveFilter(blackList,whiteList);
return filter.filter(sentence, replace);
}
}
将敏感词语句或者词汇放入sentence,然后将从数据库中查出来的黑名单集合存放到blackList中,白名单集合存放到whiteList中
第四个utils中的方法可以直接进行调用,第一个方法是识别,第二个方法是替换。