Система маркированного экрана должна иметь фильтр чувствительных слов или контроль риска контента, иначе, как вы знаете.
Итак, сегодня мы реализуем фильтрацию чувствительных слов в заграждении. Почему бы вам не использовать управление рисками?Конечно, это для экономии затрат, и вам нужны деньги для управления рисками. Конечно, если у вас есть деньги и вам все равно, вы можете взять на себя управление рисками. Таким образом, уровень безопасности выше.Мы написали эту систему заграждения для обучения, поэтому мы используем чувствительные слова для фильтрации и учимся фильтровать чувствительные слова.
Фильтрация чувствительных слов на самом деле представляет собой процесс заполнения учетных данных. Чтобы добиться заполнения учетных данных, сначала необходимо установить библиотеку словарей. Большинство текущих решений предназначены для использования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склад.