2019独角兽企业重金招聘Python工程师标准>>>

可能不止在天朝,绝大多数网站都会需要违禁词过滤模块,用于对不雅言论进行屏蔽;所以这个应该算是网站的基础功能。大概在去年的时候我开发过这个功能,当时用6600+(词数)的违禁词库,过滤2000+(字数)的文章,耗时大概20ms左右,当时自我感觉还挺良好。过了这一段时间,回想一下,其实有不少地方可以做优化、可以总结;于是从头到尾捋了一遍。

原始需求:

违禁词最原始的需求,也是最直观的,即是查找一篇未知的文档中,是否包含了一个指定词语集合(违禁词库)中的词语。其实要实现这个功能的思路有许多,也可以很简单;但为了达到一定的效率,需要一些算法和数据结构的设计。

逻辑整理:

将原始的需求转换成可实现的逻辑,这里根据思维方向(出发点),可以有两个不同的选择:1.  遍历文档的每个词,查看违禁词库中是否包含这个词; 2.  遍历违禁词库中的每个词, 查看文档中是否包含这个词。
我这里采用的是第一种思维方向,原因有二:
a.  我要过滤的文档的字数,大部分集中的2000~5000之间,少于违禁词的个数;遍历少的从性能上讲,有先天的优势。
b.  待过滤的文档是未知的,且变化的,而违禁词已知且固定;于是我们可对违禁词的数据结构做一定的设计,加快一个词在其中的查找,所以需要遍历的是文档(较主要的一个原因)。
思路有了,简单概括为:从文档中取词—>该词是否属于违禁词。
下一步我们需要整理出实现逻辑的步骤,在针对每一步骤做设计和优化。步骤如下:
1.   取出下一个字节(若最后一个字节:跳至结束),
2.   判断是否为汉字,是:记录该字节的位置w,并继续下一步;否:返回第1步。
3.   判断此汉字是否是某个违禁词的开头,是:继续下一步;否:返回第1步。
4.   继续读取下一个字符(若最后一个字节:跳至结束),判断是否为汉字,是:继续下一步;否:返回第一步。
5.   将上一步得到汉字和前面的汉字组成字符串,判断是否是某个违禁词的前缀。是:继续下一步;否:跳回第1步(取w+1字节)。
6.   查看这个前缀是否就是违禁词。是继续下一步;否:返回第4步。
7.   记录下这个违禁词的信息(词,长度,位置等)。
8.   返回第1步(从w+该违禁词长度+1处取词)
9.   结束。
老鸟们,可能都熟悉,这是分词中的前缀匹配法,其实违禁词过滤的思路和搜索中分词的思路相似,所以我也有参考Lucene在分词时的源代码来实现。另:我目前处理的违禁词中只有汉字,若您处理时有其他符号,可增加些判断。
下面是这部分逻辑的源代码:
/*** 过滤违禁词* @param sentence:待过滤字符串* @return*/private BadInfo findBadWord(String sentence) {CharType[] charTypeArray = getCharTypes(sentence);//获取出每个字符的类型BadInfo result = new BadInfo(sentence);BadWordToken token;int i = 0, j;int length = sentence.length();int foundIndex;char[] charArray;StringBuffer wordBuf = new StringBuffer();while (i < length) {// 只处理汉字和字母if (CharType.HANZI == charTypeArray[i]|| CharType.LETTER == charTypeArray[i]) {j = i + 1;wordBuf.delete(0, wordBuf.length());//新的一轮匹配,清除掉原来的wordBuf.append(sentence.charAt(i));charArray = new char[] { sentence.charAt(i) };foundIndex = wordDict.getPrefixMatch(charArray);//前缀匹配违禁词//foundIndex表示记录了前缀匹配的位置while (j <= length && foundIndex != -1) {// 表示找到了if (wordDict.isEqual(charArray, foundIndex)&& charArray.length > 1) {token = new BadWordToken(new String(charArray), i, j);result.addToken(token);//记录下来i = j - 1; // j在匹配成功时已经自加了,这里是验证确实是违禁词,所以需要将j前一个位置给i}// 去掉空格while (j < length&& charTypeArray[j] == CharType.SPACE_LIKE)j++;if (j < length&& (charTypeArray[j] == CharType.HANZI || CharType.LETTER == charTypeArray[j])) {//将下个字符和前面的组合起来, 继续前缀匹配wordBuf.append(sentence.charAt(j));charArray = new char[wordBuf.length()];wordBuf.getChars(0, charArray.length, charArray, 0);foundIndex = wordDict.getPrefixMatch(charArray,foundIndex);//前缀匹配违禁词j++;} else {break;}}}i++;}return result;}

上面的逻辑和代码实现只是过滤违禁词外层实现,具体如何在违禁词库中,查询指定字符串,是最为关键的,即:词典WordDict的数据结构,和它的算法getPrefixMatch() 方法,也是涉及到性能优化的地方。

数据结构:词典

先来说说词典WordDict的数据结构吧,它作为一个容器,里面记录所有违禁词。
为了快速查找,使用了散列的思想和类似索引倒排的结构,通过一个三维的char 数组来实现。
private char[][][] wordItem_real;

第一维 wordItem_real[i] 其含义是:具有相同开头汉字X,的所有违禁词(一组)。其中下标 i 为 X 的 GB2312 码,这样只要对文档中的某一个汉字一转码,就能马上找到以此汉字开头的所有违禁词,算是一种散列吧;

另:每组违禁词 是有序的(升序),先按长度排序,再按 char 排序。查找时用到了二分查找所以需要保持有序。
第二维 wordItem_real[i][j] 其含义是:具体的一个违禁词的字符串数组,例如违禁词“红薯” = {'红','薯'}。
第三维 wordItem_real[i][j][k] 就是 词中某个汉字了。
词典的初始化代码,这里就不贴了,主要都是些读文件,扫描单词,和排序等一些基础代码。

算法:二分查找与前缀匹配

接下来是 getPrefixMatch() 算法,它肯定依赖于 WordDict 词典的数据结构,就不多说了。它的目的是:从词典中查找以charArray对应的单词为前缀(prefix)的单词的位置, 并返回第一个满足条件的位置。为了减小搜索代价, 可以根据已有知识设置起始搜索位置, 如果不知道起始位置,默认是0
它的实现思路是:首先通过对参数中第一个字符 转GB2312 码,并根据此码获得 具有相同开头汉字的那组违禁词。然后在通过二分查找的方式,查看这组违禁词中是否包含 参数字符串前缀的 词;二分查找中具体的比较方法在稍后贴出。
/*** * * @see{getPrefixMatch(char[] charArray)}* @param charArray*            前缀单词* @param knownStart*            已知的起始位置* @return 满足前缀条件的第一个单词的位置*/public int getPrefixMatch(char[] charArray, int knownStart) {int index = Utility.getGB2312Id(charArray[0]);if (index == -1)return -1;char[][] items = wordItem_real[index];if(items == null){return -1; //没有以此字开头的违禁词}int start = knownStart, end = items.length - 1;int mid = (start + end) / 2, cmpResult;// 二分查找法while (start <= end) {cmpResult = Utility.compareArrayByPrefix(charArray, 1, items[mid],0);if (cmpResult == 0) {// 获取第一个匹配到的(短的优先)while (mid >= 0&& Utility.compareArrayByPrefix(charArray, 1,items[mid], 0) == 0)mid--;mid++;return mid;// 找到第一个以charArray为前缀的单词} else if (cmpResult < 0)end = mid - 1;elsestart = mid + 1;mid = (start + end) / 2;}return -1;}

下面是上述代码中,二分查找的比较方式:根据前缀来判断两个字符数组的大小,当前者为后者的前缀时,表示相等,当不为前缀时,按照普通字符串方式比较。呵呵,这里算是盗用lucene 源代码了。
public static int compareArrayByPrefix(char[] shortArray, int shortIndex,char[] longArray, int longIndex) {// 空数组是所有数组的前缀,不考虑indexif (shortArray == null)return 0;else if (longArray == null)return (shortIndex < shortArray.length) ? 1 : 0;int si = shortIndex, li = longIndex;while (si < shortArray.length && li < longArray.length&& shortArray[si] == longArray[li]) {si++;li++;}if (si == shortArray.length) {// shortArray 是 longArray的prefixreturn 0;} else {// 此时不可能si>shortArray.length因此只有si <// shortArray.length,表示si没有到达shortArray末尾// shortArray没有结束,但是longArray已经结束,因此shortArray > longArrayif (li == longArray.length)return 1;else// 此时不可能li>longArray.length因此只有li < longArray.length// 表示shortArray和longArray都没有结束,因此按下一个数的大小判断return (shortArray[si] > longArray[li]) ? 1 : -1;}}

主要的思路和实现代码都已经讲明了,若大家有更好的过滤违禁词的算法,希望分享,周末愉快。

参考资料:Lucene 源代码
原创博客,转载请注明: http://my.oschina.net/BreathL/blog/56265

转载于:https://my.oschina.net/BreathL/blog/56265

违禁词过滤完整设计与优化(前缀匹配、二分查找)相关推荐

  1. 网站前端进行违禁词过滤js代码

    var strChar=['最佳','最具','最爱','最赚','最优','最优秀','最好','最大','最大程度','最高','最高级','最高端','最奢侈','最低','最低级','最底', ...

  2. WordPress上好用的违禁词过滤插件分享

    违禁词过滤插件可以删除或者替换文章.页面和评论内容中出现的违禁词,同时也可以将出现违禁词的内容设为待审核状态或者移至回收站,等待进一步的人工处理.本插件不仅仅可以过滤违禁词,只要内容中出现不想要的文字 ...

  3. LeetCode 528. 按权重随机选择(前缀和+二分查找)

    文章目录 1. 题目 2. 解题 1. 题目 给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 ...

  4. LeetCode 497. 非重叠矩形中的随机点(前缀和+二分查找)

    文章目录 1. 题目 2. 解题 1. 题目 给定一个非重叠轴对齐矩形的列表 rects,写一个函数 pick 随机均匀地选取矩形覆盖的空间中的整数点. 提示: 整数点是具有整数坐标的点. 矩形周边上 ...

  5. 209 长度最小的子数组(前缀和+二分查找、滑动窗口)

    1. 问题描述: 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度.如果不存在符合条件的子数组,返回 0. 示例: 输入:s = ...

  6. 通过Trie实现违禁词过滤

    敏感词过滤 生活在天朝的网站,必须要有保持和谐的工具.根据网站的规模不同选择不同的技术方案: 1.前期上一个敏感词过滤系统,发的文章只要命中敏感词就不让发. 2.后期可以通过机器学习来自动识别一篇简历 ...

  7. [Leedcode][JAVA][第209题][长度最小的子数组][滑动窗口][前缀和][二分查找][双指针]

    [问题描述][中等] 给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度.如果不存在符合条件的连续子数组,返回 0.示例: 输入: ...

  8. LeetCode 1712. 将数组分成三个子数组的方案数(前缀和 + 二分查找)

    文章目录 1. 题目 2. 解题 221 / 3117,前7.1% 574 / 9692,前 5.9% 周赛前2题如下: LeetCode 5641. 卡车上的最大单元数(排序,模拟) LeetCod ...

  9. 敏感词过滤工具类(DFA算法匹配字典)

    直接调用方法:wordFilter package com.util;import java.io.BufferedReader; import java.io.FileNotFoundExcepti ...

最新文章

  1. Csharp: 拼音转汉字字符搜索字符串
  2. python百度云资源-python学习资源--百度云
  3. 记阿里的一次壮烈牺牲
  4. JAVA截取字符串方法
  5. Java代理设计模式(Proxy)的四种具体实现:静态代理和动态代理
  6. 区块链跟银行有什么关系?
  7. html5 视口,html5 – 在媒体查询中更改视口
  8. 基于JAVA+SpringMVC+Mybatis+MYSQL的汽车维修管理平台
  9. Python+matplotlib绘图使用Latex引擎渲染坐标轴刻度文本上标
  10. Qt 方式问题_vortex_新浪博客
  11. 阿里云何勉:如何定义团队的研发效能?
  12. 【开发】MFC到Delphi的皮肤移植
  13. 微信语音识别开放平台
  14. Unity3d 周分享(16期 2019.5.1 )
  15. 汽车距离报警系统c语言编程,基于单片机的汽车防盗报警系统的设计本科生毕业论文.doc...
  16. 口碑、银盒子相关功能是否支持问题
  17. 计算机兴趣小组活动实施方式,信息技术兴趣小组活动总结范文(通用5篇)
  18. 国产手机均价下跌,苹果逆势增长,iPhone仍是消费者最爱
  19. lunix上silk转mp3 和 mp3转silk
  20. 开源魔兽世界私服搭建

热门文章

  1. linux如何查看进程及端口,Linux如何查看端口及进程
  2. 协同过滤算法分类-UserCF
  3. python批量打印复印_惠普集群打印 小规模灵活批量打印方案
  4. java中判断list是否为空
  5. uefi装完系统后无法引导_uefi u盘无法引导怎么办
  6. vue 过滤器的使用
  7. android 获取手机蓝牙是否与其他设备蓝牙配对连接成功,android开发获取手机已连接的蓝牙设备(包括已链接的设备和已经配对绑定的设备)...
  8. matlab解决迷宫问题,用matlab处理蚂蚁迷宫问题
  9. loadrunner controller无法创建vuser
  10. 获取设备型号、设备类型等信息