敏感词检测

Olivia的小跟班 Lv3

题记

本文主要用于记录如何在go中使用敏感词检测和对底层源码的分析。主要参考的GitHub仓库是:github.com/feiin/sensitivewords

用法

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
package main

import (
"fmt"

"github.com/feiin/sensitivewords"
)

func main() {
sensitive := sensitivewords.New()
/*
* keywords.txt:
* 尼玛
* 哈哈
*/
sensitive.LoadFromFile("./keywords.txt")

sensitive.AddWord("测试")
sensitive.AddWords("+q", "+v")

s, keyword := sensitive.Find("测试啊+q/+v,尼玛,哈哈")
fmt.Printf("Find:%v, %v\n", s, keyword) //true,测试
s, results := sensitive.FindAll("测试啊+q/+v,尼玛,哈哈")
fmt.Printf("FindAll:%v, %v\n", s, results) //true, [测试 +q +v 尼玛 哈哈哈]
s, results = sensitive.FindAny("测试啊+q/+v,尼玛,哈哈", 3)
fmt.Printf("FindAny:%v, %v\n", s, results) //true, [测试 +q +v]

s = sensitive.Check("测试啊+q/+v,尼玛,哈哈")
fmt.Printf("Check: %v\n", s) //true

str := sensitive.Filter("测试啊+q/+v,尼玛,哈哈")
fmt.Printf("Filter:%v\n", str) //**啊**/**,**,**
}

这是一个实例,来展示Go如何使用敏感词检测技术。

底层源码剖析

该图片是本文仓库的文件目录。

image-20230619194424776

在这里我们主要是分析trie.go文件。

keywords.txt主要是敏感词测试文件

1
2
3
4
5
卧槽
尼玛
测试
titor
testosterone

doc.go里面没有代码

image-20230619194629502

example_test.go,sensitive_words_test.go,trie_test.go,这些均为测试代码,我们不做分析处理。

sensitive_words.go文件主要是实例化一个敏感词库,加载敏感词库的文件(keywords.txt)并调用Add方法添加到敏感词库中。对我们了解敏感词检测的实现意义不大。我们不做多的学习。

trie.go是我们接下来主要要探究的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//TrieTree Trie树
type TrieTree struct {
Root *TrieNode
}

//TrieNode 树节点
type TrieNode struct {
IsEnd bool
Children map[rune]*TrieNode
}

//NewTrieTree
func NewTrieTree() *TrieTree {
return &TrieTree{
Root: &TrieNode{
IsEnd: false,
Children: make(map[rune]*TrieNode),
},
}
}

首先定义了两个结构体:TrieTree和TrieNode。TrieTree表示整个Trie树,其中包含一个指向根节点的指针Root。TrieNode表示树中的节点,其中包含一个布尔值IsEnd用于标识当前节点是否为一个敏感词的结束节点,以及一个Children映射表,用于存储当前节点的子节点。

接下来定义了NewTrieTree函数,用于创建一个新的TrieTree对象。该函数会初始化根节点,并返回TrieTree对象的指针。

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
//Add 添加敏感词
func (tree *TrieTree) Add(word string) {

if word == "" {
return
}

treeNode := tree.Root
var treeWords = []rune(word)
current := treeNode
for _, word := range treeWords {
if node, ok := current.Children[word]; ok {
current = node

} else {
newNode := &TrieNode{
IsEnd: false,
Children: make(map[rune]*TrieNode),
}
current.Children[word] = newNode
current = newNode
}
}
current.IsEnd = true //end
}

该方法接收一个字符串参数 word,表示要添加的敏感词。首先,通过检查 word 是否为空字符串,如果为空,则直接返回,不执行任何操作。然后,定义一个指针变量 treeNode,指向 Trie 树的根节点 tree.Root,用于表示当前节点。将要添加的敏感词 word 转换为一个 rune 类型的切片 treeWords,方便按字符遍历敏感词。然后,定义一个指针变量 current,指向当前节点 treeNode,用于表示当前遍历的节点。接下来,通过 for 循环遍历敏感词的每个字符。在每次循环中,将当前字符存储在 word 变量中。通过检查当前节点的 Children 字段,判断当前字符是否已经是当前节点的子节点之一。如果是,则更新 current 为当前字符对应的子节点。如果当前字符不是当前节点的子节点,则表示该字符还未在当前节点的子节点中,需要创建一个新的节点。创建一个新的节点 newNode,设置其 IsEnd 字段为 false,并初始化其 Children 字段为一个空的 map[rune]*TrieNode。将新节点 newNode 添加到当前节点 currentChildren 字段中,键为当前字符 word。然后,将 current 更新为新添加的节点 newNode,以便继续遍历敏感词的下一个字符。循环结束后,表示敏感词的所有字符都已经遍历完毕。将最后一个字符对应的节点 currentIsEnd 字段设置为 true,表示该节点是敏感词的结尾节点。

AddWords函数本质上就是调用Add方法。

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
//Filter 过滤敏感词为*
func (tree *TrieTree) Filter(input string) string {
words := []rune(input)

var current *TrieNode
var found bool
treeNode := tree.Root
offset := 0

for i := 0; i < len(words); i++ {

w := words[i]
current, found = treeNode.Children[w]

if !found {

i = i - offset //fallback
offset = 0
treeNode = tree.Root
continue
}

if current.IsEnd {
//found

for j := i - offset; j < i+1; j++ {
words[j] = rune('*')
}

offset = 0
treeNode = tree.Root
continue

}

offset++
treeNode = current

}

return string(words)
}

image-20230620005309316

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
//Find 查找敏感词,找到第一个就退出
func (tree *TrieTree) Find(input string) (sensitive bool, keyword string) {
words := []rune(input)

var current *TrieNode
var found bool
treeNode := tree.Root
offset := 0

for i := 0; i < len(words); i++ {

w := words[i]
current, found = treeNode.Children[w]

if !found {

i = i - offset //fallback
offset = 0
treeNode = tree.Root
continue
}

if current.IsEnd {
//found
return true, string(words[i-offset : i+1])

}

offset++
treeNode = current

}

return false, ""
}

该方法接收一个字符串参数 input,表示要进行敏感词查找的输入文本。首先,将输入文本 input 转换为一个 rune 类型的切片 words,方便按字符遍历文本。然后,定义变量 current(指向当前节点)、found(表示是否找到字符)、treeNode(指向 Trie 树的根节点)、offset(表示当前遍历的字符偏移量)。通过一个 for 循环遍历输入文本的每个字符。在每次循环中,将当前字符存储在变量 w 中。通过检查当前节点的 Children 字段,判断当前字符是否是当前节点的子节点之一。如果是,则更新 current 为当前字符对应的子节点,并将 found 设置为 true。如果当前字符不是当前节点的子节点,则表示该字符不在当前节点的子节点中,需要进行回溯。将循环索引 i 减去偏移量 offset,以回退到上一个已匹配的位置。将偏移量 offset 重置为 0,表示回退后的位置。将当前节点 treeNode 更新为 Trie 树的根节点,以重新开始匹配过程。循环结束后,表示整个文本已经遍历完毕。如果最后一个字符对应的节点 current 是敏感词的结尾节点(current.IsEndtrue),则表示找到了敏感词。此时,返回结果为 true 和找到的敏感词,即 string(words[i-offset : i+1])。如果循环结束时没有找到敏感词,则返回结果为 false 和一个空字符串。通过上述操作,方法可以在输入文本中查找敏感词,并返回是否找到敏感词以及找到的第一个敏感词。请注意,此方法只会找到第一个敏感词并立即退出,而不会继续查找其他可能存在的敏感词。

FindAll函数本质上就是定义了一个[]string类型results,每次找到敏感词就append进results。

FindAny函数本质上就是定义了一个[]string类型results,每次找到敏感词就append进results,只不过他会判断len(result)==count

  • 标题: 敏感词检测
  • 作者: Olivia的小跟班
  • 创建于 : 2023-06-19 19:39:10
  • 更新于 : 2023-06-20 00:57:28
  • 链接: https://www.youandgentleness.cn/2023/06/19/敏感词检测/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
敏感词检测