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樹類,需要完成樹的構建和檢索,存在以下方法:
-
def addWord(self, word)
傳入敏感詞,作為tire樹節點生成tire樹
-
def search(self, content, Hanzi_to_pinyin, _bushou) def search2(self, content, ...)
主函數調用search開始檢測,search遞歸調用search2達成tire樹的檢索以及檢索失敗的回溯。
-
-
word類:敏感詞類,需要完成敏感詞的轉換,存在以下方法:
-
def delWrap(self)
刪除原文和敏感詞的換行符
-
def Transformation(self)
生成多樣化的敏感詞。
-
def createRever(self)
生成rever字典,用於將不同形式的敏感詞映射至原敏感詞,放在答案的<>中。
-
def _arrangement(self, word, ...) def _appendList(self, ListA)
全排列方法,傳入Transformation生成的數組,並進行全排列。appendList配合.List.copy()方法便於淺拷貝。
-
def createLastWords(self)
調用self._arrangement()方法得到全排列結果,根據中英文不同做處理,得出最終的敏感詞數組。
-
def createBushou(self)
利用Transformer()生成的部首數組(words4)創建部首List,便於在查詢樹時選擇部首branch。
為什么不直接使用words4呢,此函數還起到過濾英文的作用。
-
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實在是太痛苦了。