WordCount編寫和測試——Python實現,wordcountpython
本次項目在github上的地址:https://github.com/MaximumRescue/WordCount-Project
寫在前面的話
首先我必須要強調,如果你覺得有問題,那一定是你沒有認真看下面的說明。
先說一點題外話,從上周任務分配下來到現在,需求在不斷變動,描述也非常模糊。既然這隻是一周的“小作業”,那麼就沒有必要把需求模糊化,對於任何一點說明都應該具體清晰,給出的範例必須規範正確,況且如果這門課程的重點在軟體品質提升和測試上,那麼就不應該在需求上閃爍其辭,這樣對大家的開發很不利。這一點還是給開發造成了很大的問題。
PSP表格
PSP2.1 |
PSP階段 |
預估耗時 (分鐘) |
實際耗時 (分鐘) |
Planning |
計劃 |
|
|
· Estimate |
· 估計這個任務需要多少時間 |
15 |
15 |
Development |
開發 |
|
|
· Analysis |
· 需求分析 (包括學習新技術) |
60 |
80 |
· Design Spec |
· 產生設計文檔 |
- |
- |
· Design Review |
· 設計複審 (和同事審核設計文檔) |
- |
- |
· Coding Standard |
· 代碼規範 (為目前的開發制定合適的規範) |
- |
- |
· Design |
· 具體設計 |
60 |
90 |
· Coding |
· 具體編碼 |
500 |
450 |
· Code Review |
· 代碼複審 |
60 |
40 |
· Test |
· 測試(自我測試,修改代碼,提交修改) |
120 |
120 |
Reporting |
報告 |
|
|
· Test Report |
· 測試報告 |
120 |
140 |
· Size Measurement |
· 計算工作量 |
20 |
15 |
· Postmortem & Process Improvement Plan |
· 事後總結, 並提出流程改善計劃 |
30 |
30 |
| |
合計 |
985 |
980 |
設計思路說明
1. 首先只考慮四個基礎功能:統計字元數、單詞數、行數,統計結果以指定格式輸出到預設檔案中。
那麼最先想到的一定是模組化,對於以上不同的功能分別構造相應的獨立的方法。我個人是不贊成方法的嵌套的,因為很多時候會造成思路的混亂,而且一旦核心功能出了問題,其他模組也就全部崩潰了。
於是這裡就開始利用python的檔案處理功能以及返回的檔案控制代碼的方法來進行處理。
(1)首先是統計字元功能,很簡單f.read()返回一個檔案從頭到尾的字串,包括\n,\t等空白字元,因此只需要統計返回的字串的長度就可以了。
說明:這裡的\n,\t均只視作一個字元。
1 # return the character number of a file2 def char_num():3 # open the file with the name 'filename'4 f = open(filename, 'r')5 totalstr = f.read()6 f.close()7 return filename + ', 字元數:' + str(len(totalstr))
(2)然後是統計行數功能,另一個方法f.readlines()返回一個檔案各行組成的列表,只需要統計列表元素個數即可。
1 # return the line number of a file2 def line_num():3 # open the file with the name 'filename'4 f = open(filename, 'r')5 lines = f.readlines()6 f.close()7 return filename + ', 行數:' + str(len(lines))
(3)統計單詞數有一點複雜,但也很簡單,我們在上面返回的每一行中進行尋找,尋找的標準就是Regex:單詞是由至少兩個字母組成的且不含其他符號(不考慮不定冠詞a,串連詞如five-year-old,縮寫如don't這幾類特殊詞彙)。於是用re.findall(r'[A-Za-z]{2, }')就可以篩選出所有的單詞了。
1 # return the word number of a file 2 def word_num(): 3 # open the file with the name 'filename' 4 f = open(filename, 'r') 5 lines = f.readlines() 6 f.close() 7 # get the word by regex expression 8 linewords = [] 9 for i in range(0, len(lines)):10 linewords.append(re.findall(r'[A-Za-z]{2,}', lines[i]))11 # count the number of words12 count = 013 for lineword in linewords:14 for word in lineword:15 if word.isalpha() == False or len(word) == 1:16 lineword.remove(word)17 if lineword != ['']:18 count += len(lineword)19 return filename + ', 單詞數:' + str(count)
(4)寫入檔案利用f.write()方法就可以寫入了,注意開啟時的mode必須是a(追加),不然會抹掉之前所有的記錄。
1 # write the information to the file2 def write_file(filename, info):3 f = open(filename, 'a')4 f.write(info)5 f.close()
這幾個部分的實現還算比較簡單。但是顯然這些方法共用一個參數,所以為了方便起見,我設計了一個類包含了所有的方法,具體內容請參考上面的連結。
2. 接下來是關於擴充功能的說明
擴充功能為:遞迴處理目錄下合格檔案;傳回碼行/空行/注釋行;忽略停用詞表的單詞(保留字)。
(1)先從最簡單的保留文書處理開始說,這個與之前單詞處理很類似,區別是要除掉保留字,於是我們只需要構造一個保留字列表,判斷得到的單詞在不在表中,如果在則刪掉,統計保留下來的單詞數即可,於是我們把之前的方法進行改寫即可。擷取保留字的方法類似於擷取單詞的過程:
1 # get the preserved words list 2 def get_prelist(fname): 3 filename = join(sys.path[0], fname).replace('\\', '\\\\') 4 fhandle = open(filename, 'r') 5 wholestr = fhandle.read() 6 prelist = wholestr.split(' ') 7 for word in prelist: 8 if word == "" or word == "\n" or word == "\t": 9 prelist.remove(word)10 return prelist
(2)接下來說明一下對行的精確統計工作,正如我們在前面說明的,f.readlines()返回的是一個由各行字串組成的列表,那麼我們對每一個字串做一些處理:
先使用str.replace(a, b)方法將字串中的分行符號,定位字元和空格全部去掉,接下來就進行判斷:
如果開頭是'//'的話就作為注釋行,不考慮多行注釋;如果一行只有一個字元或沒有字元就作為空白行;其他全部視為程式碼。
1 # return line details of a file 2 def line_detail(): 3 # open the file with the name 'filename' 4 f = open(filename, 'r', encoding='utf-8') 5 # get all line string into a list 6 lines = f.readlines() 7 f.close() 8 # distinguish different lines 9 codelines, emptylines, commentlines = [], [], []10 for line in lines:11 tmpline = line.replace(' ', '')12 tmpline = tmpline.replace('\t', '')13 tmpline = tmpline.replace('\n', '')14 if len(tmpline) == 1 or len(tmpline) == 0:15 emptylines.append(line)16 elif tmpline.startswith('//'):17 commentlines.append(line)18 else:19 codelines.append(line)20 return filename + ', 程式碼/空行/注釋行:' + str(len(codelines)) + '/'\21 + str(len(emptylines)) + '/' + str(len(commentlines))
(3)關於嵌套處理檔案夾下所有的合格檔案,我們把問題進行分治:
第一,我們需要擷取檔案類型,這個的思路很簡單,我們在命令列中擷取相應參數,利用字串尾部截取即可獲得對應的檔案類型。
第二,我們根據擷取的檔案類型,在整個執行檔案夾下進行遞迴尋找,返回得到的檔案的絕對路徑列表。
1 # determine the type needed 2 tmplist = desfile.split('.') 3 type = '.' + tmplist[-1] 4 # get the required list 5 localinfo = directory.build_infolist(type) 6 # build up the information list from the file list 7 def build_infolist(type=''): 8 filenames = build_filelist(type) 9 dirnames = build_dirlist()10 # get information by creating a FileInfo object11 infos = []12 for fname in filenames:13 info = FileInfo(path, fname)14 infos.append(info)15 # deal with the inner directory16 for dirname in dirnames:17 new_path = join(path, dirname)18 newdir = DirInfo(new_path)19 new_infos = newdir.build_infolist(type)20 infos = infos + new_infos21 return infos
3. 進階功能:顯示圖形介面,使用者通過介面選取單個檔案,程式顯示相關統計資訊。
同樣是分治,一部分是通過顯示對話方塊擷取檔案路徑和檔案名稱,另一部分就是將擷取的資訊做上面的操作。
痛點在於顯示對話方塊,顯示對話方塊需要使用wx庫,利用裡面的FileDialog方法建立對話方塊,FileDialog.getdirectory()和FileDialog.getname()擷取檔案路徑和檔案名稱。給一些範例程式碼:
注意:這裡只顯示字元數,行數,單詞數(無保留字)
1 # A method to get filename by dialog 2 def dialog_get(): 3 app = wx.App() 4 frame = wx.Frame(None) 5 openwildcard = "All files(*.*)|*.*|" + \ 6 "C/C++ files(*.c;*.cpp;*.h)|*.c;*cpp;*.h|" + \ 7 "Java source files(*.java)|*.java|" + \ 8 "Python source files(*.py)|*.py|" + \ 9 "Text files(*.txt)|*.txt"10 # Create open file dialog11 openFileDialog = wx.FileDialog(frame, "Choose a file to open",12 wildcard=openwildcard, 13 style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)14 openFileDialog.ShowModal()15 dir = openFileDialog.GetDirectory()16 filename = openFileDialog.GetFilename()17 openFileDialog.Destroy()18 return dir, filename
原始碼模組說明
| FileInfo.py |
class FileInfo |
Attributes: path, fname, filename Methods: char_num(), word_num(prelist), line_num(), line_detail(), write_info() |
| DirInfo.py |
class DirInfo |
Attributes: path Methods: build_filelist(tyoe), build_dirlist(), build_infolist(type) |
| ExtraOpt.py |
- |
Methods: get_prelist(fname), dialog_get() |
| main.py |
- |
(No methods, but some important codes used as main() in java) |
最後其實還有一個setup.py檔案用於將指令檔打包成exe檔案,但這不是我們實現項目的重點,因此沒有列出來。
軟體運行說明
1. .exe檔案運行注意要點:
首先說明一下將python指令檔打包成exe檔案的方法,事先你要安裝py2exe庫,然後構造一個setup.py檔案用於打包,對於這個檔案用命令列運行:
python <filename>.py py2exe
setup.py檔案組織可以是這樣的:
1 from distutils.core import setup2 import py2exe3 setup(console=["main.py"])
那麼打包出來的情況是怎麼樣的呢?打包過程先建立一個dist檔案夾,然後將PythonXX原檔案夾裡的部分檔案複製到這裡來,然後構造一些運行必需的註冊表。大概的檔案結構如所示:
因此如果需要啟動並執行話,必須把所有檔案放在一個檔案夾裡,內容均在BIN檔案夾裡。其他運行和.exe檔案運行類似。
2. 如果.exe檔案運行不了的話,可以下載__pycache__檔案夾裡的.pyc檔案和main.py檔案,放在一個檔案夾下,用python命令運行。
Python版本3.4以上,運行代碼:
python main.py [arguments]
3. 如果上面的都不行的話,保證Python 3.4+,安裝wxPython庫,下載所有.py檔案,運行main.py指令碼,其他和2.中內容相同。
測試過程設計
1. 先說明一下寫完以後對代碼的初步評估:
(1)命令列參數處理的結構不夠最佳化,這個部分也很無奈,我採用的字典構造方法是將檔案和對應巨集指令引數關聯,但這種是否是最優方法,我依舊存疑,關於這個方面需要參考DOS或者python終端的參數處理方法;
(2)這裡考慮到一種簡單的情況,即檔案都能打得開,沒有添加異常處理機制,但是我認為異常處理是必須的,時間有限沒有加以完善。
(3)檔案部署和模組化設計似乎還有一些提升空間。
總之,我覺得這裡還是需要做一些最佳化的,關於python debugger等等工具還是要學一下的。
2. 具體測試:篇幅有限,給出三個檔案及部分結果顯示:
(1)圖形介面測試:命令:
wc.exe -x
file_1.c
1 {1}2 2
file_2.c
1 a2 a/3 a+b
file_3.c
1 ---2
顯示的介面如下:
輸出的結果如下:
1.
file_1.c, 字元數:5file_1.c, 行數:2file_1.c, 單詞數:02.
file_2.c, 字元數:8file_2.c, 行數:3file_2.c, 單詞數:03.
file_3.c, 字元數:4file_3.c, 行數:1file_3.c, 單詞數:0
(2)單個檔案命令測試:
testfile.c
1 {2 }//3 //4 a5 aasa//6 while and or if endif
StopList.txt
while if for
那麼測試各個命令:
4. wc.exe -c testfile.ctestfile.c, 字元數:525. wc.exe -w testfile.ctestfile.c, 單詞數:66. wc.exe -l testfile.ctestfile.c, 行數:67. wc.exe -a testfile.ctestfile.c, 程式碼/空行/注釋行:3/2/18. wc.exe -w testfile.c -e StopList.txttestfile.c, 單詞數:49. wc.exe -s -c -w -l *.c -e StopList.txtfile_1.c, 字元數:5file_1.c, 單詞數:0file_1.c, 行數:2file_2.c, 字元數:8file_2.c, 單詞數:0file_2.c, 行數:3file_3.c, 字元數:4file_3.c, 單詞數:0file_3.c, 行數:1testfile.c, 字元數:52testfile.c, 單詞數:4testfile.c, 行數:610. wc.exe -c -w -l -a testfile.c -o result.txttestfile.c, 字元數:52testfile.c, 單詞數:6testfile.c, 行數:6testfile.c, 程式碼/空行/注釋行:3/2/1
這裡顯示的正確的結果,過程中出過幾個問題並改正過來了:
1)-e 過程檔案無法讀取:原因:get_prelist()擷取路徑錯誤,打包成exe後路徑需要重新定向
解決方案:把cur_file_dir()放到ExtraOpt.py中使用該方法路徑就重新調好了。
2)-w -e重複輸出:原因:兩個設定在不同的判斷條件中,並沒有同時判斷。
解決方案:把關於保留字列表的處理放在-w的處理過程中,-e單獨擷取列表。
目前還沒有出現其他問題,但是一定要注意命令列的正確使用,錯誤的命令列得不到正確的結果。
小結
這次項目內容還是比較充實的,說起來大家都是快畢業的人了,上過的課也都不少了,有經驗的人都明白,構建一個項目,不管它再小,有意無意地都會盡量保證一個相對清晰合理的構架。很多厲害的同學寫出來的代碼都是模組劃分非常清晰,結構非常規整的,其健壯性和可重用性都很高。這種做法往往就意味著更高的工作量。搜尋資訊還是有些困難的,在搜尋引擎中百度屬於最低一級,必應屬於中級,google才是進階(雖然不能用),搜尋出來的常常不能滿足要求,需要大量尋找和人工篩選。而像CSDN,部落格園,開源中國這些專業網站上的部落格抄襲轉載的很多,而且更新的很慢(也許現在大家都去做架構或者AI去了,沒人管基礎內容的更新),使用語言的特性很多都是已經被淘汰的,新特性在官方文檔上的說明和範例並不是那麼好懂的(手冊類的文檔,又不是教材)。經常會出現你想找一種功能的實現方法,跳出來的大多是垃圾資訊或者無用的內容,可以說在開發過程中很大一部分時間是浪費在提取有用資訊的過程上。
但是做出來了整個過程還是很值得的。不過話說回來,雖然我的測試目前沒有檢測到問題,但還是需要進一步測試,更加複雜的測試案例是必要的。總之如果上面的設計過程或實現方法有什麼問題,希望大家不吝賜教。