在内容管理项目中,我们经常会用到敏感词检查/过滤服务,这里我们使用 Go 编写一个简单的敏感词过滤服务。

主要功能如下:

  1. 加载敏感词列表: 从文件或直接在代码中定义。
  2. 添加/删除敏感词: 允许动态修改敏感词列表。
  3. 文本过滤: 检测文本中是否包含敏感词,并进行替换或直接返回结果。

我们将使用 Trie (前缀树) 数据结构来提高查找效率。

以下是代码实现:

package main

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"sync"
)

// TrieNode  Trie树节点
type TrieNode struct {
	children map[rune]*TrieNode
	isEnd    bool
}

// Trie  Trie树
type Trie struct {
	root *TrieNode
	lock sync.RWMutex
}

// NewTrie  创建新的Trie树
func NewTrie() *Trie {
	return &Trie{
		root: &TrieNode{children: make(map[rune]*TrieNode)},
	}
}

// Insert  向Trie树中插入敏感词
func (t *Trie) Insert(word string) {
	t.lock.Lock()
	defer t.lock.Unlock()

	node := t.root
	for _, char := range word {
		if _, ok := node.children[char]; !ok {
			node.children[char] = &TrieNode{children: make(map[rune]*TrieNode)}
		}
		node = node.children[char]
	}
	node.isEnd = true
}

// Delete  从Trie树中删除敏感词
func (t *Trie) Delete(word string) {
	t.lock.Lock()
	defer t.lock.Unlock()

	node := t.root
	nodes := make([]*TrieNode, 0)
	chars := make([]rune, 0)
	for _, char := range word {
		if _, ok := node.children[char]; !ok {
			return // 如果没有找到,直接返回
		}
		nodes = append(nodes, node)
		chars = append(chars, char)
		node = node.children[char]
	}
	if !node.isEnd {
		return // 查找的不是一个词的结尾,不需要删除
	}

	if len(node.children) == 0 {
		for i := len(nodes) - 1; i >= 0; i-- {
			parentNode := nodes[i]
			delete(parentNode.children, chars[i])
			if len(parentNode.children) > 0 || parentNode == t.root {
				break
			}
		}
	} else {
		node.isEnd = false // 如果该节点还有子节点,仅取消isEnd
	}
}


// Contains  检查Trie树中是否存在敏感词
func (t *Trie) Contains(word string) bool {
    t.lock.RLock()
    defer t.lock.RUnlock()

    node := t.root
    for _, char := range word {
        if _, ok := node.children[char]; !ok {
            return false
        }
        node = node.children[char]
    }
    return node.isEnd
}


// Filter  过滤文本中的敏感词,并用替换符替换
func (t *Trie) Filter(text string, replaceRune rune) string {
	t.lock.RLock()
	defer t.lock.RUnlock()

    result := []rune(text)
    for i := 0; i < len(result); i++ {
        node := t.root
        start := i
        for j := i; j < len(result); j++ {
            char := result[j]
            if _, ok := node.children[char]; !ok {
                break
            }
            node = node.children[char]
            if node.isEnd {
                // 发现敏感词,替换
                for k := start; k <= j; k++ {
                    result[k] = replaceRune
                }
                i = j
                break
            }
        }
    }

    return string(result)
}


// LoadFromFile  从文件中加载敏感词
func (t *Trie) LoadFromFile(filePath string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            if err == io.EOF {
                break
            }
            return err
        }
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
        t.Insert(line)
    }
    return nil
}

func main() {
    trie := NewTrie()

    // 1. 从文件加载敏感词
    err := trie.LoadFromFile("sensitive_words.txt") 
    if err != nil {
        log.Println("加载敏感词文件失败:", err)
        // 如果加载失败,使用默认敏感词
		trie.Insert("fuck")
		trie.Insert("shit")
    }


	// 2. 添加/删除敏感词
    trie.Insert("尼玛")
    trie.Delete("shit")

	// 3. 文本过滤
    text1 := "这是一段测试文本,包含fuck和尼玛,还有一些正常内容。"
    filteredText1 := trie.Filter(text1, '*')
    fmt.Println("过滤前:", text1)
    fmt.Println("过滤后:", filteredText1)

	text2 := "你好,世界,没有敏感词!"
    filteredText2 := trie.Filter(text2, '*')
    fmt.Println("过滤前:", text2)
    fmt.Println("过滤后:", filteredText2)
	
	// 4. 测试 Contains
	fmt.Println("Contains 'fuck':", trie.Contains("fuck")) // true
	fmt.Println("Contains 'shit':", trie.Contains("shit")) // false
	fmt.Println("Contains '尼玛':", trie.Contains("尼玛")) // true
}

说明:

  1. 创建 sensitive_words.txt 文件: 将敏感词一行一个放入此文件中。
  2. 编译并运行代码: go run main.go
  3. 测试结果: 查看控制台输出,可以看到敏感词被替换为了 *

代码解析:

  • TrieNodeTrie: 定义了 Trie 树的数据结构。
  • Insert(word string): 向 Trie 树插入一个敏感词。
  • Delete(word string): 从Trie树中删除敏感词。
  • Contains(word string): 检查 Trie 树是否包含指定敏感词。
  • Filter(text string, replaceRune rune): 过滤文本中的敏感词,并用 replaceRune 替换。
  • LoadFromFile(filePath string): 从指定文件路径加载敏感词,一行一个。
  • main() 函数: 演示了如何使用这个敏感词过滤服务。

优点:

  • 高效: 使用 Trie 树进行查找,时间复杂度接近 O(n),n为敏感词长度,相比于直接匹配,效率更高。
  • 可扩展: 可以很容易地添加新的敏感词。
  • 线程安全: 使用 sync.RWMutex 实现了并发安全的读写操作。

进一步优化:

  • 忽略大小写和空格: 可以对输入的文本和敏感词进行预处理,转换为小写并去除空格。
  • 支持多种替换方式: 可以根据需求选择不同的替换符或替换规则。
  • 使用高性能的第三方库: 可以使用高性能的 Trie 树库,例如 github.com/emirpasic/gods
  • 配置化: 将配置(如文件路径,替换符)放入配置文件中。
  • API 服务: 将此服务封装成 API 供其他应用调用。

注意:

  • 这个示例是一个简单的敏感词过滤实现,在实际应用中可能需要考虑更多的复杂场景,如:
    • 同音字、形近字: 可以考虑使用模糊匹配或更复杂的 NLP 技术来处理。
    • 组合敏感词: 需要考虑不同组合方式的敏感词。
    • 上下文: 有些词在特定上下文中才是敏感的。
  • 为了安全和完整性,敏感词列表需要定期更新和维护。

你可以根据自己的需求进行修改和扩展,希望这个例子能够帮助你更好的理解和完善 敏感词过滤服务!