前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽。

  基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成“***”就可以了。对于子串的查找,就KMP算法就可以了。但是敏感词这么多,总不能一个一个地遍历看看里面有没有相应的词吧!

  于是我想到了前几天写的字典树。如果把它改造一下,并KMP算法结合,似乎可以节约不少时间。

  首先说明一下思路:

  对于KMP算法,这里不过多阐述。对于敏感词库,如果把它存进字典树,并在每个节点存上它的next值。在进行匹配的时候,遍历主串,提取出单个字(对于UTF-8编码,可以是任何国家的字),然后去字典树的根结点的unordered_map中进行查找是否存在。如果不存在,则对下一个字进行相同处理;如果存在,则进入该子节点,然后继续查找。字典树结构如下图:

  1~6是编号,后面说明一些东西的时候用到。

  Root节点里不存任何数据,只是提供一个词典的起始位置。那个表格用的是unordered_map。

  对于一个树型结构,如果直接用KMP算法中的next值来确定下一个应该在哪个节点进行查找似乎会有点问题。比如,对于5号节点,next值为1,但是要怎么用这个"1"进入要查找的节点呢?

  由于每个节点只需要知道自己如果匹配失败应该跳到哪个节点,我想了以下两种方案:

  1、把next改成存着节点的地址,类似线索二叉树,这样可以很方便地进行节点转换。

  2、用栈,每次进入子节点,就对原节点的地址进行压栈,next中存的值是要从栈中弹出几个元素。

  由于之前的字典树在遍历的时候采用list实现的栈来确定下一个词是哪个,于是我选择用第二种方案。

  方案有了,就是如何实现的事了。

  我先对字典树的数据结构进行修改:

DictionaryData.h

 #ifndef __DICTIONARYDATA_H__
#define __DICTIONARYDATA_H__ #include <string>
#include <unordered_map>
#include <memory> namespace ccx{ using std::string;
using std::unordered_map;
using std::shared_ptr; struct DictElem
{
string _word;
bool _isend;//是否到词尾
int _next;//KMP next值 此处有修改,存的是弹栈数量
unordered_map<string, shared_ptr<DictElem> > _words;
}; typedef shared_ptr<DictElem> pDictElem; } #endif

  相应地,字典树的成员函数也要进行修改。

Dictionary.h

 #ifndef __DICTIONARY_H__
#define __DICTIONARY_H__ #include "DictionaryData.h"
#include "DictionaryConf.h" #include <memory>
#include <vector>
#include <list> namespace ccx{ using std::shared_ptr;
using std::vector;
using std::list;
using std::pair; class Dictionary
{
typedef pair<int, int> Loc;
typedef unordered_map<string, pDictElem>::iterator WordIt;
public:
Dictionary();
void push(const string & word);//插入
void push(vector<string> & words);//插入
bool search(const string & word);//查找
bool associate(const string & word, vector<string> & data);//联想
string Kmp(const string & word); private:
bool Kmp(vector<string> & word, vector<Loc> & loc);
void getKmpNext(const vector<string> & characters, vector<int> & next);
void AddWord(const string & word);
void splitWord(const string & word, vector<string> & characters);//把词拆成字
int search(vector<string> & data, pDictElem & pcur);
pDictElem _dictionary;
DictionaryConf _conf; //遍历
public:
string getCurChar();
string getCurWord();
bool isEnd();
void resetIt();
void next();
private:
void resetPoint(pDictElem pcur);
void next(pDictElem & pcur, list<WordIt> & stackWord, list<pDictElem> & stackDict);
void nextWord(pDictElem & pcur, list<WordIt> & stackWord, list<pDictElem> & stackDict);
string getCurWord(list<WordIt> & stackWord); pDictElem _pcur;
WordIt _itcur; //用list实现栈,遍历时方便
list<WordIt> _stackWord;
list<pDictElem> _stackDict; //导入导出
public:
void leading_in();
void leading_out();
}; } #endif

  首先是对插入新词进行修改:

 void Dictionary::AddWord(const string & word)
{
vector<string> characters;
splitWord(word, characters);
vector<int> kmpnext;
getKmpNext(characters, kmpnext); vector<int>::iterator it_int;
it_int = kmpnext.begin();
vector<string>::iterator it_char;
it_char = characters.begin();
pDictElem root;
root = _dictionary;
for(; it_char != characters.end(); ++it_char, ++it_int)
{
WordIt it_word;
it_word = root->_words.find(*it_char); if(it_word == root->_words.end())
{
pair<string, pDictElem> temp;
temp.first = *it_char;
pDictElem dictemp(new DictElem);
dictemp->_word = *it_char;
dictemp->_next = *it_int;
dictemp->_isend = false;
temp.second = dictemp;
root->_words.insert(temp);
root = dictemp;
}else{
root = it_word->second;
}
}
if(!root->_isend)
{
root->_isend = true;
}
}

  这里的getKmpNext方法是新加入的,用来求next值:

 void Dictionary::getKmpNext(const vector<string> & characters, vector<int> & kmpnext)
{
int size = characters.size();
for(int i = ; i < size; ++i)
{
kmpnext.push_back();
} int i = -;
int j = ;
kmpnext[] = -;
while(j < size)
{
if(i == - || kmpnext[i] == kmpnext[j])
{
++i;
++j;
kmpnext[j] = i;
}else{
i = kmpnext[i];
}
}
for(i = ; i < size; ++i)
{
kmpnext[i] = i - kmpnext[i];
}
}

  第4~7行可以用vector 的resize方法,直接修改它的容量。

  22行之前就是用来求KMP算法的next数组的,后几行是求弹栈数量的。

  举个例子:

  对于模式串“编程软件”,next数组为:-1 0 0 0,弹栈数量为1 1 2 3。如:

                                                                                   字典树                           栈

  此时若匹配不成功,则要把“件”、“软”、“程”全弹出来。当“编”也不匹配时,弹出,重新在root中的unordered_map中查找。

  进行匹配的代码如下:

 bool Dictionary::Kmp(vector<string> & word, vector<Loc> & loc)
{
pDictElem root = _dictionary;
list<pDictElem> stackDict; int start = ;
int size = word.size();
int i = ;
while(i < size)
{
WordIt it_word;
it_word = root->_words.find(word[i]);
if(it_word == root->_words.end())
{
if(stackDict.size())
{
int num = root->_next;
for(int j = ; j < num - ; ++j)
{
stackDict.pop_back();
}
root = stackDict.back();
stackDict.pop_back();
start += num;
}else{
++i;
start = i;
}
continue;
}else{
stackDict.push_back(root);
root = it_word->second;
if(root->_isend)
{
Loc loctemp;
loctemp.first = start;
loctemp.second = i;
loc.push_back(loctemp);
start = i + ;
}
}
++i;
}
return loc.size();
}

  形参中,word是把主串拆成字后的集合,loc是要传出的参数,参数内容为所有的敏感词的起始位置与结束位置。外层还有一层封装:

 string Dictionary::Kmp(const string & word)
{
vector<string> temp;
splitWord(word, temp);
vector<Loc> loc; if(!Kmp(temp, loc))
{
return word;
}
int size = loc.size();
for(int i = ; i < size; ++i)
{
for(int j = loc[i].first; j <= loc[i].second; ++j)
{
temp[j] = "*";
}
}
string ret;
for(auto & elem : temp)
{
ret += elem;
}
return ret;
}

  在这里,调用之前写的splitWord方法对主串进行分字操作,并且把敏感词替换成“*”,然后把结果传出。

  这些写完差不多就可以用了。以下是测试内容:

  敏感词设定为“好好玩耍”、“编程软件”、“编程学习”、“编程学习网站”、“编程训练”、“编程入门”六个词。

  主串设定为“我不要好好玩耍好好进行编程学习然后建一个编程编程编程学习网站给编程纩编程软件者使用进行编程训练与编程学习”。

  测试结果如下:

我不要好好玩耍好好进行编程学习然后建一个编程编程编程学习网站给编程纩编程软件者使用进行编程训练与编程学习
我不要****好好进行****然后建一个编程编程******给编程纩****者使用进行****与****

  那么,如果机智的小伙伴在敏感词中间加了空格要怎么办呢?

  我又想到两种方案:

  方案一,在分字之后删除空格。

  空格只占一个字节,但是在splitWord中也会被当成字存进vector,此时用erase+remore_if删除即可:

 bool deleterule(string & word)
{
return word == " ";
} string Dictionary::Kmp(const string & word)
{
vector<string> temp;
splitWord(word, temp); temp.erase(std::remove_if(temp.begin(), temp.end(), deleterule)); vector<Loc> loc; if(!Kmp(temp, loc))
{
return word;
}
int size = loc.size();
for(int i = ; i < size; ++i)
{
for(int j = loc[i].first; j <= loc[i].second; ++j)
{
temp[j] = "*";
}
}
string ret;
for(auto & elem : temp)
{
ret += elem;
}
return ret;
}

  测试如下:

我不要好好 玩耍好好进行编程学习然后建一个编程编程编程学    习网站给编程纩编程软件者使用进行编程训练与编程学习
我不要****好好进行****然后建一个编程编程******给编程纩****者使用进行****与****

  方案二,在匹配的时候读到空格就跳过:

 bool Dictionary::Kmp(vector<string> & word, vector<Loc> & loc)
{
pDictElem root = _dictionary;
list<pDictElem> stackDict; int start = ;
int size = word.size();
int i = ;
while(i < size)
{
if(word[i] == " ")
{
++i;
if(!stackDict.size())
{
++start;
}
continue;
}
WordIt it_word;
it_word = root->_words.find(word[i]);
if(it_word == root->_words.end())
{
if(stackDict.size())
{
int num = root->_next;
for(int j = ; j < num - ; ++j)
{
stackDict.pop_back();
}
root = stackDict.back();
stackDict.pop_back();
start += num;
}else{
++i;
start = i;
}
continue;
}else{
stackDict.push_back(root);
root = it_word->second;
if(root->_isend)
{
Loc loctemp;
loctemp.first = start;
loctemp.second = i;
loc.push_back(loctemp);
start = i + ;
}
}
++i;
}
return loc.size();
}

  测试:

我不要好好 玩耍好好进行编程学习然后建一个编程编程编程学    习网站给编程纩编程软件者使用进行编程训练与编程学习
我不要*****好好进行****然后建一个编程编程**********给编程纩****者使用进行****与****

  一开始的时候的BUG:

  1、“编程编程编程学习”无法提取出“编程学习”

  2、敏感词起始位置乱七八糟

  3、弹栈时机乱七八糟

  4、敏感词中同时存在“编程学习”与“编程学习网站”时会发生段错误

  5、4解决了之后,会出现只匹配“编程学习”,而“网站”二字没有替换

  1~4 BUG调整一下就可以了,至于5嘛,莫明其妙就可以了,我也不知道怎么回事。

Dictionary.cc

 #include "Dictionary.h"
#include <json/json.h>
#include <iostream>
#include <fstream>
#include <string>
#include <algorithm> #define PLAN1 namespace ccx{ using std::endl;
using std::cout;
using std::pair;
using std::ofstream;
using std::ifstream; Dictionary::Dictionary()
: _dictionary(new DictElem)
, _conf()
{
_dictionary->_isend = false;
_dictionary->_next = ;
_pcur = _dictionary;
} void Dictionary::splitWord(const string & word, vector<string> & characters)
{
int num = word.size();
int i = ;
while(i < num)
{
int size = ;
if(word[i] & 0x80)
{
char temp = word[i];
temp <<= ;
do{
temp <<= ;
++size;
}while(temp & 0x80);
}
string subWord;
subWord = word.substr(i, size);
characters.push_back(subWord);
i += size;
}
} void Dictionary::getKmpNext(const vector<string> & characters, vector<int> & kmpnext)
{
int size = characters.size();
for(int i = ; i < size; ++i)
{
kmpnext.push_back();
} int i = -;
int j = ;
kmpnext[] = -;
while(j < size)
{
if(i == - || kmpnext[i] == kmpnext[j])
{
++i;
++j;
kmpnext[j] = i;
}else{
i = kmpnext[i];
}
}
for(i = ; i < size; ++i)
{
kmpnext[i] = i - kmpnext[i];
}
} void Dictionary::AddWord(const string & word)
{
vector<string> characters;
splitWord(word, characters);
vector<int> kmpnext;
getKmpNext(characters, kmpnext); vector<int>::iterator it_int;
it_int = kmpnext.begin();
vector<string>::iterator it_char;
it_char = characters.begin();
pDictElem root;
root = _dictionary;
for(; it_char != characters.end(); ++it_char, ++it_int)
{
WordIt it_word;
it_word = root->_words.find(*it_char); if(it_word == root->_words.end())
{
pair<string, pDictElem> temp;
temp.first = *it_char;
pDictElem dictemp(new DictElem);
dictemp->_word = *it_char;
dictemp->_next = *it_int;
dictemp->_isend = false;
temp.second = dictemp;
root->_words.insert(temp);
root = dictemp;
}else{
root = it_word->second;
}
}
if(!root->_isend)
{
root->_isend = true;
}
} void Dictionary::push(const string & word)
{
AddWord(word);
} void Dictionary::push(vector<string> & words)
{
int size = words.size();
for(int i = ; i < size; ++i)
{
push(words[i]);
}
} bool Dictionary::search(const string & word)
{
pDictElem root = _dictionary;
vector<string> temp;
splitWord(word, temp); int ret = search(temp, root);
int size = temp.size();
if(ret != size)
{
return false;
}
return true;
} int Dictionary::search(vector<string> & characters, pDictElem & root)
{
vector<string>::iterator it_char;
it_char = characters.begin();
root = _dictionary;
int i = ;
for(; it_char != characters.end(); ++it_char, ++i)
{
WordIt it_word;
it_word = root->_words.find(*it_char); if(it_word == root->_words.end())
{
break;
}else{
root = it_word->second;
}
}
return i;
} bool Dictionary::associate(const string & word, vector<string> & data)
{
pDictElem root = _dictionary;
vector<string> temp;
splitWord(word, temp); int ret = search(temp, root);
int size = temp.size();
if(ret != size)
{
return false;
} list<WordIt> stackWord;
list<pDictElem> stackDict;
next(root, stackWord, stackDict);
while(root)
{
string temp = getCurWord(stackWord);
data.push_back(temp);
next(root, stackWord, stackDict);
} if(!data.size())
{
return false;
}
return true;
} #ifdef PLAN1
//敏感词中带空格的第一种方案
bool deleterule(string & word)
{
return word == " ";
}
#endif string Dictionary::Kmp(const string & word)
{
vector<string> temp;
splitWord(word, temp); #ifdef PLAN1
temp.erase(std::remove_if(temp.begin(), temp.end(), deleterule));
#endif vector<Loc> loc; if(!Kmp(temp, loc))
{
return word;
}
int size = loc.size();
for(int i = ; i < size; ++i)
{
for(int j = loc[i].first; j <= loc[i].second; ++j)
{
temp[j] = "*";
}
}
string ret;
for(auto & elem : temp)
{
ret += elem;
}
return ret;
} bool Dictionary::Kmp(vector<string> & word, vector<Loc> & loc)
{
pDictElem root = _dictionary;
list<pDictElem> stackDict; int start = ;
int size = word.size();
int i = ;
while(i < size)
{
#ifdef PLAN2
//敏感词中带空格的第二种方案
if(word[i] == " ")
{
++i;
if(!stackDict.size())
{
++start;
}
continue;
}
#endif
WordIt it_word;
it_word = root->_words.find(word[i]);
if(it_word == root->_words.end())
{
if(stackDict.size())
{
int num = root->_next;
for(int j = ; j < num - ; ++j)
{
stackDict.pop_back();
}
root = stackDict.back();
stackDict.pop_back();
start += num;
}else{
++i;
start = i;
}
continue;
}else{
stackDict.push_back(root);
root = it_word->second;
if(root->_isend)
{
Loc loctemp;
loctemp.first = start;
loctemp.second = i;
loc.push_back(loctemp);
start = i + ;
}
}
++i;
}
return loc.size();
} //遍历用 void Dictionary::resetPoint(pDictElem pcur)
{
_pcur = pcur;
if(_stackDict.size())
{
_stackDict.clear();
}
if(_stackWord.size())
{
_stackWord.clear();
}
next();
} void Dictionary::resetIt()
{
resetPoint(_dictionary);
} void Dictionary::next()
{
next(_pcur, _stackWord, _stackDict);
} void Dictionary::next(pDictElem & pcur, list<WordIt> & stackWord, list<pDictElem> & stackDict)
{
while(pcur)
{
nextWord(pcur, stackWord, stackDict);
if(!pcur || pcur->_isend)
{
break;
}
}
} void Dictionary::nextWord(pDictElem & pcur, list<WordIt> & stackWord, list<pDictElem> & stackDict)
{
if(pcur)
{
if(pcur->_words.size())
{
stackDict.push_back(pcur);
stackWord.push_back(pcur->_words.begin());
pcur = stackWord.back()->second;
}else{
++(stackWord.back());
}
while(stackWord.back() == stackDict.back()->_words.end())
{
stackDict.pop_back();
stackWord.pop_back();
if(!stackDict.size())
{
pcur = NULL;
}
++(stackWord.back());
}
if(pcur)
{
pcur = stackWord.back()->second;
}
}
} string Dictionary::getCurChar()
{
return _pcur->_word;
} string Dictionary::getCurWord()
{
return getCurWord(_stackWord);
} string Dictionary::getCurWord(list<WordIt> & stackWord)
{
string temp;
list<WordIt>::iterator it_word;
it_word = stackWord.begin(); for(; it_word != stackWord.end(); ++it_word)
{
temp += (*it_word)->first;
}
return temp;
} bool Dictionary::isEnd()
{
return _pcur == NULL;
} void Dictionary::leading_in()//导入,失败没必要退出程序
{
ifstream ifs;
const char * path = _conf.getDictionaryPath().c_str();
ifs.open(path);
if(!ifs.good())
{
cout << "open Dictionary.json error(leading_in)" << endl;
}else{
Json::Value root;
Json::Reader reader; if(!reader.parse(ifs, root, false))
{
cout << "json read Dictionary.json error" << endl;
}else{
int size = root.size();
for(int i = ; i < size; ++i)
{
string word = root[i]["Word"].asString();
AddWord(word);
}
}
}
} void Dictionary::leading_out()
{
Json::Value root;
Json::FastWriter writer; resetIt(); while(!isEnd())
{
Json::Value elem;
elem["Word"] = getCurWord();
root.append(elem);
next();
} string words;
words = writer.write(root); ofstream ofs;
const char * path = _conf.getDictionaryPath().c_str();
ofs.open(path);
if(!ofs.good())
{
cout << "open Dictionary.json error(leading_out)" << endl;
ofs.open("Dictionary.tmp");
if(!ofs.good())
{
exit(EXIT_FAILURE);
}
} ofs << words;
ofs.close();
} }

  github:https://github.com/ccx19930930/KMP_with_Dictionary

  结论:我的词典真的成了胖接口了!!!

萌新笔记——用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)的更多相关文章

  1. 用KMP算法与Trie字典树实现屏蔽敏感词(UTF-8编码)

    前几天写好了字典,又刚好重温了KMP算法,恰逢遇到朋友吐槽最近被和谐的词越来越多了,于是突发奇想,想要自己实现一下敏感词屏蔽. 基本敏感词的屏蔽说起来很简单,只要把字符串中的敏感词替换成“***”就可 ...

  2. 萌新笔记——Cardinality Estimation算法学习(二)(Linear Counting算法、最大似然估计(MLE))

    在上篇,我了解了基数的基本概念,现在进入Linear Counting算法的学习. 理解颇浅,还请大神指点! http://blog.codinglabs.org/articles/algorithm ...

  3. 萌新笔记——Cardinality Estimation算法学习(一)(了解基数计算的基本概念及回顾求字符串中不重复元素的个数的问题)

    最近在菜鸟教程上自学redis.看到Redis HyperLogLog的时候,对"基数"以及其它一些没接触过(或者是忘了)的东西产生了好奇. 于是就去搜了"HyperLo ...

  4. 萌新笔记——C++里创建 Trie字典树(中文词典)(一)(插入、遍历)

    萌新做词典第一篇,做得不好,还请指正,谢谢大佬! 写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示 ...

  5. 算法笔记之KMP算法

    本文是<算法笔记>KMP算法章节的阅读笔记,文中主要内容来源于<算法笔记>.本文主要介绍了next数组.KMP算法及其应用以及对KMP算法的优化. KMP算法主要用于解决字符串 ...

  6. 萌新笔记——C++里将string类字符串(utf-8编码)分解成单个字(可中英混输)

    最近在建词典,使用Trie字典树,需要把字符串分解成单个字.由于传入的字符串中可能包含中文或者英文,它们的字节数并不相同.一开始天真地认为中文就是两个字节,于是很happy地直接判断当前位置的字符的A ...

  7. 算法导论:Trie字典树

    1. 概述 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树. Trie一词来自retrieve,发音为/tr ...

  8. [原创] Trie树 php 实现敏感词过滤

    目录 背景 简介 存储结构 PHP 其他语言 字符串分割 示例代码 php 优化 缓存字典树 常驻服务 参考文章 背景 项目中需要过滤用户发送的聊天文本, 由于敏感词有将近2W条, 如果用 str_r ...

  9. C++里创建 Trie字典树(中文词典)(一)(插入、遍历)

    萌新做词典第一篇,做得不好,还请指正,谢谢大佬! 写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示 ...

随机推荐

  1. C++ 高性能无锁日志系统

    服务器编程中,日志系统需要满足几个条件 .高效,日志系统不应占用太多资源 .简洁,为了一个简单的日志功能引入大量第三方代码未必值得 .线程安全,服务器中各个线程都能同时写出日志 .轮替,服务器不出故障 ...

  2. 偶遇this之坑

    前言 在写一个懒加载插件时,遇到一个坑,就是this的指向问题,我想这种情况大部分人都会遇到,就写下来,新手也有个参考. 事件 有些页面图片比较多,但用户还不一定会全看,这样的话,全部去加载这些图片, ...

  3. Sql Server系列:规范化及基本设计

    1 理解表 表是具有相同常规属性(attribute)的数据实例的集合.这些数据实例组成数据行和列. 2 数据表设计范式 2.1 第一范式 第一范式(1NF)是关于消除重复数据组和保证原子性的规范化. ...

  4. 版本控制工具比较-CVS,SVN,GIT

    首先介绍几个版本控制软件相互比较的重要依据: a.版本库模型(Repository model):描述了多个源码版本库副本间的关系,有客户端/服务器和分布式两种模式.在客户端/服务器模式下,每一用户通 ...

  5. 学习SpringMVC——国际化+上传+下载

    每个星期一道菜,这个星期也不例外~~~ 一个软件,一个产品,都是一点点开发并完善起来的,功能越来越多,性能越来越强,用户体验越来越好……这每个指标的提高都需要切切实实的做点东西出来,好比,你的这个产品 ...

  6. php内核分析(二)-ZTS和zend_try

    这里阅读的php版本为PHP-7.1.0 RC3,阅读代码的平台为linux ZTS 我们会看到文章中有很多地方是: #ifdef ZTS # define CG(v) ZEND_TSRMG(comp ...

  7. Kafka如何创建topic?

    Kafka创建topic命令很简单,一条命令足矣:bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-facto ...

  8. [Asp.net 5] ApplicationBuilder详解

    ApplicationBuilder(IApplicationBuilder接口),是OWIN的基础,而且里面都是代理.代理的代理,各种lambda表达式,估计要看这部分代码,很多人得头昏脑涨.今天就 ...

  9. PHP多图片上传实例demo

    upload.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http:/ ...

  10. Socket编程——怎么实现一个服务器多个客户端之间的连接

      package coreBookSocket; import java.io.IOException; import java.net.ServerSocket; import java.net. ...