Система пулевого экрана от 0 до 1 - фильтрация чувствительных слов

Go

Система маркированного экрана должна иметь фильтр чувствительных слов или контроль риска контента, иначе, как вы знаете.

Итак, сегодня мы реализуем фильтрацию чувствительных слов в заграждении. Почему бы вам не использовать управление рисками?Конечно, это для экономии затрат, и вам нужны деньги для управления рисками. Конечно, если у вас есть деньги и вам все равно, вы можете взять на себя управление рисками. Таким образом, уровень безопасности выше.Мы написали эту систему заграждения для обучения, поэтому мы используем чувствительные слова для фильтрации и учимся фильтровать чувствительные слова.

Фильтрация чувствительных слов на самом деле представляет собой процесс заполнения учетных данных. Чтобы добиться заполнения учетных данных, сначала необходимо установить библиотеку словарей. Большинство текущих решений предназначены для использованияTrie Treeдля создания библиотеки словарей.

Trie Tree

В компьютерных науках дерево, также известное как дерево префиксов или дерево словарей, представляет собой упорядоченное дерево, используемое для хранения ассоциативных массивов, ключами которых обычно являются строки. В отличие от бинарного дерева поиска, ключ не хранится непосредственно в узле, а определяется положением узла в дереве. Все потомки узла имеют одинаковый префикс, представляющий собой строку, соответствующую этому узлу, а корневой узел соответствует пустой строке. Как правило, не все узлы имеют соответствующие значения, только ключи, соответствующие листовым узлам, и некоторые внутренние узлы имеют соответствующие значения.

Структура trie, содержащая 8 клавиш: «A», «to», «tea», «ted», «ten», «i», «in», «inn».

Ключи не нужно явно хранить в узле. Полное слово отмечено на иллюстрации, просто чтобы продемонстрировать принцип trie.

Взято из вышеперечисленногоВикипедия.

Реализовать дерево проб

определение структуры

type Trie struct {
	Word rune
	IsEnd bool
	Child map[rune]*Trie
}

Wordпредставляет значение узла,IsEndУказывает, является ли это листовым узлом,ChildПредставляет дочерний узел.

WordПочему используются переменныеruneвведите вместо использованияbyteЧто насчет типа?

так какbyteОбозначает байты, но количество байтов в китайском и английском языках разное, см. следующий пример:

first := "社区"
fmt.Println([]byte(first)) // 输出结果[231 164 190 229 140 186],中文字符串每个占三个字节

second := "first"
fmt.Println([]byte(second)) // 输出结果[102 105 114 115 116],每个英文字符占一个字节

Итак, если вы используетеbyteЕсли это так, нам нужно различать китайский и английский языки и смотреть на следующий пример:

first := "社区"
fmt.Println([]rune(first)) // 输出结果[31038 21306],每个中文字符串占一个标识符

second := "first"
fmt.Println([]rune(second)) // 输出结果[102 105 114 115 116],每个英文字符占一个标识符

Официальное описание этих двух типов:

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

в официальномtourесть предложение runeвыражатьunicodeКодовая точка, чтобы нам не нужно было различать китайский и английский языки.

Вставка Trie

вставить деликатное словоTrie, нужно сначала поискатьTrie, чтобы определить, существует ли уже каждый символ чувствительного слова вTrie, если он уже существует, продолжайте движение внизTrie, если он не существует, вставьте:

func (t *Trie) Insert(word string) {
	word = strings.TrimSpace(word)
	ptr := t
	for _, u := range word {
		_, ok := ptr.Child[u]
		if !ok {
			node := make(map[rune]*Trie)
			ptr.Child[u] = &Trie{Word: u, Child: node}
		}
		ptr = ptr.Child[u]
	}
	ptr.IsEnd = true
}

Давайте распечатаем его, чтобы увидеть, правильно ли он был вставлен:

func (t *Trie) Walk() {
	var walk func(string, *Trie)
	walk = func(pfx string, node *Trie) {
		if node == nil {
			return
		}

		if node.Word != 0 {
			pfx += string(node.Word)
		}

		if node.IsEnd {
			fmt.Println(string(pfx))
		}

		for _, v := range node.Child {
			walk(pfx, v)
		}
	}
	walk("", t)
}

func main() {
	trie := Trie{Word: 0, Child: make(map[rune]*Trie)}
	trie.Insert("大姨妈")
	trie.Insert("姨妈jin")
	trie.Insert("你大爷")
	trie.Insert("bitch")
	trie.Insert("bitches")
	trie.Walk()
}

выходной результат

$ go run trie.go
大姨妈
姨妈jin
你大爷
bitch
bitches

Как видите, он вставлен правильноtrie.

Попробуйте поиск

Введите предложение и узнайте, содержит ли предложение чувствительные слова

func (t *Trie) Search(segment string) []string {
	segment = strings.TrimSpace(segment)
    ptr := t
	var matched []string
	item := ""
	for _, u := range segment {
		c, ok := ptr.Child[u]
		if !ok {
			ptr = t
			continue
		}
		item += string(c.Word)
	
		if c.IsEnd {
			matched = append(matched, item)
		}
		ptr = c
	}
	return matched
}

Давайте проверим это снова, на этот раз мы используемgo testтестировать:

import (
	"testing"
)

var trieTree = Trie{Word: 0, Child: make(map[rune]*Trie)}

func TestTrie(t1 *testing.T) {
	trieTree.Insert("你大爷")
	trieTree.Insert("大姨妈")
	trieTree.Insert("姨妈jin")
	trieTree.Insert("jin子")
	trieTree.Insert("大姨父")
	trieTree.Insert("妈了个吧")
	trieTree.Insert("狗日的")
	trieTree.Insert("去你吗的")
	trieTree.Insert("bitch")
	trieTree.Insert("bitches")

	//trieTree.Walk()

	matched := trieTree.Search("你大爷")
	t1.Log(matched)
	if len(matched) != 1 || matched[0] != "你大爷" {
		t1.Errorf("search: %s, expected: %v, actual: %v", "你大爷", []string{"你大爷"}, matched)
	}
}

Структура этого дерева должна быть такой

$ go test -v .
=== RUN   TestTrie
    trie_test.go:24: [你大爷]
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.012s

плюс-vпараметры, см. подробности.

Другими словами, тест,

matched := trieTree.Search("英文单词bitches意思是母狗")
t1.Log(matched)
if len(matched) != 2 || matched[0] != "bitch" || matched[1] != "bitches" {
    t1.Errorf("search: %s, expected: %v, actual: %v", "英文单词bitches意思是母狗", []string{"bitch", "bitches"}, matched)
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:30: [bitch bitches]
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.012s

Переходим к следующему тесту:

matched := trieTree.Search("狗日的大姨妈啊")
t1.Log(matched)
if len(matched) != 2 || matched[0] != "狗日的" || matched[1] != "大姨妈" {
    t1.Errorf("search: %s, expected: %v, actual: %v", "狗日的大姨妈啊", []string{"狗日的", "大姨妈"}, matched)
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:36: [狗日的]
    trie_test.go:38: search: 狗日的大姨妈啊, expected: [狗日的 大姨妈], actual: [狗日的]
--- FAIL: TestTrie (0.00s)
FAIL
FAIL    danmaku/utilities       0.012s
FAIL

Тест провален! Попал только в "собачий день", а последний не попал, почему так?

Давайте проанализируем:

segment[0] = "狗"
segment[1] = "日"
segment[2] = "的"

До этого момента это нормально, продолжайте:

segment[3] = "大"

В этот момент курсор для обхода дерева должен быть здесь:Поэтому, когда попадается чувствительное слово, нам нужно сбросить курсор:

				if c.IsEnd {
                    matched = append(matched, item)
                    // 重置检索起点
                    ptr = t
                    continue
                }
$ go test -v .
=== RUN   TestTrie
    trie_test.go:36: [狗日的 狗日的大姨妈]
    trie_test.go:38: search: 狗日的大姨妈啊, expected: [狗日的 大姨妈], actual: [狗日的 狗日的大姨妈]
--- FAIL: TestTrie (0.00s)
FAIL
FAIL    danmaku/utilities       0.012s
FAIL

или нет, мы ожидаем, что результат будет[狗日的 大姨妈], но фактически полученный результат[狗日的 狗日的大姨妈], ударил раньше狗日的сводится к следующим результатам. Таким образом, мы можем не только сбросить полученный курсор, но и сбросить соответствующий результат:

if c.IsEnd {
    matched = append(matched, item)
    if len(c.Child) == 0 {
        // 重置检索起点
        item = ""
        ptr = t
        continue
    }
}
$ go test -v .
=== RUN   TestTrie
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.012s

Тест пройден!

Перейти к следующему варианту использования

matched := trieTree.Search("我去你大爷的")
t1.Log(matched)
if len(matched) != 1 {
    t1.Errorf("search: %s, expected: %v, actual: %v", "我去你大爷的", []string{"你大爷"}, matched)
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:42: []
    trie_test.go:44: search: 我去你大爷的, expected: [你大爷], actual: []
--- FAIL: TestTrie (0.00s)
FAIL
FAIL    danmaku/utilities       0.012s
FAIL

Не удалось и не получили желаемого результата. Почему это?

Рассмотрим подробнее параметрыsegmentЗначение "Я пойду к твоему дяде", траверсsegment,

segment[0] = "我"
segment[1] = "去"

В это время попал в словарь:

Продолжить вниз:

segment[2] = "你"
segment[3] = "大"

Курсор дерева опускается, указывая на,Потомsegment[3]не соответствует. но,segment[2]должен ударить你大爷из

В этот момент, если мы просто сбросим курсор дерева, потому чтоsegmentКурсор уже в3Здесь все равно не работает. Таким образом, нам нужно не только сбросить курсор дерева, но и сброситьsegmentкурсор. ноrangeЕго можно пройти только спереди назад, но не сзади, поэтому мы не можем использоватьrangeпересечь, использовать вместо этогоfor. но,forПри обходе строки она проходится байтами, поэтому вам нужно преобразовать строку вruneтип:

func (t *Trie) Search(segment string) []string {
	segment = strings.TrimSpace(segment)
	segmentRune := []rune(segment)
	var matched []string

	ptr := t
	item := ""
	index := 0
	for i:=0; i < len(segmentRune); i++ {
		c, ok := ptr.Child[segmentRune[i]]
		if !ok {
			i = index
			index++
			item = ""
			ptr = t
			continue
		}

		item += string(c.Word)

		// 例如:bitch和bitches
		// {Word: b, IsEnd: false, Child: {}}
		//                            ↓
		//   {Word: i, IsEnd: false, Child: {}}
		//                              ↓
		//     {Word: t, IsEnd: false, Child: {}}
		//                                ↓
		//       {Word: c, IsEnd: false, Child: {}}
		//                                  ↓
		//         {Word: h, IsEnd: true, Child: {}}
		//                                  ↓
		//         {Word: e, IsEnd: false, Child: {}}
		//                                    ↓
		//           {Word: s, IsEnd: true, Child: {}}
		if c.IsEnd {
			matched = append(matched, item)
			if len(c.Child) == 0 {
				item = ""
				ptr = t
				continue
			}
		}
		ptr = c
	}
	return matched
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:42: [你大爷]
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.012s

Тест пройден!

Перейти к следующему набору тестов (устал, ε=(´ο`*))) увы~~)

matched := trieTree.Search("大姨妈jin子")
t1.Log(matched)
if len(matched) != 3 {
    t1.Errorf("search: %s, expected: %v, actual: %v", "大姨妈jin子", []string{"大姨妈", "姨妈jin", "jin子"}, matched)
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:48: [大姨妈 jin子]
    trie_test.go:50: search: 大姨妈jin子, expected: [大姨妈 姨妈jin jin子], actual: [大姨妈 jin子]
--- FAIL: TestTrie (0.00s)
FAIL
FAIL    danmaku/utilities       0.012s
FAIL

провалил испытание,姨妈jinПропущенный.

Проанализируем еще раз:

segment[0] = "大"
segment[1] = "姨"
segment[2] = "妈"

На данный момент программа здесь:

if c.IsEnd {
    matched = append(matched, item)
    if len(c.Child) == 0 {
        item = ""
        ptr = t
        continue
    }
}

Курсор trie сбрасывается и продолжается вниз:

segment[3] = "j"

В это время ударитьjin子изj, затем продолжайте вниз до точного совпаденияjin子.

Итак, мы не можем просто сбросить, где мы не совпалиsegmantКурсор также сбрасывается при совпадении чувствительного слова.segmentкурсор:

if c.IsEnd {
    matched = append(matched, item)
    if len(c.Child) == 0 {
        i = index
        index++
        item = ""
        ptr = t
        continue
    }
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:48: [大姨妈 姨妈jin jin子]
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.012s

Тест пройден!

matched := trieTree.Search("我很正常")
t1.Log(matched)
if len(matched) > 0 {
    t1.Errorf("search: %s, expected: %v, actual: %v", "我很正常", "", matched)
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:54: []
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.012s

Тест пройден! Наконец полегчало!

Удаление Trie

Удаление Trie очень просто.Сначала пройдите Trie, найдите чувствительные слова, которые необходимо удалить, запишите путь в процессе обхода, а затем удалите сзади наперед:

func (t *Trie) Delete(word string) {
	word = strings.TrimSpace(word)
	var branch []*Trie
	ptr := t
	for _, u := range word {
		branch = append(branch, ptr)
		c, ok := ptr.Child[u]
		if !ok {
			return
		}
		ptr = c
	}

	// 只命中字典中部分词
	if !ptr.IsEnd {
		return
	}

	// 如bitch和bitches
	// 删除bitch时,只需要将bitch最后一个节点的IsEnd改为false即可
	if len(ptr.Child) != 0 {
		ptr.IsEnd = false
		return
	}

	for len(branch) > 0 {
		p := branch[len(branch) - 1]
		branch = branch[:len(branch) - 1]

		delete(p.Child, ptr.Word)
        // IsEnd == true 如bitch和bitches,删除bitches时,只需要删除后面的"es"即可
        // len(Child) != 0 整个敏感词全删除
		if p.IsEnd || len(p.Child) != 0 {
			break
		}
		ptr = p
	}
}

Протестируйте еще раз:

trieTree.Delete("你大爷")
matched := trieTree.Search("你大爷")
t1.Log(matched)
if len(matched) != 0 {
    t1.Errorf("search: %s, expected: %v, actual: %v", "你大爷", []string{"你大爷"}, matched)
}
$ go test -v .
=== RUN   TestTrie
    trie_test.go:61: []
--- PASS: TestTrie (0.00s)
PASS
ok      danmaku/utilities       0.011s

Ну наконец то свершилось! ! !

фильтровать чувствительные слова

danmakuMsg := c.filter(rsvData.Msg)
func (c *Client) filter(msg string) string {
	matched := trieTree.Search(msg)
	if len(matched) != 0 {
		var oldNew []string
		for _, v := range matched {
			oldNew = append(oldNew, v, "***")
		}
		replacer := strings.NewReplacer(oldNew...)
		return replacer.Replace(msg)
	}
	return msg
}

Добавьте еще один HTTP-интерфейс, чтобы добавить конфиденциальные слова:

http.HandleFunc("/illegal", illegalHandle)
func illegalHandle(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/illegal" {
		http.Error(w, "Not found", http.StatusNotFound)
		return
	}
	if r.Method != "POST" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	words := r.PostFormValue("words")
	fmt.Println(words)
	if words == "" {
		http.Error(w, "words值不能为空", http.StatusBadRequest)
		return
	}
	trieTree.Insert(words)
	w.Write([]byte{})
}

Отправить шквал «Тетя Джинзи»

Готово!

Код отправлен наgiteeсклад.