0%

一、简介

Nacos 提供用于存储配置和其他元数据的 key/value 存储,为分布式系统中的外部化配置提供服务器端和客户端支持。使用 Spring Cloud Alibaba Nacos Config,您可以在 Nacos Server 集中管理你 Spring Cloud 应用的外部属性配置。

Spring Cloud Alibaba Nacos Config 是 Config Server 和 Client 的替代方案,客户端和服务器上的概念与 Spring Environment 和 PropertySource 有着一致的抽象,在特殊的 bootstrap 阶段,配置被加载到 Spring 环境中。当应用程序通过部署管道从开发到测试再到生产时,您可以管理这些环境之间的配置,并确保应用程序具有迁移时需要运行的所有内容。

阅读全文 »

一、版本说明

我觉得有必要介绍Spring-cloud-alibabaSpringcloud的版本关系。

1、毕业版本依赖关系

SpringCloud Version SpringCloud-Alibaba version SpringBoot Version
Spring Cloud Hoxton.SR8 2.2.3.RELEASE 2.3.2.RELEASE
Spring Cloud Greenwich.SR6 2.1.3.RELEASE 2.1.13.RELEASE
Spring Cloud Hoxton.SR8 2.2.2.RELEASE 2.3.2.RELEASE
Spring Cloud Hoxton.SR3 2.2.1.RELEASE 2.2.5.RELEASE
Spring Cloud Hoxton.RELEASE 2.2.0.RELEASE 2.2.X.RELEASE
Spring Cloud Greenwich 2.1.2.RELEASE 2.1.X.RELEASE
Spring Cloud Finchley 2.0.3.RELEASE 2.0.X.RELEASE
Spring Cloud Edgware 1.5.1.RELEASE(停止维护,建议升级) 1.5.X.RELEASE
阅读全文 »

一、Nacos简介

Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

Nacos 的主要特性有:

  • 1、服务发现和服务健康检测
    简单的讲就是微服务的注册中心组件
  • 2、动态配置服务
    简单的讲就是微服务的配置中心组件
  • 3、动态DNS服务
    DNS解析服务,可配置路由的权重,实现中间层的负载均衡
  • 4、服务及元数据管理
    可视化管理服务的元数据、包括服务的描述、生命周期、健康状态、流量管理等等

可查看官网战略图:

阅读全文 »

一、前言

在工作中需要做敏感词过滤,如何高效的过滤敏感词,然后通过科普知道了DFA算法。

二、DFA概述

在计算理论中,确定有限状态自动机(DFA) 是一个能实现状态转移的自动机。对于一个给定的输入,DFA会根据当前状态和输入符号,确定地转移到下一个状态。与之类似还有非确定有限自动机(NFA)

简单的说就是:DFA消耗输入符号的字符串。对每个输入符号它变换到一个新状态直到所有输入符号到被耗尽。下一个状态是唯一确定的。

个人粗俗的理解:就像树的结构,从根开始到枝叶子结束,沿着字符路径走到叶子节点,就完成了一个敏感词的匹配

本篇不讲它的函数公式这些,直接代码走起!!!

三、Java代码实现

我们知道敏感词都是一个词语,我们把词语拆成一个字然后再关联组合,这样是不是就像DFA的状态转移。在Java中可以使用HashMap来实现这样的结构。

1
2
3
4
5
6
7
// 比如,现在的敏感词有:代开发票、代购、代购商
// 转为DFA 状态就会变成如下这样:

代 → 开 → 发 → 票

购 → 商

1、构建DFA 模型结构

我们要知道什么时候结束,所以在代码里加一个结束标识(isEnd)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 敏感词集合
*/
public static Map<String,Object> sensitiveWordMap;

/**
* 初始化敏感词库,构建DFA算法模型
* @param sensitiveWordSet 需要加载的敏感词集合
*/
private static void load(Set<String> sensitiveWordSet) {
//初始化敏感词容器,减少扩容操作
sensitiveWordMap = new HashMap<>(sensitiveWordSet.size());
String key;
// 总词库的指针
Map<String,Object> nowMap;
Iterator<String> iterator = sensitiveWordSet.iterator();
while (iterator.hasNext()) {
key = iterator.next();
// 加载下一个词重新指向总词库,因为addKey里面指针被改变了
nowMap = sensitiveWordMap;
addKey(key, nowMap);
}
}

/**
* 添加敏感词
*
* @param key 要添加的敏感词
* @param nowMap 已加载的敏感词库
*/
public static void addKey(String key, Map<String,Object> nowMap) {
if (StrUtil.isBlank(key)) {
return;
}
// 把关键字拆成一个一个字
for (int i = 0; i < key.length(); i++) {
//转换成char型
char keyChar = key.charAt(i);
//库中获取关键字
Object wordMap = nowMap.get(String.valueOf(keyChar));
//如果存在该key,直接赋值,用于下一个循环获取
if (wordMap != null) {
// 这里的情况就相当与上面的例子:代购 和 代购商
nowMap = (Map<String,Object>) wordMap;
} else {
//不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个
Map<String,Object> newWorMap = new HashMap<>();
//不是最后一个
newWorMap.put("isEnd", "0");
// 这里注意一点,这个put 的是一个对象指针,如果对象改变,put进行的对象也就改变了
// 然后nowMap 最开始是总词库的指针,所以这里虽然没有出现总词库,但是总词库最终还是会添加到新的key
nowMap.put(String.valueOf(keyChar), newWorMap);
nowMap = newWorMap;
}
if (i == key.length() - 1) {
//最后一个
nowMap.put("isEnd", "1");
}
}
}

上面已经构建好了DFA 词库模型。长如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"代": {
"开": {
"发": {
"票": {
"isEnd": "1"
},
"isEnd": "0"
},
"isEnd": "0"
},
"isEnd": "0",
"购": {
"商": {
"isEnd": "1"
},
"isEnd": "1"
}
}
}

发现 的标识都是 1 也就是结束,这是正常的。

2、如何查找

我们查找是否包含敏感词的时候也是一个一个去找,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 判断文字是否包含敏感字符
*
* @param txt 文字
* @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
* @return 若包含返回true,否则返回false
*/
public static boolean contains(String txt, int matchType) {
// 总词库的指针
Map<String,Object> sensitiveMap = sensitiveWordMap;
for (int i = 0; i < txt.length(); i++) {
//判断是否包含敏感字符
int matchFlag = checkWord(txt, i, matchType, sensitiveMap);
if (matchFlag > 0) {
//大于0存在,返回true
return true;
}
}
return false;
}

/**
* 校验文字是否包含敏感词
* @param txt 文字
* @param beginIndex 开始索引
* @param matchType 匹配规则
* @param wordStoreMap 敏感词库
* @return 返回找到敏感词字符的长度
*/
private static int checkWord(String txt, int beginIndex, int matchType, Map<String,Object> wordStoreMap) {
//敏感词结束标识位:用于敏感词只有1位的情况
boolean flag = false;
//匹配标识数默认为0
int matchFlag = 0;
char word;
Map<String,Object> nowMap = wordStoreMap;
for (int i = beginIndex; i < txt.length(); i++) {
word = txt.charAt(i);
//获取指定key
nowMap = (Map<String,Object>) nowMap.get(String.valueOf(word));
//存在,则判断是否为最后一个
if (nowMap != null) {
//找到相应key,匹配标识+1
matchFlag++;
//如果为最后一个匹配规则,结束循环,返回匹配标识数
if ("1".equals(nowMap.get("isEnd"))) {
//结束标志位为true
flag = true;
//最小规则,直接返回,最大规则还需继续查找
if (MIN_MATCHTYPE == matchType) {
break;
}
}
} else {
//不存在,直接返回
break;
}
}
return flag ? matchFlag : 0;
}

其实代码不一定非得和我上面写得一样,我只是拆得比较细,后面再组装,只要看懂思路之后随便改成你想要的方法。

完整版的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;

/**
* DFA算法
*
* @author lrs
* @since 2020-10-16
*/
@Slf4j
public class SensitiveWordUtils {
/**
* 敏感词匹配规则
* 最小匹配规则,如:敏感词库["代购","代购商"],语句:"我是代购商",匹配结果:我是[代购]商
*/
public static final int MIN_MATCHTYPE = 1;

/**
* 最大匹配规则,如:敏感词库["代购","代购商"],语句:"我是代购商",匹配结果:我是[代购商]
*/
public static final int MAX_MATCHTYPE = 2;

/**
* 敏感词集合
*/
public static Map<String,Object> sensitiveWordMap;

/**
* 加载敏感词库
* @param sensitiveWordSet 敏感词库
*/
public static synchronized void load(Set<String> sensitiveWordSet) {
initWords(sensitiveWordSet);
log.info("==加载敏感词库={}个==", sensitiveWordSet.size());
}

/**
* 初始化敏感词库,构建DFA算法模型
* @param sensitiveWordSet 敏感词库
*/
private static void initWords(Set<String> sensitiveWordSet) {
//初始化敏感词容器,减少扩容操作
sensitiveWordMap = new HashMap<>(sensitiveWordSet.size());
String word;
Map<String,Object> nowMap;
Iterator<String> iterator = sensitiveWordSet.iterator();
while (iterator.hasNext()) {
word = iterator.next();
nowMap = sensitiveWordMap;
addWord(word, nowMap);
}
}
/**
* 添加敏感词
*
* @param word 要添加的敏感词
* @param nowMap 已加载的敏感词库
*/
public static void addWord(String word, Map<String, Object> nowMap) {
if (StrUtil.isBlank(word)) {
return;
}
for (int i = 0; i < word.length(); i++) {
//转换成char型
char keyChar = word.charAt(i);
//库中获取关键字
Object wordMap = nowMap.get(String.valueOf(keyChar));
//如果存在该key,直接赋值,用于下一个循环获取
if (wordMap != null) {
nowMap = (Map<String, Object>) wordMap;
} else {
//不存在则,则构建一个map,同时将isEnd设置为0,因为他不是最后一个
Map<String, Object> newWorMap = new HashMap<>();
//不是最后一个
newWorMap.put("isEnd", "0");
nowMap.put(String.valueOf(keyChar), newWorMap);
nowMap = newWorMap;
}
if (i == word.length() - 1) {
//最后一个
nowMap.put("isEnd", "1");
}
}
}

/**
* 移除敏感词
* @param word 敏感词
* @param nowMap 总词库
* @return boolean
*/
public static boolean removeWord(String word, Map<String, Object> nowMap) {
if (StrUtil.isBlank(word)) {
return false;
}
boolean canRemove=false;
String oneLeveKey = String.valueOf(word.charAt(0));
// 最外层的map
Map<String,Object> tempMap = (Map<String, Object>) nowMap.get(oneLeveKey);
for (int i = 1; i < word.length(); i++) {
//转换成char型
char keyChar = word.charAt(i);
//库中获取关键字
Object wordMap = tempMap.get(String.valueOf(keyChar));
if(wordMap==null){
canRemove=false;
break;
}
tempMap= (Map<String, Object>) wordMap;
canRemove=true;
}
if(canRemove && tempMap!=null){
if(tempMap.size()==1){
nowMap.remove(oneLeveKey);
log.info("敏感词库已移除:{} 关键词",word);
}else {
tempMap.put("isEnd","0");
log.info("敏感词库已更新:{} 状态",word);
}
}
return canRemove;
}

/**
* 获取词库的方法,本地或redis
* @return 返回本地敏感词库,可改为redis
*/
public static Map<String,Object> getSensitiveMap() {
return sensitiveWordMap;
}

/**
* 判断文字是否包含敏感字符
*
* @param txt 文字
* @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
* @return 若包含返回true,否则返回false
*/
public static boolean contains(String txt, int matchType) {
Map<String,Object> sensitiveMap = getSensitiveMap();
for (int i = 0; i < txt.length(); i++) {
//判断是否包含敏感字符
int matchFlag = checkWord(txt, i, matchType, sensitiveMap);
if (matchFlag > 0) {
//大于0存在,返回true
return true;
}
}
return false;
}

/**
* 判断文字是否包含敏感字符
*
* @param txt 文字
* @return 若包含返回true,否则返回false
*/
public static boolean contains(String txt) {
return contains(txt, MIN_MATCHTYPE);
}

/**
* 获取文字中的敏感词
*
* @param txt 文字
* @param matchType 匹配规则 1:最小匹配规则,2:最大匹配规则
* @return 返回匹配的敏感词
*/
public static Set<String> getSensitiveWord(String txt, int matchType) {
Map<String,Object> sensitiveMap = getSensitiveMap();
Set<String> resultSet = new HashSet<>();
for (int i = 0; i < txt.length(); i++) {
//判断是否包含敏感字符
int length = checkWord(txt, i, matchType, sensitiveMap);
//存在,加入list中
if (length > 0) {
resultSet.add(txt.substring(i, i + length));
//减1的原因,是因为for会自增
i = i + length - 1;
}
}
return resultSet;
}

/**
* 获取文字中的敏感词
* @param txt 文字
* @return 返回敏感词
*/
public static Set<String> getSensitiveWord(String txt) {
return getSensitiveWord(txt, MIN_MATCHTYPE);
}

/**
* 替换敏感字字符
* @param txt 文本
* @param replaceValue 替换的字符,替换单个字
* @return 返回替换后的字符串
*/
public static String replaceWordByOne(String txt, String replaceValue) {
return replaceWord(txt, replaceValue, MAX_MATCHTYPE,false);
}

/**
* 替换敏感字字符
* @param txt 文本
* @param replaceStr 替换的字符串,替换整个词
* @return 返回替换后的字符串
*/
public static String replaceWordByAll(String txt, String replaceStr) {
return replaceWord(txt, replaceStr, MAX_MATCHTYPE,true);
}

/**
* 替换敏感字字符
* @param txt 文本
* @param replaceValue 替换的字符
* @param matchType 敏感词匹配规则
* @param isReplaceAll replaceValue 替换所有还是替换 敏感词的一个字,true-所有。false- 一个
* @return 返回替换后的字符串
*/
public static String replaceWord(String txt, String replaceValue, int matchType,boolean isReplaceAll) {
String resultTxt = txt;
//获取所有的敏感词
Set<String> set = getSensitiveWord(txt, matchType);
Iterator<String> iterator = set.iterator();
String word;
String replaceString;
while (iterator.hasNext()) {
word = iterator.next();
replaceString = isReplaceAll?replaceValue:getReplaceChars(replaceValue, word.length());
resultTxt = resultTxt.replaceAll(word, replaceString);
}
return resultTxt;
}

/**
* 获取替换字符串
* @param replaceChar 替换的字符
* @param length 长度
* @return 返回 length 个 replaceChar
*/
private static String getReplaceChars(Object replaceChar, int length) {
StringBuilder sb = new StringBuilder();
sb.append(replaceChar);
for (int i = 1; i < length; i++) {
sb.append(replaceChar);
}
return sb.toString();
}

/**
* 校验文字是否包含敏感词
* @param txt 文字
* @param beginIndex 开始索引
* @param matchType 匹配规则
* @param wordStoreMap 敏感词库
* @return 返回找到敏感词字符的长度
*/
private static int checkWord(String txt, int beginIndex, int matchType, Map<String,Object> wordStoreMap) {
//敏感词结束标识位:用于敏感词只有1位的情况
boolean flag = false;
//匹配标识数默认为0
int matchFlag = 0;
char word;
Map<String,Object> nowMap = wordStoreMap;
for (int i = beginIndex; i < txt.length(); i++) {
word = txt.charAt(i);
//获取指定key
nowMap = (Map<String,Object>) nowMap.get(String.valueOf(word));
//存在,则判断是否为最后一个
if (nowMap != null) {
//找到相应key,匹配标识+1
matchFlag++;
//如果为最后一个匹配规则,结束循环,返回匹配标识数
if ("1".equals(nowMap.get("isEnd"))) {
//结束标志位为true
flag = true;
//最小规则,直接返回,最大规则还需继续查找
if (MIN_MATCHTYPE == matchType) {
break;
}
}
} else {
//不存在,直接返回
break;
}
}
return flag ? matchFlag : 0;
}

/**
* 过滤常见特殊字符与空格
* @param str
* @return
*/
public static String filterSpecialStr(String str) {
String regEx = "[`~!@#$%^&*()+=|{}:;\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?']";
Pattern pattern = Pattern.compile(regEx);
return pattern.matcher(str).replaceAll("").replaceAll(" ","").trim();
}

/**
* 读取敏感词文件
* @param filePaths 文件绝对路径,支持多个文件
* @return 返回敏感词词库
*/
public static Set<String> readFile(List<String> filePaths) {
Set<String> result = new HashSet<>();
InputStreamReader read = null;
BufferedReader bufferedReader = null;
try {
if (filePaths != null && filePaths.size() > 0) {
for (String filePath : filePaths) {
File file = new File(filePath);
int count = 0;
//判断文件是否存在
if (file.isFile() && file.exists()) {
read = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(read);
String lineTxt ;
while ((lineTxt = bufferedReader.readLine()) != null) {
if (StrUtil.isNotBlank(lineTxt)) {
result.add(lineTxt);
count++;
}
}
} else {
log.info("找不到指定的文件={}", filePath);
}
log.info("加载文件:{},个数={}", filePath, count);
}
}
if (bufferedReader != null) {
bufferedReader.close();
}
if (read != null) {
read.close();
}
} catch (Exception e) {
log.error("加载敏感词文件出错", e);
}
return result;
}


public static void main(String[] args) {
test();
}


public static void test() {
List<String> fileList = new ArrayList<>();
// fileList.add("D:\\敏感词\\广告.txt");

Set<String> sensitiveWordSet = readFile(fileList);
sensitiveWordSet.add("代开发票");
sensitiveWordSet.add("代购");
sensitiveWordSet.add("代购商");

System.out.println("加载敏感词个数:" + sensitiveWordSet.size());
//初始化敏感词库
SensitiveWordUtils.load(sensitiveWordSet);
String content = "结束标志位结束代购商标职业志位结束标志敏感位结束标志位代购结束标志位结束苹果标";
long beginTime = System.currentTimeMillis();
boolean isContains = contains(content);
Set<String> sensitiveWord = getSensitiveWord(content);
System.out.println("敏感词=" + sensitiveWord);
System.out.println("耗时:" + (System.currentTimeMillis() - beginTime) + "ms");
System.out.println(replaceWordByOne(content, "*"));
System.out.println(replaceWordByAll(content, "替换整个词"));
}
}

不需要的方法自行去掉,自己改一些就行。

3、优化版

上面是我好多年前写的,最近重构了一版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;

/**
* DFA算法实现的敏感词过滤工具类
* @author rstyro
* @since 2025-11-03
*/
@Slf4j
public class SensitiveWordUtil {

/**
* 敏感词匹配规则
* 最小匹配规则,如:敏感词库["代购","代购商"],语句:"我是代购商",匹配结果:我是[代购]商
*/
public static final int MIN_MATCH_TYPE = 1;
/**
* 最大匹配规则,如:敏感词库["代购","代购商"],语句:"我是代购商",匹配结果:我是[代购商]
*/
public static final int MAX_MATCH_TYPE = 2;

/**
* 敏感词集合
*/
private static volatile Map<Character, SensitiveNode> sensitiveWordMap;

/**
* 特殊字符过滤正则
* 匹配常见特殊字符和空格
*/
private static final Pattern SPECIAL_CHAR_PATTERN =
Pattern.compile("[`~!@#$%^&*()+=|{}:;\\[\\].<>/?~!@#¥%……&*()——+|{}【】';:\"。,、?\\s]");

/**
* 敏感词节点内部类
*/
private static class SensitiveNode {
// 标记当前节点是否为一个敏感词的结尾
private boolean isEnd;
// 子节点映射Map,Key为字符,Value为对应的子节点
private final Map<Character, SensitiveNode> children;

public SensitiveNode() {
this.children = new HashMap<>();
this.isEnd = false;
}

public boolean isEnd() {
return isEnd;
}

public void setEnd(boolean end) {
isEnd = end;
}

public Map<Character, SensitiveNode> getChildren() {
return children;
}

public SensitiveNode getChild(Character key) {
return children.get(key);
}

public SensitiveNode putChild(Character key, SensitiveNode node) {
return children.put(key, node);
}

public SensitiveNode removeChild(Character key) {
return children.remove(key);
}

public boolean hasChildren() {
return !children.isEmpty();
}
}


/**
* 加载敏感词库
* @param sensitiveWordSet 敏感词集合
*/
public static synchronized void load(Set<String> sensitiveWordSet) {
if (sensitiveWordSet == null || sensitiveWordSet.isEmpty()) {
sensitiveWordMap = Collections.emptyMap();
log.warn("==加载敏感词库: 词库为空==");
return;
}

initWords(sensitiveWordSet);
log.info("==加载敏感词库完成,共{}个敏感词==", sensitiveWordSet.size());
}

/**
* 初始化敏感词库,构建DFA算法模型
* 将平面结构的敏感词集合转换为树形结构,便于快速查找
*/
private static void initWords(Set<String> sensitiveWordSet) {
Map<Character, SensitiveNode> newWordMap = new HashMap<>(sensitiveWordSet.size());

for (String word : sensitiveWordSet) {
if (StrUtil.isNotBlank(word)) {
addWord(word, newWordMap);
}
}

sensitiveWordMap = newWordMap;
}

/**
* 添加敏感词
* @param word 要添加的敏感词,如果为空则忽略
* @param wordMap 目标词库映射表
*/
public static void addWord(String word, Map<Character, SensitiveNode> wordMap) {
if (StrUtil.isBlank(word)) {
return;
}
// 每个敏感词,都从根节点开始查找
Map<Character, SensitiveNode> currentMap = wordMap;

for (int i = 0; i < word.length(); i++) {
char keyChar = word.charAt(i);
SensitiveNode node = currentMap.get(keyChar);

if (node == null) {
node = new SensitiveNode();
currentMap.put(keyChar, node);
}

if (i == word.length() - 1) {
node.setEnd(true);
}
// 移动到下一层节点
currentMap = node.getChildren();
}
}

/**
* 移除敏感词
*/
public static boolean removeWord(String word) {
return removeWord(word, sensitiveWordMap);
}

public static boolean removeWord(String word, Map<Character, SensitiveNode> wordMap) {
if (StrUtil.isBlank(word) || wordMap == null) {
return false;
}
// 使用递归方法移除敏感词
return removeWordRecursive(word, 0, wordMap, new ArrayList<>());
}

private static boolean removeWordRecursive(String word, int index,
Map<Character, SensitiveNode> currentMap,
List<Map<Character, SensitiveNode>> parentMaps) {
if (index == word.length()) {
return false;
}

char currentChar = word.charAt(index);
SensitiveNode currentNode = currentMap.get(currentChar);

if (currentNode == null) {
return false;
}

parentMaps.add(currentMap);

if (index == word.length() - 1) {
if (currentNode.isEnd()) {
if (!currentNode.hasChildren()) {
// 如果没有子节点,直接移除
Map<Character, SensitiveNode> parentMap = parentMaps.get(parentMaps.size() - 1);
parentMap.remove(currentChar);
} else {
// 如果有子节点,只标记为非结束节点
currentNode.setEnd(false);
}
log.info("敏感词库已移除/更新:{} 关键词", word);
return true;
}
return false;
}

return removeWordRecursive(word, index + 1, currentNode.getChildren(), parentMaps);
}

/**
* 判断文字是否包含敏感字符,默认:使用最小匹配规则
*/
public static boolean contains(String txt) {
return contains(txt, MIN_MATCH_TYPE);
}

/**
* 判断文本是否包含敏感词(可指定匹配规则)
* @param txt 待检测的文本
* @param matchType 匹配规则:MIN_MATCH_TYPE 或 MAX_MATCH_TYPE
* @return 如果包含敏感词返回true,否则返回false
*/
public static boolean contains(String txt, int matchType) {
if (StrUtil.isBlank(txt) || sensitiveWordMap == null || sensitiveWordMap.isEmpty()) {
return false;
}

// 可选:先过滤特殊字符再检查
String filteredTxt = filterSpecialStr(txt);
for (int i = 0; i < filteredTxt.length(); i++) {
if (checkWord(filteredTxt, i, matchType) > 0) {
return true;
}
}
return false;
}

/**
* 获取文本中包含的所有敏感词
*/
public static Set<String> getSensitiveWord(String txt) {
return getSensitiveWord(txt, MIN_MATCH_TYPE);
}

public static Set<String> getSensitiveWord(String txt, int matchType) {
Set<String> resultSet = new LinkedHashSet<>();

if (StrUtil.isBlank(txt) || sensitiveWordMap == null || sensitiveWordMap.isEmpty()) {
return resultSet;
}

// 可选:先过滤特殊字符再检测
String filteredTxt = filterSpecialStr(txt);
for (int i = 0; i < filteredTxt.length(); i++) {
int length = checkWord(filteredTxt, i, matchType);
if (length > 0) {
resultSet.add(filteredTxt.substring(i, i + length));
i += length - 1;
}
}
return resultSet;
}

/**
* 替换敏感字字符 - 按单个字符替换
*/
public static String replaceWordByOne(String txt, String replaceValue) {
return replaceWord(txt, replaceValue, MAX_MATCH_TYPE, false);
}

/**
* 替换敏感字字符 - 按整个词替换
*/
public static String replaceWordByAll(String txt, String replaceStr) {
return replaceWord(txt, replaceStr, MAX_MATCH_TYPE, true);
}

/**
* 替换敏感字字符
*/
public static String replaceWord(String txt, String replaceValue, int matchType, boolean isReplaceAll) {
if (StrUtil.isBlank(txt) || sensitiveWordMap == null || sensitiveWordMap.isEmpty()) {
return txt;
}

StringBuilder resultBuilder = new StringBuilder(txt);
Set<String> sensitiveWords = getSensitiveWord(txt, matchType);

for (String word : sensitiveWords) {
String replacement = isReplaceAll ? replaceValue :
String.valueOf(replaceValue).repeat(word.length());
int startIndex;
while ((startIndex = resultBuilder.indexOf(word)) != -1) {
resultBuilder.replace(startIndex, startIndex + word.length(), replacement);
}
}

return resultBuilder.toString();
}

/**
* 替换敏感字字符(先过滤特殊字符)
*/
public static String replaceWordWithFilter(String txt, String replaceValue, int matchType, boolean isReplaceAll) {
if (StrUtil.isBlank(txt)) {
return txt;
}

// 先过滤特殊字符
String filteredTxt = filterSpecialStr(txt);
return replaceWord(filteredTxt, replaceValue, matchType, isReplaceAll);
}

/**
* 校验文字是否包含敏感词
*/
private static int checkWord(String txt, int beginIndex, int matchType) {
if (sensitiveWordMap == null) {
return 0;
}

int matchFlag = 0;
boolean foundEnd = false;
Map<Character, SensitiveNode> currentMap = sensitiveWordMap;

for (int i = beginIndex; i < txt.length(); i++) {
char currentChar = txt.charAt(i);
SensitiveNode node = currentMap.get(currentChar);

if (node == null) {
break;
}

matchFlag++;
if (node.isEnd()) {
foundEnd = true;
if (matchType == MIN_MATCH_TYPE) {
break;
}
}
currentMap = node.getChildren();
}

return foundEnd ? matchFlag : 0;
}

/**
* 过滤特殊字符与空格
* 修正后的方法,现在在 contains 和 getSensitiveWord 中被使用
*/
public static String filterSpecialStr(String str) {
if (StrUtil.isBlank(str)) {
return str;
}
return SPECIAL_CHAR_PATTERN.matcher(str).replaceAll("");
}

/**
* 读取敏感词文件
*/
public static Set<String> readFile(List<String> filePaths) {
Set<String> result = new LinkedHashSet<>();

if (filePaths == null || filePaths.isEmpty()) {
return result;
}

for (String filePath : filePaths) {
readSingleFile(filePath, result);
}

return result;
}

private static void readSingleFile(String filePath, Set<String> result) {
File file = new File(filePath);
if (!file.exists() || !file.isFile()) {
log.warn("找不到指定的文件: {}", filePath);
return;
}

int count = 0;
try (FileInputStream fis = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(fis, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(reader)) {

String line;
while ((line = bufferedReader.readLine()) != null) {
if (StrUtil.isNotBlank(line)) {
String trimmedLine = line.trim();
result.add(trimmedLine);
count++;
}
}

log.info("加载文件:{},成功加载 {} 个敏感词", filePath, count);

} catch (Exception e) {
log.error("加载敏感词文件出错: {}", filePath, e);
}
}

/**
* 获取当前敏感词数量(估算)
*/
public static int getSensitiveWordCount() {
if (sensitiveWordMap == null) {
return 0;
}
return countWords(sensitiveWordMap);
}

private static int countWords(Map<Character, SensitiveNode> nodeMap) {
int count = 0;
for (SensitiveNode node : nodeMap.values()) {
if (node.isEnd()) {
count++;
}
count += countWords(node.getChildren());
}
return count;
}


/**
* 测试方法
*/
public static void main(String[] args) {
testSensitiveWord();
System.out.println(JSON.toJSONString(sensitiveWordMap));
}

private static void testSensitiveWord() {
// 加载敏感词库
Set<String> sensitiveWordSet = new HashSet<>();
sensitiveWordSet.add("代开发票");
sensitiveWordSet.add("代购");
sensitiveWordSet.add("代购商");
sensitiveWordSet.add("敏感词");

SensitiveWordUtil.load(sensitiveWordSet);

String content = "结束标志位结束代购商!标职业志位结束#标$敏感%位结束标志代开$发票位代购结束标志位结束苹果标";

// 测试过滤特殊字符
System.out.println("原始文本: " + content);
System.out.println("过滤后文本: " + filterSpecialStr(content));

// 检测敏感词
boolean isContains = contains(content);
Set<String> sensitiveWords = getSensitiveWord(content);
System.out.println("文本包含敏感词: " + isContains);
System.out.println("发现的敏感词: " + sensitiveWords);

// 替换敏感词
System.out.println("按字符替换: " + replaceWordByOne(content, "*"));
System.out.println("按词替换: " + replaceWordByAll(content, "***"));

// 测试带过滤的替换
System.out.println("带过滤的替换: " + replaceWordWithFilter(content, "*", MAX_MATCH_TYPE, false));

System.out.println("当前敏感词数量: " + getSensitiveWordCount());
}
}
  • 子节点新建一个内部类SensitiveNode,新增特殊符号过滤后,再匹配

四、总结

在日常开发中,敏感词过滤是一个常见的需求。无论是社交平台、论坛还是电商系统,都需要对用户输入的内容进行敏感词检测。

DFA算法通过树形结构和状态转移的理念,将敏感词过滤的时间复杂度大幅降低,特别适合处理大规模敏感词库的场景。

一、前言

Elasticsearch 也是支持脚本查询的。像查询的时候,有时简单的字段排序已经不满足我们的需求了,也可以使用脚本自定义表达式排序。
当然它也不只用来做排序而已,还有更新删除都是可以的。实战走起!!!

阅读全文 »

一、snapshot 简介

在网络中最重要的是数据,所有在存储应用中,定期的做数据备份还是很有必要的。而snapshot 就是Elasticsearch为了做数据备份用的api。翻译过来就是快照。
ES的高可用是通过多节点多副本来实现的,但是副本的数据并没有提供灾难性故障的保护。所有提供一个快照api是很有必要。
比如集群数据转移功能,通过snapshot 数据拷贝还是很方便的。

阅读全文 »