使用Go语言实现简单敏感词过滤

敏感词过滤,算是一个比较常见的功能,尤其是在内容、社交类应用中更是如此。本文介绍如何使用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多平台发布

相关推荐
灵感__idea4 小时前
JavaScript高级程序设计(第5版):无处不在的集合
前端·javascript·程序员
dmy6 小时前
n8n内网快速部署
运维·人工智能·程序员
憨憨睡不醒啊8 小时前
如何让LLM智能体开发助力求职之路——构建属于你的智能体开发知识体系📚📚📚
面试·程序员·llm
程序员岳焱10 小时前
Java 程序员成长记(二):菜鸟入职之 MyBatis XML「陷阱」
java·后端·程序员
liangdabiao11 小时前
让AI写出真正可用的图文并茂的帖子(微信公众号,小红书,博客)
程序员
安妮的心动录11 小时前
人是习惯的结果
面试·程序员·求职
小兵张健12 小时前
笔记本清灰记录
程序员
陈随易15 小时前
Univer v0.8.0 发布,开源免费版 Google Sheets
前端·后端·程序员
陈随易2 天前
Element Plus 2.10.0 重磅发布!新增Splitter组件
前端·后端·程序员
陈随易2 天前
2025年100个产品计划之第11个(哆啦工具箱) - 像哆啦A梦口袋一样丰富的工具箱
前端·后端·程序员