第一次个人编程作业

第一次个人编程作业github链接

一、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 10 5
Development 开发
· Analysis · 需求分析 (包括学习新技术) 120 90
· Design Spec · 生成设计文档 30 30
· Design Review · 设计复审 20 15
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 20 40
· Design · 具体设计 180 240
· Coding · 具体编码 540 720
· Code Review · 代码复审 120 60
· Test · 测试(自我测试,修改代码,提交修改) 180 120
Reporting 报告
· Test Repor · 测试报告 30 15
· Size Measurement · 计算工作量 20 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 15 15
· 合计 1285 1360

二、计算模块接口

2.1 计算模块接口的设计与实现过程

2.1.1 设计思路(针对每个需求)

  • 中文敏感词可能进行一些伪装,在敏感词中插入除字母、数字、换行的若干字符仍属于敏感词。如:当山寨为敏感词词汇时,山_寨,山@寨,山 寨,均可视为敏感词。
  • 英文文本不区分大小写,在敏感词中插入若干空格、数字等其他符号(换行、字母除外),也属于敏感词,如hello为敏感词时,he_llo,h%ell@o,he llo均为敏感词。
    • 思路:设置一个特殊符号集合,这个特殊符合集合中不仅包含有特殊符号,还包含有数字空格,因此中英文敏感词的过滤可共用一个特殊符号集,当遇到敏感词头的时候,将Flag记为1,当Flag为1的时候遇到的特殊符号都为敏感词中的特殊符号,遇到敏感词尾的时候,再将Flag重置为0
    • 例子:#傻子……%中的“#”“……%”在检测时Flag都为0,就直接忽略。傻#¥子中的“#¥”则会被保留。
  • 中文文本中存在部分谐音替换、拼音替代、拼音首字母替代的敏感词(拼音不区分大小写),如 shan寨,栅寨,山Z等均可视为敏感词。还存在少部分较难检测变形如繁体
    • 思路:因为繁体和简体的拼音是相同的,而且还需要检测拼音以及拼音首字母,所以干脆将所有的敏感词汇都转化成拼音形式以及首字母形式,并将它们进行排列组合。然后创建一个字典专门用来存储 '错误文本:正确文本' 这样的键值对,在后续的算法中若检测出敏感词的各种形式,都可以在字典中找到正确的中文形式
    • 例子:笨蛋转化为拼音以及首字母并分别放进列表中,则笨:[ben,b], 蛋:[dan,d],再将其进行排列组合,变成'bendan','bend','bdan','bd',为后续的算法做准备。
  • 中文文本中还存在少部分较难检测变形如拆分偏旁部首(只考虑左右结构)
    • 思路:先去网上获取一个只拆分左右结构的字典,获取每个敏感词字的拆分形式,并存在字符串中。
    • 例子:卧槽可在字典中找到“臣卜木曹”,将其转为字符串,为后续算法做准备。
    • 展示部分字典

  • 对输出进行一些可视化表示,如自动生成统计图、词云图等等(不要在测试的main文件中体现)。
    • 思路:就用matplotlib.pyplot即可实现对答案文件的统计,较简单。

2.1.2 算法

我们本次作业要实现的功能是敏感词过滤。我上网查询了之后,发现在实现文字过滤的算法中,DFA是唯一比较好的实现算法。DFA 即 Deterministic Finite Automaton,也就是确定有穷自动机,它是通过event和当前的 state 得到下一个 state,即 event + state = nextstate。下图是网上了解的对其状态的转换:

但说实在的,我并没有看懂这个状态图,大家有感兴趣可以自行去了解。

  • 我的理解

    我们首先需要:
    • 一颗由敏感词汇形成的字典树
    • 待检测字符串
  • 如何得到一个敏感词树?

    首先是需要拥有一份敏感词汇文件,比如:中国,中国人民,牛逼,建立的树大概长下面这样:


    因为在上一点已经提到了我们要做到检测出繁体、拼音或首字母形式的敏感词,因此我们建立的字典树应该是以字母为结点的,格式与上图相同啦。(在进行拆分偏旁的敏感词检测时候,是以偏旁部首为结点。)
  • 为什么要组织成这样的一棵树呢?(算法思路)
    • 根据 DFA 的概念:通过event 和当前的 state 得到下一个 state,即 event + state = nextstate,它是一个字符一个字符的检测,如果检测的字符在我们的树中,就进入命中的树,看下一个字母在不在树里面,如果持续的命中就持续进入,最后完全命中了,也就是那个字的子树只有一个元素,并且元素的键是end(这里是在我们的这个例子中,看图就明白了)。就是完全命中了关键词,就可以记录命中。
    • 但在我们的例子中我们可以看见,我们检测到以下两种情况的时候,第一条:算法到中国就结束,检测不出中国人民,第二条:算法到中国人m就结束了,所以我们还需要用到长尾检测
      • 我是中国人民,我爱中国。
      • 我是中国人ming,我爱中国。(树中还存在'zhonguorenm')
    • 这样的长尾检测会引发一个问题,那就是回滚,当我们命中了前置的词,后续的没有命中的时候就得记录并且回滚,这个回滚的长度是是多少呢?解决回滚的方式也很简单:设置两个变量normal_stepmirror_image_step,前一个为正常步数,后一个为镜像步数,但需要回滚时,就将 normal_step - mirror_image_step 再加上初始检测位置,即完成回滚。

2.1.3 类与函数

  • 只有一个类DfaFilter
    • 用于进行字典树的创建过滤操作等。
  • DfaFilter类中含有五个个函数
    • GetSensitiveWords:对敏感词汇进行预处理,转化为拼音、首字母形式,对拆分偏旁做单独处理(转化成偏旁部首形式)。
    • DiGui:对GetSensitiveWords函数处理过后的一个词的每个字的拼音或首字母进行排列组合
    • AddSensitiveWords:对DiGui函数排列组合过后的字符串逐个加入到字典树中。
    • FilterSensitiveWords:对待检测字符串进行过滤敏感词操作。
    • SplitSideFilter:专门针对拆分偏旁部首的敏感词进行过滤操作。

2.2 计算模块接口部分的性能改进。

  • 利用pycharm专业版中自带的profile对程序进行性能测试
  • 按总消耗时间来排序:

可以看到除了main.py以外,pypinyin库占用了最多的时间,但是这是我认为这个库还是比较简便完善的,是我设计该程序时的硬性要求,所以并没有想到可以用什么库来代替,因此没有改善的思路。

  • 按自用消耗时间来排序:

可以看到FilterSensitiveWords函数(过滤敏感词)占用了最多的时间,听说用AC自动机可以优化性能,但由于我不会,所以使用了大量的if和else语句,当初在理清自己的逻辑时就花费了不少时间,不过跟别人对比起来,感觉自己的运行速度也不慢。

  • 自己程序中消耗最大的函数
    def FilterSensitiveWords(self, message, linepos):
        #起始位置
        start = 0
        #一行的敏感词总数
        part_total = 0
        while start < len(message):
            tree = self.Sensitive_dict
            #将文本进行切片后检测,比如start不断++,直到检测到敏感词头,则过滤后,start到达敏感词尾,然后继续切片检测
            message_chars = message[start:]
            #正常状态下,放入敏感词字母
            normal_ret = ''
            #分支状态下,放入敏感词字母,为了回退的时候,加入的字母也能同时回退,直接设置两个空字符串
            mirror_image_ret = ''
            #正常步数
            normal_step = 0
            #回退步数
            mirror_image_step = 0
            stop = 0
            #已经检测到敏感词标志
            Code_flag = 0
            #进入分支状态标志
            Branch_flag = 0
            for char in message_chars:
                #创建一个空字符串,用来存待检测字的拼音
                pychar=''
                for index,value in enumerate(pypinyin.pinyin(char,style=pypinyin.NORMAL)):
                    pychar+=''.join(value).lower()
                #front为敏感词头在原文本中的位置
                front = start
                #Code_flag状态为1,表明正在检测一个敏感词,此时如果待检测的字为特殊符号,则步数+1,进入下一次循环
                if Code_flag == 1 and pychar[0] in SpecialCode:
                    #检测的位置不能超过待检测文本长度
                    if (start+normal_step+1) < len(message):
                        #该记号表明前面遇到了分支,镜像步数用于回退
                        if Branch_flag == 1:
                            normal_step += 1
                            mirror_image_step += 1
                            continue
                        #若未遇到分支,则正常步数+1
                        else:
                            normal_step += 1
                            continue

                #遍历字符串中的每个字母
                for i in pychar:
                    #如果这个字母在敏感词树中
                    if i in tree:
                        #对于正常状态下,且值不含结束键的字母存入normal_ret
                        if Branch_flag == 0 and self.delimit not in tree[i]:
                            normal_ret += ''.join(i)
                        #对于值只有结束键的字母
                        elif self.delimit in tree[i] and len(tree[i]) == 1:
                            #分支状态
                            if Branch_flag == 1:
                                mirror_image_ret += ''.join(i)
                            #正常状态
                            else:
                                normal_ret += ''.join(i)
                        #分支状态下或者值不仅仅包含结束键
                        else:
                            #区分此时的字母在原文本中是为单字母还是一个字的拼音的一个字母
                            #如果是一个字的拼音的一个字母,比如***中的前一个g
                            if len(pychar) != 1:
                                mirror_image_ret += ''.join(i)
                            #如果是单字母,比如falg中的g
                            else:
                                normal_ret += ''.join(i)
                        Code_flag = 1
                        #结束键不在这个字母的字典中
                        if self.delimit not in tree[i]:
                            #跳转到i结点
                            tree = tree[i]
                        #结束键在这个字母的字典中,但子树的字典键值对不止一个,表明此时遇到了分支,进入分支状态
                        elif self.delimit in tree[i] and len(tree[i]) != 1:
                            Branch_flag = 1
                            #跳转到i结点
                            tree = tree[i]
                        #这个字母的字典里只有结束键,表示找到了敏感词尾
                        else:
                            #找到敏感词尾的位置
                            start += normal_step
                            back = start
                            part_total += 1
                            #找到错误文本对应的正确文本
                            true = dict[normal_ret+mirror_image_ret]
                            # 将结果写入ret列表中
                            ret.append('line'+str(linepos)+':'+'<'+true+'>'+message[front:back+1])
                            #停止遍历
                            stop = 1
                            break
                    #如果这个字母不在敏感词树中
                    else:
                        #如果为分支状态
                        if Branch_flag == 1:
                            if normal_ret in dict:
                                start += normal_step
                                #步数回退
                                start -= mirror_image_step
                                back = start
                                part_total += 1
                                true = dict[normal_ret]
                                ret.append('Line' + str(linepos) + ':' + '<' + true + '>' + message[front:back+1])
                                #重置分支状态
                                Branch_flag = 0
                                #停止遍历
                                stop = 1
                                break
                            else:
                                start += normal_step
                                start -= mirror_image_step
                                stop = 1
                                break
                        else:
                            #停止遍历
                            stop = 1
                            break
                if stop == 1:
                    break
                #分支状态下,镜像步数和正常步数要同步,用于回退
                if Branch_flag == 1:
                    normal_step += 1
                    mirror_image_step += 1
                else:
                    normal_step += 1
            start += 1
        #返回敏感词数量
        return part_total

2.3 计算模块部分单元测试展示

2.3.1 对过滤敏感词函数的测试

  • 验证FilterSensitiveWords函数:功能为除了拆分偏旁以外的形式检测
    • 构造思路:进行拼音、首字母、繁体、谐音字、插入符号的测试
    • 代码如下:
    def test_FilterSensitiveWords(self):
        words_path = 'words.txt'
        org = ['讲一个f123z@#u的故事吧\n',
                '30年前,女孩在男孩寝室楼下锤着男孩的肩膀哭诉着说:hanj就要来了,我就要回家了,\n',
               '你能让嫁@#期再短一点吗,让我们在一起的日子更长一点吗?你不能!\n',
               '男孩听到后默默流泪离开。30年后,男孩成为福州daxue校长,从此福州大學成为全国放假最晚的学校。\n']
        ans = ['Line1:<fzu>f123z@#u','Line2:<寒假>hanj','Line3:<假期>嫁@#期','Line4:<福州大学>福州daxue','Line4:<福州大学>福州大學']
        x = main.DfaFilter()
        x.GetSensitiveWords(words_path)
        ret = []
        for i in range(1,len(org)+1):
            y,ret = x.FilterSensitiveWords(org[i-1],i,ret)
        self.assertEqual(ans, ret)

  • 验证SplitSideFilter函数:进行拆分偏旁部首形式的检测
    • 构造思路:进行全拆分,或部分拆分敏感词的测试
    • 代码如下:
    def test_SplitSideFilter(self):
        words_path1 = 'words.txt'
        org1 = ['希望亻叚其月可以快点到来\n',
                '寒亻叚真的很短。\n']
        ans1 = ['Line1:<假期>亻叚其月','Line2:<寒假>寒亻叚']
        x1 = main.DfaFilter()
        x1.GetSensitiveWords(words_path1)
        ret1 = []
        for j in range(1,len(org1)+1):
            y1,ret1 = x1.SplitSideFilter(org1[j-1],j,ret1)
        self.assertEqual(ans1, ret1)


2.3.2 单元测试覆盖率

  • 查看代码覆盖结果


2.4 计算模块部分异常处理说明

  • 输入输出异常,即想打开的文件不存在时
    def test_IOE(self):
        try:
            file= open ('words.txt',encoding='utf-8')
        except IOError:
            print("[Error] No such file or directory: 'words.txt'")
        else:
            print("The file was read successfully")
            file.close()

  • 无法引入模块,基本上是路径问题或名称错误
    def test_Import(self):
        try:
            from abc import efg
        except ImportError:
            print("ImportError:no such module:abc")

  • 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5]
    def test_Index(self):
        x=[0,1,2]
        try:
            print(x[5])
        except IndexError:
            print("IndexError:list index out of range")

  • 试图访问字典里不存在的
    def test_Key(self):
        dict = {'柯老板':"牛逼","软工":"真难"}
        try:
            x = dict['lzf']
        except KeyError:
            print("KeyError:lzf")
        else:
            return x

  • 参数数量不为3
    def test_Parameter(self):
        if len(sys.argv)!=3:
            print("参数错误")
        else:
            return

三、心得

  • 体会:刚看见题目发布的时候粗略浏览了一下,就知道很难了,刚开始的时候一点思路都没,就想着有什么方便的库来实现,但是发现在安装以及使用上都太麻烦了,后来才想到用转拼音的方式来解决。大概花了将近30个小时来写这份作业,包括今天也写了大半天的博客了,感觉自己终于有点像“程序员”了。之前从来没花这么久的时间在打代码上,在身心俱疲的同时也感觉收益颇丰。从刚开始的只想实现一部分功能,慢慢地就一个功能一个功能的实现了(debug花了大半时间!老是de完一个bug出现另外一个bug),最后终于把所有功能都实现了(不知道有没有什么特别的情况自己没有想到)。
    • 看了下自己的样例答案和测评组给的,好像就只是我多检测出了几个英文的那个东西(给和谐两次了,怕了怕了,把下面的样例全部打码了呜呜呜呜呜别再封了要傻了。)

    • 最后,因为之前作业里有说将输出可视化,于是写了个统计图,可以将结果转化为柱状图。
    • 求网站管理员明察,别删我博客了!
posted @ 2021-09-16 11:36  雀食蟀  阅读(170)  评论(1编辑  收藏  举报