第一次個人編程作業


https://github.com/wu372620060/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實在是太痛苦了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM