第一次个人编程作业

https://github.com/pat-chou-li/sensitiveWord

———————————————————————————————————————————————————

一、PSP表格

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

二、计算模块接口

(3.1)计算模块接口的设计与实现过程。设计包括代码如何组织,比如会有几个类,几个函数,他们之间关系如何,关键函数是否需要画出流程图?说明你的算法的关键(不必列出源代码),以及独到之处。(18')

3.1.2.1: 算法概述

tire树+递归函数进行状态转移查询

1.将敏感词进行变换,得到多类型的敏感词,如

"秋叶" -> 'qy' + 'qiuye' + '禾火口十'

2.将生成的多类型敏感词进行组合排列,如

'qy' + 'qiuye' + '禾火口十' -> 'qy' + 'qye' + 'q口十' + 'qiuy' + 'qiuye' + 'qiu口十' + '禾火y' + '禾火ye' + '禾火口叶'

3.构建tire树,标记isEnd节点。

4.如果我们遇到了原文中的一段字:'禾火叶',我们依次读取‘禾’、‘火’、‘叶’,并检测是否在部首List['禾','火',‘口’,‘叶’]中,在的话将其传入search中进行查询,并再将其转换成拼音再进行一次查询,否则只进行一次转换成拼音的查询。

无论任何情况都转换成拼音并进行查询的好处是,我们可以很容易的处理谐音,简繁体的情况,但是遇到部首则要特殊处理。

遇到非法字符跳过并计数即可,达到20次便return。

3.1.2.2: 算法细节

1.我们对于同一个节点Node_1,如果它存在于部首List,我们要进行两次查询。那么查询失败的时候怎么回到Node_1并保留在Node_1时的所有状态呢?

将函数写为递归形式,状态转移则是一次函数的递归调用。

当查询失败时自然而然会回到主函数继续执行下一条语句,由于只传递了参数的拷贝,在递归结束返回主函数时并不会有任何变量的改变,可以很轻易的发起下一次查询。

2.为什么要同时存‘qiu’和'q'->'i'->'u'

当丢入一个字进行查询,例如求,那么会从求转换成qiu导致增加了两个字符。如果你想要q->i->u依次进行查询则很难确定敏感词的起始和最终位置,起码会使代码复杂化。

如果存下'qiu'作为节点,那么求查到qiu就是一个一对一的对应关系,直接进行下一次查询,并将敏感词结束位置+1即可。

3.1.2: 类设计
  • Node类:作为Tire树节点,包含next、fail、isWord、depth成员变量

  • Ahocorasick类:Tire树类,需要完成树的构建和检索,存在以下方法:

    1. def addWord(self, word)
      

      传入敏感词,作为tire树节点生成tire树

    2. def search(self, content, Hanzi_to_pinyin, _bushou)
      def search2(self, content, ...)
      

      主函数调用search开始检测,search递归调用search2达成tire树的检索以及检索失败的回溯。

  • word类:敏感词类,需要完成敏感词的转换,存在以下方法:

    1. def delWrap(self)
      

      删除原文和敏感词的换行符

    2. def Transformation(self)
      

      生成多样化的敏感词。

    3. def createRever(self)
      

      生成rever字典,用于将不同形式的敏感词映射至原敏感词,放在答案的<>中。

    4. def _arrangement(self, word, ...) 
      def _appendList(self, ListA)
      

      全排列方法,传入Transformation生成的数组,并进行全排列。appendList配合.List.copy()方法便于浅拷贝。

    5. def createLastWords(self)
      

      调用self._arrangement()方法得到全排列结果,根据中英文不同做处理,得出最终的敏感词数组。

    6. def createBushou(self)
      

      利用Transformer()生成的部首数组(words4)创建部首List,便于在查询树时选择部首branch。

      为什么不直接使用words4呢,此函数还起到过滤英文的作用。

    7. def getAnswer(self)
      

      调用Ahocorasick树类,传入最终生成的敏感词和字典,在Tire树上查询并根据条件过滤,得到答案。

(3.2)计算模块接口部分的性能改进。记录在改进计算模块性能上所花费的时间,描述你改进的思路,并展示一张性能分析图(由VS 2019、JProfiler或者Jetbrains系列IDE自带的Profiler的性能分析工具自动生成),并展示你程序中消耗最大的函数。(12')

此处大约花费一小时。

最大的性能消耗是search2函数,即在Tire树上的状态转移递归调用。

对于搜索函数的性能改进,最需要的就是剪枝:

  • 改进前:对任何一个被检测的字word都转换成pinyin_word,如'法'转换成'法'和'fa'进行两次检测。

可以看到,search2函数加上子函数调用总耗时达到了0.176s

  • 改进后:对任何一个被检测的字,先检查是否为汉字且为部首,是的话就执行两次检测,否则只检测其拼音形式。(英文的拼音形式就是本身)

正如预期,search2的时间大幅减少,到达了0.132s。

而且由于剪枝,递归中涉及到的所有函数的被调用次数都减少了。

(3.3)计算模块部分单元测试展示。展示出项目部分单元测试代码,并说明测试的函数,构造测试数据的思路。并将单元测试得到的测试覆盖率截图,发表在博客中。(12')

部分单元测试代码:

1.验证全排列函数word.arrangement()

样例构建思路:该样例用于测试word类的全排列功能是否正常,此函数基本没有任何特殊情况,是一个基本的全排列递归算法。

    def test_arrangement(self):
        words = '秋叶'
        words1 = ['秋', '叶']
        words2 = ['qiu', 'ye']
        words3 = ['q', 'y']
        words4 = ['禾火', '口十']
        ans = [['qiu', 'ye'], ['qiu', 'y'], ['qiu', '\\ye'], ['qiu', '\\口十'], ['q', 'ye'], ['q', 'y'], ['q', '\\ye'], ['q', '\\口十'], [
            '\\qiu', 'ye'], ['\\qiu', 'y'], ['\\qiu', '\\ye'], ['\\qiu', '\\口十'], ['\\禾火', 'ye'], ['\\禾火', 'y'], ['\\禾火', '\\ye'], ['\\禾火', '\\口十']]
        test = sensitiveWord([words], [])
        res = test._arrangement(words, words1, words2, words3,
                                words4, 0, len(words), [], [])
        flag = True
        if len(res) != len(ans):
            flag = False
        else:
            for item in res:
                if item not in ans:
                    flag = False
                    break
        self.assertEqual(flag, True)

2.验证Tire树搜索函数AhoTree.search()

样例构建思路:测试中文中插入乱码、拆字、拆字并加入乱码等情况

    def test_search(self):
        words = ['欢笑', '不可思议', '国度']
        org = [
            '……最后一次看到还能这样欢@#$%^&*(#$%^笑的孩子究竟已是多久之前呢。\n',
            '从来没有听说过的不可12思议的歌声,不!!可思~~讠||义的舞蹈。看来今天似乎是祭典的日子。\n',
            '我想,总有一天也要住进有这样的孩子们的笑脸的国chi度。\n'
        ]
        ans_example = ['total: 2',
                       'Line1: <欢笑> 欢@#$%^&*(#$%^笑', 'Line2: <不可思议> 不!!可思~~讠||义']
        _sensitiveWord = sensitiveWord(words, org)
        _sensitiveWord.delWrap()
        _sensitiveWord.Transformation()
        _sensitiveWord.createRever()
        _sensitiveWord.createLastWords()
        _sensitiveWord.createBushou()
        ans = _sensitiveWord.getAnswer()
        self.assertEqual(ans_example, ans)

单元测试覆盖率截图:

(3.4)计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。(6')

IO异常:读取pinyin.txt等大文件时,未成功读入抛出异常:

    def test_IO(self):
        try:
            fp = codecs.open(path.join(path.dirname(
                __file__), 'pinyin.txt'), 'r', 'utf-8')
        except IOError:
            self.assertTrue(False)
            raise Exception("Can't load data from pinyin.txt")
        except UnicodeDecodeError:
            self.assertTrue(False)
            raise Exception("Can't decode data from pinyin.txt")
        else:
            for l in fp.readlines():
                self.table[l[0]] = l[1:-1]
            fp.close()
            self.assertTrue(True)

三、心得

(4.1)在完成本次作业过程的心得体会(3')

​ 由于最常用的语言JavaScript并没有被允许使用,临时学习了另一个动态语言python。用下来感觉虽然都是动态语言,但是差别还是蛮大的,很多js特性都没有在python有所体现。作为一个非python语言使用者,很多代码风格也被相关的静态代码分析器批判了。

​ 起初建树的时候疏于考虑,将所有字的拼音放进node里觉得就已经完事了,检测的时候只检测拼音,完美解决了谐音、简繁体,非法字符的处理也很简单。但是处理部首却遇到了巨大的困难,以致于后期不得不完全重构核心代码,浪费了大量时间。并且加了很多不够优美的分支判断,不知道会不会出现一些意外的bug。还是感到在设计算法的时候要深入思考,面面俱到,否则到code的时候边写代码边改bug实在是太痛苦了。

posted @ 2021-09-07 19:53  szly  阅读(368)  评论(0编辑  收藏  举报