敏感词过滤是随着互联网社区发展一起发展起来的一种阻止网络犯罪和网络暴力的技术手段,通过对可能存在犯罪或网络暴力可能的关键词进行有针对性的筛查和屏蔽,很多时候我们能够防患于未然,把后果严重的犯罪行为扼杀于萌芽之中。
随着各种社交平台等的日益火爆,敏感词过滤逐渐成了非常重要的也是值得重视的功能。那么在 Serverless 架构下,通过Python 语言,敏感词过滤又有那些新的实现呢?我们能否是用最简单的方法,实现一个敏感词过滤的API呢?
了解敏感过滤的几种方法
Replace方法
如果说敏感词过滤,其实不如说是文本的替换,以Python为例,说到词汇替换,不得不想到replace,我们可以准备一个敏感词库,然后通过replace进行敏感词替换:
1def worldFilter(keywords, text): 2 for eve in keywords: 3 text = text.replace(eve, "***") 4 return text 5keywords = ("关键词1", "关键词2", "关键词3") 6content = "这是一个关键词替换的例子,这里涉及到了关键词1还有关键词2,最后还会有关键词3。" 7print(worldFilter(keywords, content))
但是动动脑大家就会发现,这种做法在文本和敏感词库非常庞大的前提下,会有很严重的性能问题。例如我将代码进行修改,进行基本的性能测试:
1import time 2 3def worldFilter(keywords, text): 4 for eve in keywords: 5 text = text.replace(eve, "***") 6 return text 7keywords =[ "关键词" + str(i) for i in range(0,10000)] 8content = "这是一个关键词替换的例子,这里涉及到了关键词1还有关键词2,最后还会有关键词3。" * 1000 9startTime = time.time() 10worldFilter(keywords, content) 11print(time.time()-startTime)
此时的输出结果是:0.12426114082336426,可以看到性能非常差。
正则表达方法
与其用replace,还不如通过正则表达re.sub来的更加快速。
1import time 2import re 3def worldFilter(keywords, text): 4 return re.sub("|".join(keywords), "***", text) 5keywords =[ "关键词" + str(i) for i in range(0,10000)] 6content = "这是一个关键词替换的例子,这里涉及到了关键词1还有关键词2,最后还会有关键词3。" * 1000 7startTime = time.time() 8worldFilter(keywords, content) 9print(time.time()-startTime)
我们同样增加性能测试,按照上面的方法进行改造测试,输出结果是0.24773502349853516。通过这样的例子,我们可以发现,其性能磣韩剧并不大,但是实际上随着文本量增加,正则表达这种做法在性能层面会变高很多。
DFA过滤敏感词
这种方法相对来说效率会更高一些。例如,我们认为坏人,坏孩子,坏蛋是敏感词,则他们的树关系可以表达:

用DFA字典来表示:
1{ 2 '坏': { 3 '蛋': { 4 '\x00': 0 5 }, 6 '人': { 7 '\x00': 0 8 }, 9 '孩': { 10 '子': { 11 '\x00': 0 12 } 13 } 14 } 15}
使用这种树表示问题最大的好处就是可以降低检索次数,提高检索效率,基本代码实现:
1import time 2 3class DFAFilter(object): 4 def __init__(self): 5 self.keyword_chains = {} # 关键词链表 6 self.delimit = '\x00' # 限定 7 8 def parse(self, path): 9 with open(path, encoding='utf-8') as f: 10 for keyword in f: 11 chars = str(keyword).strip().lower() # 关键词英文变为小写 12 if not chars: # 如果关键词为空直接返回 13 return 14 level = self.keyword_chains 15 for i in range(len(chars)): 16 if chars[i] in level: 17 level = level[chars[i]] 18 else: 19 if not isinstance(level, dict): 20 break 21 for j in range(i, len(chars)): 22 level[chars[j]] = {} 23 last_level, last_char = level, chars[j] 24 level = level[chars[j]] 25 last_level[last_char] = {self.delimit: 0} 26 break 27 if i == len(chars) - 1: 28 level[self.delimit] = 0 29 30 def filter(self, message, repl="*"): 31 message = message.lower() 32 ret = [] 33 start = 0 34 while start < len(message): 35 level = self.keyword_chains 36 step_ins = 0 37 for char in message[start:]: 38 if char in level: 39 step_ins += 1 40 if self.delimit not in level[char]: 41 level = level[char] 42 else: 43 ret.append(repl * step_ins) 44 start += step_ins - 1 45 break 46 else: 47 ret.append(message[start]) 48 break 49 else: 50 ret.append(message[start]) 51 start += 1 52 53 return ''.join(ret) 54 55 56 57gfw = DFAFilter() 58gfw.parse( "./sensitive_words") 59content = "这是一个关键词替换的例子,这里涉及到了关键词1还有关键词2,最后还会有关键词3。" * 1000 60startTime = time.time() 61result = gfw.filter(content) 62print(time.time()-startTime)
这里我们的字典库是:
1with open("./sensitive_words", 'w') as f: 2 f.write("\n".join( [ "关键词" + str(i) for i in range(0,10000)]))
执行结果:
0.06450581550598145
可以看到性能进一步提升。
AC自动机过滤敏感词算法
接下来,我们来看一下 AC自动机过滤敏感词算法:
AC自动机:一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。
简单地讲,AC自动机就是字典树+kmp算法+失配指针
代码实现:
1import time 2class Node(object): 3 def __init__(self): 4 self.next = {} 5 self.fail = None 6 self.isWord = False 7 self.word = "" 8 9 10class AcAutomation(object): 11 12 def __init__(self): 13 self.root = Node() 14 15 # 查找敏感词函数 16 def search(self, content): 17 p = self.root 18 result = [] 19 currentposition = 0 20 21 while currentposition < len(content): 22 word = content[currentposition] 23 while word in p.next == False and p != self.root: 24 p = p.fail 25 26 if word in p.next: 27 p = p.next[word] 28 else: 29 p = self.root 30 31 if p.isWord: 32 result.append(p.word) 33 p = self.root 34 currentposition += 1 35 return result 36 37 # 加载敏感词库函数 38 def parse(self, path): 39 with open(path, encoding='utf-8') as f: 40 for keyword in f: 41 temp_root = self.root 42 for char in str(keyword).strip(): 43 if char not in temp_root.next: 44 temp_root.next[char] = Node() 45 temp_root = temp_root.next[char] 46 temp_root.isWord = True 47 temp_root.word = str(keyword).strip() 48 49 # 敏感词替换函数 50 def wordsFilter(self, text): 51 """ 52 :param ah: AC自动机 53 :param text: 文本 54 :return: 过滤敏感词之后的文本 55 """ 56 result = list(set(self.search(text))) 57 for x in result: 58 m = text.replace(x, '*' * len(x)) 59 text = m 60 return text 61 62 63acAutomation = AcAutomation() 64acAutomation.parse('./sensitive_words') 65startTime = time.time() 66print(acAutomation.wordsFilter("这是一个关键词替换的例子,这里涉及到了关键词1还有关键词2,最后还会有关键词3。"*1000)) 67print(time.time()-startTime)
词库同样是:
1with open("./sensitive_words", 'w') as f: 2 f.write("\n".join( [ "关键词" + str(i) for i in range(0,10000)]))
使用上面的方法,测试结果为0.017391204833984375。
敏感词过滤方法小结
可以看到这个所有算法中,在上述的基本算法中DFA过滤敏感词性能最高,但是实际上,对于后两者算法,并没有谁一定更好,可能某些时候,AC自动机过滤敏感词算法会得到更高的性能,所以在生产生活中,推荐时候用两者,可以根据自己的具体业务需要来做。
实现敏感词过滤API
将代码部署到Serverless架构上,可以选择API网关与函数计算进行结合,以AC自动机过滤敏感词算法为例:我们只需要增加是几行代码就好,完整代码如下:
1# -*- coding:utf-8 -*- 2 3import json, uuid 4 5 6class Node(object): 7 def __init__(self): 8 self.next = {} 9 self.fail = None 10 self.isWord = False 11 self.word = "" 12 13 14class AcAutomation(object): 15 16 def __init__(self): 17 self.root = Node() 18 19 # 查找敏感词函数 20 def search(self, content): 21 p = self.root 22 result = [] 23 currentposition = 0 24 25 while currentposition < len(content): 26 word = content[currentposition] 27 while word in p.next == False and p != self.root: 28 p = p.fail 29 30 if word in p.next: 31 p = p.next[word] 32 else: 33 p = self.root 34 35 if p.isWord: 36 result.append(p.word) 37 p = self.root 38 currentposition += 1 39 return result 40 41 # 加载敏感词库函数 42 def parse(self, path): 43 with open(path, encoding='utf-8') as f: 44 for keyword in f: 45 temp_root = self.root 46 for char in str(keyword).strip(): 47 if char not in temp_root.next: 48 temp_root.next[char] = Node() 49 temp_root = temp_root.next[char] 50 temp_root.isWord = True 51 temp_root.word = str(keyword).strip() 52 53 # 敏感词替换函数 54 def wordsFilter(self, text): 55 """ 56 :param ah: AC自动机 57 :param text: 文本 58 :return: 过滤敏感词之后的文本 59 """ 60 result = list(set(self.search(text))) 61 for x in result: 62 m = text.replace(x, '*' * len(x)) 63 text = m 64 return text 65 66 67def response(msg, error=False): 68 return_data = { 69 "uuid": str(uuid.uuid1()), 70 "error": error, 71 "message": msg 72 } 73 print(return_data) 74 return return_data 75 76 77acAutomation = AcAutomation() 78path = './sensitive_words' 79acAutomation.parse(path) 80 81 82def main_handler(event, context): 83 try: 84 sourceContent = json.loads(event["body"])["content"] 85 return response({ 86 "sourceContent": sourceContent, 87 "filtedContent": acAutomation.wordsFilter(sourceContent) 88 }) 89 except Exception as e: 90 return response(str(e), True)
最后,为了方便本地测试,我们可以增加:
1def test(): 2 event = { 3 "requestContext": { 4 "serviceId": "service-f94sy04v", 5 "path": "/test/{path}", 6 "httpMethod": "POST", 7 "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 8 "identity": { 9 "secretId": "abdcdxxxxxxxsdfs" 10 }, 11 "sourceIp": "14.17.22.34", 12 "stage": "release" 13 }, 14 "headers": { 15 "Accept-Language": "en-US,en,cn", 16 "Accept": "text/html,application/xml,application/json", 17 "Host": "service-3ei3tii4-251000691.ap-guangzhou.apigateway.myqloud.com", 18 "User-Agent": "User Agent String" 19 }, 20 "body": "{\"content\":\"这是一个测试的文本,我也就呵呵了\"}", 21 "pathParameters": { 22 "path": "value" 23 }, 24 "queryStringParameters": { 25 "foo": "bar" 26 }, 27 "headerParameters": { 28 "Refer": "10.0.2.14" 29 }, 30 "stageVariables": { 31 "stage": "release" 32 }, 33 "path": "/test/value", 34 "queryString": { 35 "foo": "bar", 36 "bob": "alice" 37 }, 38 "httpMethod": "POST" 39 } 40 print(main_handler(event, None)) 41 42 43if __name__ == "__main__": 44 test()
完成之后,我们就可以测试运行一下,例如我的字典是:
1呵呵 2测试
执行之后结果:
{'uuid': '9961ae2a-5cfc-11ea-a7c2-acde48001122', 'error': False, 'message': {'sourceContent': '这是一个测试的文本,我也就呵呵了', 'filtedContent': '这是一个**的文本,我也就**了'}}
接下来,我们将代码部署到云端,新建serverless.yaml:
1sensitive_word_filtering: 2 component: "@serverless/tencent-scf" 3 inputs: 4 name: sensitive_word_filtering 5 codeUri: ./ 6 exclude: 7 - .gitignore 8 - .git/** 9 - .serverless 10 - .env 11 handler: index.main_handler 12 runtime: Python3.6 13 region: ap-beijing 14 description: 敏感词过滤 15 memorySize: 64 16 timeout: 2 17 events: 18 - apigw: 19 name: serverless 20 parameters: 21 environment: release 22 endpoints: 23 - path: /sensitive_word_filtering 24 description: 敏感词过滤 25 method: POST 26 enableCORS: true 27 param: 28 - name: content 29 position: BODY 30 required: 'FALSE' 31 type: string 32 desc: 待过滤的句子
然后通过sls --debug进行部署,部署结果:

最后,通过PostMan进行测试:

总结
敏感词过滤是目前非常常见的需求/技术,通过敏感词过滤,我们可以在一定程度上降低恶意言语或者违规言论的出现,在上述实践过程,有以下两点内容:
- 对于敏感词库额获得问题:Github上有很多,可以自行搜索下载,因为敏感词词库里面有很多敏感词,所以我也不能直接放在这个上面供大家使用,所以还需要大家自行在Github上搜索使用;
- 这个API使用场景的问题:完全可以放在我们的社区跟帖系统/留言评论系统/博客发布系统中,防止出现敏感词汇,可以降低不必要的麻烦出现。
Serverless Framework 30 天试用计划
我们诚邀您来体验最便捷的 Serverless 开发和部署方式。在试用期内,相关联的产品及服务均提供免费资源和专业的技术支持,帮助您的业务快速、便捷地实现 Serverless!
One More Thing
3 秒你能做什么?喝一口水,看一封邮件,还是 —— 部署一个完整的 Serverless 应用?
复制链接至 PC 浏览器访问:https://serverless.cloud.tencent.com/deploy/express
3 秒极速部署,立即体验史上最快的 Serverless HTTP 实战开发!
传送门:
- GitHub: github.com/serverless
- 官网:serverless.com
欢迎访问:Serverless 中文网,您可以在 最佳实践 里体验更多关于 Serverless 应用的开发!