敏感词过滤,算是一个比较常见的功能,尤其是在内容、社交类应用中更是如此。本文介绍如何使用Go语言实现简单的敏感词过滤功能。
简单敏感词过滤-ai版
先列出一个gpt给出来的一个简单前缀树的实现:
go
// 初始化敏感词切片
var sensitiveWords = []string{}
// TrieNode 表示Trie树的节点
type TrieNode struct {
children map[rune]*TrieNode
isEnd bool
Text string
}
// Trie 表示敏感词的Trie树
type Trie struct {
root *TrieNode
}
// NewTrie 创建一个新的Trie树
func NewTrie() *Trie {
return &Trie{
root: &TrieNode{
children: make(map[rune]*TrieNode),
isEnd: false,
},
}
}
// Insert 将一个敏感词插入到Trie树中
func (t *Trie) Insert(word string) {
node := t.root
for _, char := range []rune(word) {
if _, ok := node.children[char]; !ok {
node.children[char] = &TrieNode{
children: make(map[rune]*TrieNode),
isEnd: false,
}
}
node = node.children[char]
}
node.Text = word
node.isEnd = true
}
// Contains 检测文本中是否包含敏感词
func (t *Trie) Contains(text string) bool {
node := t.root
for _, char := range []rune(text) {
if _, ok := node.children[char]; !ok {
continue
}
node = node.children[char]
if node.isEnd {
return true
}
}
return false
}
这个版本的代码中,构建了一个简单的前缀树来存储敏感词,如果某个节点存储的是敏感词的最后一个字符,则isEnd值为true。这样,当我们检测到某个节点的isEnd值为true时,就说明检测到了敏感词。
如果只是为了检测到一段文本是否包含敏感词,而不需要匹配出所有的敏感词,那实际上在敏感词a包含敏感词b时,我们可以只存储单词b。
我们编写一个测试用例,测试一下上面的代码:
go
func TestCheckWord1(t *testing.T) {
trie := NewTrie()
for _, word := range sensitiveWords {
trie.Insert(word)
}
content := "这里是一段非法活动文本。"
search := trie.Contains(content)
assert.Equal(t, search, true)
}
测试结果如下:

测试通过。(再这样下去程序员真要失业了!)
当然,上面的代码不完善,例如:不是并发安全的、不支持删除敏感词、没有返回检测到的敏感词。我们来完善一下。
完善敏感词过滤
下面我们在上面的代码基础上,添加一些功能。
go
package sensitivewordcheck
import "sync"
// TrieV1Node 表示TrieV1树的节点
type TrieV1Node struct {
children map[rune]*TrieV1Node // 子节点
isEnd bool
Text string
Value rune
parent *TrieV1Node // 父节点
}
// TrieV1 表示敏感词的TrieV1树
type TrieV1 struct {
root *TrieV1Node
lock sync.RWMutex
}
// NewTrieV1 创建一个新的TrieV1树
func NewTrieV1() *TrieV1 {
return &TrieV1{
root: &TrieV1Node{
children: make(map[rune]*TrieV1Node),
isEnd: false,
},
}
}
// Insert 将一个敏感词插入到TrieV1树中
func (t *TrieV1) Insert(word string) {
t.lock.Lock()
defer t.lock.Unlock()
node := t.root
for _, char := range []rune(word) {
if _, ok := node.children[char]; !ok {
node.children[char] = &TrieV1Node{
children: make(map[rune]*TrieV1Node),
isEnd: false,
parent: node,
Value: char,
}
}
node = node.children[char]
}
node.Text = word
node.isEnd = true
}
// Contains 检测文本中是否包含敏感词
func (t *TrieV1) Contains(text string) bool {
t.lock.RLock()
defer t.lock.RUnlock()
node := t.root
for _, char := range []rune(text) {
if _, ok := node.children[char]; !ok {
continue
}
node = node.children[char]
if node.isEnd {
return true
}
}
return false
}
// Check 检测文本中是否包含敏感词,并返回第一个敏感词
func (t *TrieV1) Check(text string) string {
t.lock.RLock()
defer t.lock.RUnlock()
node := t.root
for _, char := range text {
if _, ok := node.children[char]; !ok {
continue
}
node = node.children[char]
if node.isEnd {
return node.Text
}
}
return ""
}
// Rebuild 重新构建敏感词树
func (t *TrieV1) Rebuild(words []string) {
t.lock.Lock()
defer t.lock.Unlock()
t.root = &TrieV1Node{}
for _, word := range words {
t.Insert(word)
}
}
// Delete 删除一个敏感词
func (t *TrieV1) Delete(word string) {
t.lock.Lock()
defer t.lock.Unlock()
node := t.root
for _, char := range []rune(word) {
if _, ok := node.children[char]; !ok {
return
}
node = node.children[char]
if node.isEnd {
node.isEnd = false
node.Text = ""
if len(node.children) > 0 { // 有子节点,不能删除
break
}
// 递归删除
t.doDel(node)
}
}
}
func (t *TrieV1) doDel(node *TrieV1Node) {
// 再次判断是否可以删除
if node == nil || len(node.children) > 0 {
return
}
// 从上级节点的children中删除本节点
delete(node.parent.children, node.Value)
// 判断上一层节点是否可以删除
t.doDel(node.parent)
}
在上面的版本中,我们添加了读写锁来保证并发安全,并且添加了删除敏感词的功能。
敏感词库的变更,是一个并不频繁的操作,而可以预见的时,敏感词库不会太大。所以,我们是否可以在敏感词库发生变更时,直接重构整个敏感词库,在重构完成后,再切换到新的敏感词库上呢?
测试代码:
go
package sensitivewordcheck
import (
"github.com/stretchr/testify/assert"
"testing"
)
var trieV1 *TrieV1
func init() {
trieV1 = NewTrieV1()
for _, word := range sensitiveWords {
trieV1.Insert(word)
}
}
func TestCheckWordAndDelete(t *testing.T) {
// 添加敏感词 非法捕鱼
trieV1.Insert("非法捕鱼")
assert.Equal(t, trieV1.Contains("你要去非法捕鱼吗?"), true)
// 添加敏感词 非法打猎
trieV1.Insert("非法打猎")
assert.Equal(t, trieV1.Contains("你要去非法打猎吗?"), true)
// 删除敏感词 非法打猎
trieV1.Delete("非法打猎")
// 不再包含 非法打猎
assert.Equal(t, trieV1.Contains("你要去非法打猎吗?"), false)
// 非法捕鱼 不受影响
assert.Equal(t, trieV1.Contains("你要去非法捕鱼吗?"), true)
// 更长的敏感词
trieV1.Insert("非法捕鱼工具")
assert.Equal(t, trieV1.Contains("你要去买非法捕鱼工具吗?"), true)
// 删除 非法捕鱼
trieV1.Delete("非法捕鱼")
assert.Equal(t, trieV1.Contains("你要去非法捕鱼吗?"), false)
// 如果有子节点,不删除
assert.Equal(t, trieV1.Contains("你要去买非法捕鱼工具吗?"), true)
}
上面的测试用例中,我们添加了添加、删除敏感词功能,并校验了删除敏感词的正确性,以及在有更长的敏感词时是否会无删除。 上述用例在本机测试通过。
后记
以上,我们实现了一个简单的敏感词过滤功能。实际上,敏感词过滤还可以做得更复杂,添加更多功能,比如,检测拼音、过滤特殊字符等等。这些功能,可以在上面的代码基础上,自行扩展。但是需要考虑的是:扩展功能的同时,是否会影响性能,尤其是在检测超长文本时。
本文由mdnice多平台发布