当前位置:   article > 正文

38个敏感词_敏感词过滤算法对比,顺便开源了个工具库

敏感文本判断算法

be5b1ddce1aec2dc28c5bc8ff09b65e7.png
首页图片引用自:[互联网潜规则:如何进行敏感词屏蔽](https://www.iyunying.org/yunying/yyjc/81040.html)

敏感词算法的对比

现在社区内敏感词算法大致实现有两种:DFA(Deterministic Finite Automaton 确定有穷自动机)算法和AC(Aho-Corasick自动机)算法,在掘金社区找到比较有代表性的两篇文章:《js实现敏感词过滤算法》和《开源了一个 JavaScript 版敏感词过滤库》

二者代码我都看了一下,从我角度上来做一个简单对比(其中DFA算法是在原作者基础上的一些改动之后的版本)

当前测试的电脑是MacBook Pro (Retina, 15-inch, Mid 2015),CPU性能是2.2 GHz Intel Core i7

代码实现上

AC算法相对复杂,所以其实现方案也比较复杂,没有拿出纸和笔的话还真的挺难读懂的。但是DFA算法就比较简单易懂了,看着代码就能大概完成整个实现逻辑的构建。所以从代码的实现以及可读性,DFA算法算是比较深得我心吧

DFA算法占优

代码功能上

AC算法的作者提供了诸多功能,比如支持快查询,支持临时添加单词等等,而第一种算法在我的改进后目前只支持快查询,所以这个功能层面AC算法略好,不过并不意味这DFA算法这些功能就实现不了,只是看大家需不需要了,需要的话我将会在开源库中不断完善。

AC算法占优

算法时间和空间上

接下去说说算法的耗时和内存占用,下面是我使用的benchmark脚本,测试数据可以在这里看到:传送门,脚本如下:

  1. const { makeSensitiveMap, checkSensitiveWord } = require('../dist/help')
  2. const FastScanner = require('fastscan')
  3. const fs = require('fs')
  4. const path = require('path')
  5. function loadOneHundredThousandSentences() {
  6. const data = fs.readFileSync(path.resolve(__dirname, './sensitiveWords.txt'), 'utf8')
  7. const wordsArray = data.trim().split('|')
  8. console.log(`Now we have sensitive words length is: [${wordsArray.length}]`)
  9. return wordsArray
  10. }
  11. function loadOneHundredThousandWords() {
  12. const data = fs.readFileSync(path.resolve(__dirname, './beCheckedWords.txt'), 'utf8')
  13. const words = data.trim()
  14. console.log(`Now we have checking words length is: [${words.length}]`)
  15. return words
  16. }
  17. function benchmarkForDFAalgorithm() {
  18. const wordsArray = loadOneHundredThousandSentences()
  19. const before = process.memoryUsage()
  20. console.time('DFA algorithm load sensitive map tree')
  21. const wordMaps = makeSensitiveMap(wordsArray)
  22. console.timeEnd('DFA algorithm load sensitive map tree')
  23. const after = process.memoryUsage()
  24. console.log("DFA algorithm build tree of %d words costs rss=%dM heapTotal=%dM heapUsed=%dM", wordsArray.length, (after.rss-before.rss) >> 20, (after.heapTotal - before.heapTotal) >> 20, (after.heapUsed - before.heapUsed) >> 20)
  25. const toBeCheckedWords = loadOneHundredThousandWords()
  26. console.time('DFA algorithm check one hundred thousand words')
  27. checkSensitiveWord(toBeCheckedWords, false, wordMaps)
  28. console.timeEnd('DFA algorithm check one hundred thousand words')
  29. }
  30. function benchmarkForACalgorithm() {
  31. const wordsArray = loadOneHundredThousandSentences()
  32. const before = process.memoryUsage()
  33. console.time('AC algorithm load sensitive map tree')
  34. const scanner = new FastScanner(wordsArray)
  35. console.timeEnd('AC algorithm load sensitive map tree')
  36. const after = process.memoryUsage()
  37. console.log("AC algorithm build tree of %d words costs rss=%dM heapTotal=%dM heapUsed=%dM", wordsArray.length, (after.rss-before.rss) >> 20, (after.heapTotal - before.heapTotal) >> 20, (after.heapUsed - before.heapUsed) >> 20)
  38. const toBeCheckedWords = loadOneHundredThousandWords()
  39. console.time('AC algorithm check one hundred thousand words')
  40. scanner.search(toBeCheckedWords)
  41. console.timeEnd('AC algorithm check one hundred thousand words')
  42. }
  43. // 内存的测试需要单独跑,否则二者之间会有相互冲突的情况
  44. // benchmarkForDFAalgorithm()
  45. // benchmarkForACalgorithm()

我们直接以100000个词汇作为敏感词汇去构建,并输入100000个单词去检索,得到的测试结果如下:(内存的数据是单独函数跑的,要不因为GC问题可能测试不准确)

  1. Now we have sensitive words length is: [107888]
  2. DFA algorithm load sensitive map tree: 244.374ms
  3. DFA algorithm build tree of 107888 words costs rss=121M heapTotal=117M heapUsed=98M
  4. Now we have checking words length is: [100000]
  5. DFA algorithm check one hundred thousand words: 98.768ms
  6. Now we have sensitive words length is: [107888]
  7. AC algorithm load sensitive map tree: 1529.913ms
  8. AC algorithm build tree of 107888 words costs rss=174M heapTotal=161M heapUsed=136M
  9. Now we have checking words length is: [100000]
  10. AC algorithm check one hundred thousand words: 98.532ms

从上面的测试结果,AC的测试结果和原作者提供的大致一直,并且可以看出在词汇树的构建和内存占用上,DFA算法远远比AC算法好,执行搜索的时候,二者才不相上下,因此这回合DFA算法占优

DFA算法占优

结论

最后综上所述,结论如下:

  • 如果你要简单判断少量词汇,二者都可以使用
  • 如果你的敏感词汇很大,那么建议你使用DFA算法

《js实现敏感词过滤算法》的代码改动

原文作者已经介绍了很多DFA算法的思路,这里就不再赘述,因为原作者将校验的函数分成了两个,容易遗漏掉第二个函数filterSensitiveWord。所以我就想整合成一个函数,一开始想着使用递归的尾调用来实现的,伪代码实现如下:

  1. checkSensitiveWord(sensitiveMap: wordMap, txt: string, index: number) {
  2. let currentMap = sensitiveMap;
  3. // const matchWords = new Map()
  4. let wordNum = 0;//记录过滤
  5. let sensitiveWord = ''; //记录过滤出来的敏感词
  6. // 递归到结尾了,结束递归
  7. if (index === txt.length) {
  8. return
  9. }
  10. for (let i = index; i < txt.length; i++) {
  11. const word = txt.charAt(i);
  12. currentMap = currentMap.get(word);
  13. if (currentMap) {
  14. wordNum++;
  15. sensitiveWord += word;
  16. if (currentMap.get('laster') === true) {
  17. // 表示已到词的结尾,将该敏感词存储起来
  18. 存储的代码....
  19. // 再继续递归
  20. return this.checkSensitiveWord(sensitiveMap, txt, index + 1)
  21. }
  22. } else {
  23. return this.checkSensitiveWord(sensitiveMap, txt, index + 1)
  24. }
  25. }
  26. }

结果因为nodejs从版本8开始不再支持递归尾调用的优化,所以这段代码如果检查比较少的文本的话是没问题,但是文本量一旦变大,就很容易造成栈溢出了。

所以还是老老实实地使用循环遍历的方式,并增加支持快速搜索的功能,最后的代码如下

  1. /**
  2. * 检查搜寻的文本是否含有敏感词汇
  3. * @param txt 需要查找敏感词的文本
  4. * @param sensitiveWordsMap 敏感词汇的Map结构,允许自定义,如果自定义需要使用上面的函数makeSensitiveMap去生成
  5. * @param isQuickSearch 是否需要快速查询,如果是的话查找到值是返回true,反之是false
  6. */
  7. export function checkSensitiveWord(
  8. txt: string,
  9. isQuickSearch = false,
  10. sensitiveWordsMap?: wordMap) {
  11. const defaultMap = () => {
  12. const data = fs.readFileSync(path.resolve(__dirname, '../utils/sensitiveWords.txt'), 'utf8')
  13. const wordsArray = data.trim().split('|')
  14. return makeSensitiveMap(wordsArray)
  15. }
  16. const _sensitiveWordsMap = sensitiveWordsMap ? sensitiveWordsMap : defaultMap()
  17. const matchWords = new Map()
  18. for (let i = 0; i < txt.length; i++) {
  19. let currentMap = _sensitiveWordsMap;
  20. let sensitiveWord = ''; //记录过滤出来的敏感词
  21. for (let j = i; j < txt.length; j++) {
  22. const word = txt.charAt(j);
  23. currentMap = currentMap.get(word);
  24. if (currentMap) {
  25. sensitiveWord += word;
  26. if (currentMap.get('isEnd') === true) {
  27. // 如果是快速查找,不关心敏感词的搜索结果,找到一个即返回,适用于正常的输入检测
  28. if (isQuickSearch) {
  29. return true
  30. }
  31. // 表示已到词的结尾
  32. const isExist = matchWords.get(sensitiveWord)
  33. if (isExist) {
  34. isExist.push({ location: i })
  35. matchWords.set(sensitiveWord, isExist)
  36. } else {
  37. matchWords.set(sensitiveWord, [{ location: i }])
  38. }
  39. break
  40. }
  41. } else {
  42. break;
  43. }
  44. }
  45. }
  46. // 到这一步如果是快速查询还没有返回,说明没有找到敏感词
  47. if (isQuickSearch) {
  48. return false
  49. }
  50. return matchWords
  51. }

完整的实例请参考: awesome-js

顺便开源了一个工具库

眼尖的童鞋发现了,这些函数不仅仅是直接粘贴复制使用,而是隶属于这个工具库awesome-js,是的,这就是要给大家安利的开源工具库,该工具库包含了很多我在平时业务开发中用到的一些公共函数,目前分为四大块:数学工具、正则表达式工具、辅助工具以及http处理工具。

怎么使用这个工具库呢?

下载

  1. npm install awesome-js --save
  2. yarn add awesome-js

使用

使用import { AwesomeHelp } from 'awesome-js'即可正常使用

提供了哪些功能呢?

得益于ts的类型定义,我们去翻一翻types文件就行了,为了让大家省事,贴在这里也无妨~

  1. export interface Deferred {
  2. resolve: (value?: any) => any
  3. reject: (reason?: any) => void
  4. promise: Promise<any>
  5. }
  6. type wordMap = Map<string, recursiveMap | boolean>
  7. interface recursiveMap extends wordMap {}
  8. export namespace AwesomeRegx {
  9. /**
  10. * @description 匹配手机号码
  11. */
  12. export const phoneNumber: RegExp;
  13. /**
  14. * @description 匹配Emoji字符
  15. */
  16. export const isEmoji: RegExp;
  17. /**
  18. * @description 隐私手机号,会将手机号的中间四位数替换成*
  19. */
  20. export const privacyMobile: (mobile: string) => string;
  21. /**
  22. * @description 姓名脱敏,将除第一个字之外的非空字符替换为*
  23. */
  24. export const privacyName: (name: string) => string;
  25. /**
  26. * @description 匹配中文字符和全角字符
  27. */
  28. export const chineseAndfullWidthChar: RegExp;
  29. /**
  30. * @description 匹配image标签里面的src属性,常用于将src的http去掉
  31. */
  32. export const imgSrc: RegExp;
  33. /**
  34. * @description 简单的匹配身份证号
  35. */
  36. export const simpleIdentityNo: RegExp;
  37. /**
  38. * @description 匹配中文名字
  39. */
  40. export const chineseName: RegExp;
  41. /**
  42. * @description 匹配正整数
  43. */
  44. export const positiveInteger: RegExp;
  45. /**
  46. * @description 匹配整数
  47. */
  48. export const integer: RegExp;
  49. /**
  50. * @description 匹配负整数
  51. */
  52. export const negativeInteger: RegExp;
  53. /**
  54. * @description 匹配非负整数
  55. */
  56. export const nonnegativeInteger: RegExp;
  57. /**
  58. * @description 匹配非正整数
  59. */
  60. export const nonPostiveInterger: RegExp;
  61. /**
  62. * @description 匹配正浮点数
  63. */
  64. export const postiveFloat: RegExp;
  65. /**
  66. * @description 匹配负浮点数
  67. */
  68. export const negativeFloat: RegExp;
  69. /**
  70. * @description 匹配浮点数
  71. */
  72. export const float: RegExp;
  73. /**
  74. * @description 匹配非负浮点数
  75. */
  76. export const nonNegativeFloat: RegExp;
  77. /**
  78. * @description 匹配非正浮点数
  79. */
  80. export const nonPositiveFloat: RegExp;
  81. /**
  82. * @description 匹配英文26个字母
  83. */
  84. export const alphabat: RegExp;
  85. /**
  86. * @description 匹配大写的英文字母
  87. */
  88. export const upperAlpha: RegExp;
  89. /**
  90. * @description 匹配小写的英文字母
  91. */
  92. export const lowerAlpha: RegExp;
  93. /**
  94. * @description 匹配英文字母和数字加下划线
  95. */
  96. export const alphaNumWithUnderline: RegExp;
  97. /**
  98. * @description 匹配双字节字符
  99. */
  100. export const DBC: RegExp;
  101. /**
  102. * @description 匹配空行
  103. */
  104. export const emptyLine: RegExp;
  105. /**
  106. * @description 匹配首部或者尾部有空白字符的字符串
  107. */
  108. export const emptyCharInStartAndEnd: RegExp;
  109. /**
  110. * @description 匹配中文字符
  111. */
  112. export const chinese: RegExp;
  113. /**
  114. * @description 匹配邮箱
  115. */
  116. export const email: RegExp;
  117. /**
  118. * @description 匹配url
  119. */
  120. export const url: RegExp;
  121. /**
  122. * @description 匹配ip地址
  123. */
  124. export const ip: RegExp;
  125. /**
  126. * @description 匹配电话座机
  127. */
  128. export const telPhone: RegExp;
  129. /**
  130. * @description 匹配邮政编码
  131. */
  132. export const postalCode: RegExp;
  133. }
  134. export namespace AwesomeHelp {
  135. /**
  136. * @description 根据对象的某些字段的值对数组对象进行分类
  137. * @param list 需要分类的数组对象(必须是一个数组)
  138. * @param fields 需要分类的字段(必须传递一个函数, 支持多个字段)
  139. */
  140. export function groupBySomeFields<T>(list: T[], fields: (item: T) => any[]): T[][]
  141. /**
  142. * @description 对Date的扩展,将 Date 转化为指定格式的String
  143. * @param date 需要转换格式的日期
  144. * @param format 日期转换的最后格式,比如YYYY-MM-DD
  145. */
  146. export function convertDate(date: Date, format: string): string
  147. /**
  148. * @description 浮点数相加
  149. */
  150. export function addFloat(arg1: number, arg2: number): number
  151. /**
  152. * @description 浮点数相减
  153. */
  154. export function minusFloat(arg1: number, arg2: number): number
  155. /**
  156. * @description 浮点数相除
  157. */
  158. export function divFloat(arg1: number, arg2: number): number
  159. /**
  160. * @description 浮点数相乘
  161. */
  162. export function timesFloat(arg1: number, arg2: number): number
  163. export function makeDeferred(): Deferred
  164. /**
  165. * @description 判断是否是生成器
  166. */
  167. export function isGenerator(obj: any): boolean
  168. /**
  169. * @description 判断是否是生成器函数
  170. */
  171. export function isGeneratorFunction(obj: any): boolean
  172. /**
  173. * @description 判断是否是Promise
  174. */
  175. export function isPromise(obj: any): boolean
  176. /**
  177. * @description 千分法计数
  178. */
  179. export function toThousands(num: number): string
  180. /**
  181. * 隐藏所有的数字位除了指定的某一位,比如需要转换100000的所有0为?,那么就要这样调用hiddenNumberExpectSpecified(100000, 0, '?') => 1?????
  182. * @param num 需要操作的数字
  183. * @param expected 不想被隐藏的位数,从左边最高index开始算起,默认是最高位也就是0
  184. * @param hiddenStr 希望隐藏的数字转换成哪个字符,默认是?
  185. */
  186. export function hiddenNumberExpectSpecified(num: number, expected: number, hiddenStr: string): string
  187. /**
  188. * 将所有的敏感词汇组成一个嵌套的Map结构,使用的是DFA数据结构算法
  189. * @param sensitiveWordList
  190. */
  191. export function makeSensitiveMap(sensitiveWordList: string[]): wordMap
  192. /**
  193. * 检查搜寻的文本是否含有敏感词汇
  194. * @param txt 需要查找敏感词的文本
  195. * @param sensitiveWordsMap 敏感词汇的Map结构,允许自定义,如果自定义需要使用上面的函数makeSensitiveMap去生成,如果没有传,默认使用自带的敏感词库
  196. * @param isQuickSearch 是否需要快速查询,默认是false,如果是的话查找到值是返回true,反之是false
  197. */
  198. export function checkSensitiveWord(
  199. txt: string,
  200. isQuickSearch?: null,
  201. sensitiveWordsMap?: wordMap): Map<string, { location: number}[] >
  202. export function checkSensitiveWord(
  203. txt: string,
  204. isQuickSearch: boolean,
  205. sensitiveWordsMap?: wordMap): boolean
  206. }
  207. export namespace AwesomeMath {
  208. export class Region {
  209. constructor(points: number[][])
  210. /**
  211. * @description 计算多边形的中间点的坐标(经纬度)
  212. */
  213. public centroid: () => { x: number, y: number}
  214. /**
  215. * @description 简单的匹配身份证号
  216. */
  217. private area: () => number
  218. }
  219. /**
  220. * @description 计算两点之间的直线距离
  221. * @param {number} lng1 起点纬度
  222. * @param {number} lat1 起点纬度
  223. * @param {number} lng2 终点纬度
  224. * @param {number} lat2 终点纬度
  225. * @returns {number} 两点之间的直线距离,单位:米
  226. */
  227. export function getDistance(lng1: number, lat1: number, lng2: number, lat2: number): number
  228. /**
  229. * 转换经度或者纬度为地图可识别的格式
  230. * @param origin
  231. */
  232. export function decodeLatLng(origin: number): number
  233. /**
  234. *
  235. * @param origin 转换经度或者纬度为整数格式
  236. */
  237. export function encodeLatLng(origin : number): number
  238. }
  239. export namespace AwesomeHttp {
  240. /**
  241. * @description 更新url中query请求的某个参数,可以配合replaceState去更新浏览器的历史记录
  242. * @param baseUrl 需要更新的url
  243. * @param key 需要更新的key
  244. * @param value 更新的新值
  245. */
  246. export function updateQueryStringParam(baseUrl: string, key: string, value: any): string
  247. /**
  248. * @description 解析queryObject后组合一起追加到path后面
  249. */
  250. export function queryObject2String(path: string, queryObject: object): string
  251. }

最后

欢迎大家PR,添加更多函数,方便你我他~我也会不断更新该工具库,欢迎watch~

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/153865
推荐阅读
相关标签
  

闽ICP备14008679号