軟件工程第1次作業—詞頻統計
作業要求的博客鏈接:https://edu.cnblogs.com/campus/nenu/2016CS/homework/2110
git倉庫地址:https://git.coding.net/isak_even/wf.git
一、本次作業采用c/c++進行編程。首先分析項目要求概括功能:
1)對指定的文件進行詞頻統計,註意此文件應和wf.exe在同一目錄下。(輸入 wf -c xxx.txt)
2)輸入指定路徑,對其目錄下文件名稱按照字典序最靠前的文本文件進行詞頻統計。(輸入 wf -f X:\xxx\xxx )
3)對指定文件名或者指定路徑下的字典序最靠前的文件進行詞頻統計,輸出頻率最高的N個詞。(輸入 wf -f X:\xxx\xxx -n X 或者 wf -c xxx.txt -n X ,“-n X”的位置可前可後)
4)對指定路徑下的指定文件進行詞頻統計,也具有輸出頻率最高的N個詞的功能。(輸入 wf -f X:\xxx\xxx -c xxx.txt [-n X])
二、根據分析規劃函數模塊:
1)處理輸入(分別處理路徑名、文件名、數字N)。
2)搜索指定路徑下的所有txt文件,並且選定字典序最靠前的文件。
3)處理指定文本文件的內容,保存為字符串。
4)切割字符串並且統計詞頻。
5)主函數,負責調用各個模塊,控制流程。
三、模塊代碼實現過程:
1)處理輸入(分別處理路徑名、文件名、數字N)。
輸入時用檢測是否出現特定字符來處理不同的內容。比如出現“-f”時後面就是路徑名,出現“-c”時後面就是文件名。
這裏的主要問題是如何在字符串裏找指定的子串,我使用的是find(),此函數如果找到子串會返回第一個字符的索引,如果沒有找到,則返回npos。
文件名和數字N的處理方法一樣,只是在處理數字時要把字符轉化為數字,可以用 sscanf( number.c_str(),"%d",&number1 );
具體代碼如下:
/*---處理輸入的路徑---*/ bool getfilepath() { bool exist=false; string::size_type index2 = inputstr.find("-f"); if(index2!=string::npos) { exist=true; index2=index2+3; while(inputstr[index2]!=‘ ‘&&inputstr[index2]!=‘\n‘) { path_name=path_name+inputstr[index2]; index2++; } } return exist; }
2)搜索指定路徑下的所有txt文件,並且選定字典序最靠前的文件。
這裏的主要思路是獲取該路徑下所有符合要求的文件(用format 控制文件類型,在這裏傳入的是".txt"),將所有合適的文件名放在files中。
然後在主函數中用sort給files排序,files[0]就是我們需要的文件。
這段代碼借鑒自https://www.cnblogs.com/tgyf/p/3839894.html
/*-------獲取特定格式的文件名-------*/ void GetAllFormatFiles( string path, vector<string>& files,string format) { long hFile = 0;//文件句柄 struct _finddata_t fileinfo; //文件信息 string p; if((hFile = _findfirst(p.assign(path).append("\\*" + format).c_str(),&fileinfo)) != -1) { do { if((fileinfo.attrib & _A_SUBDIR)) { if(strcmp(fileinfo.name,".") != 0 && strcmp(fileinfo.name,"..") != 0) { GetAllFormatFiles( p.assign(path).append("\\").append(fileinfo.name), files,format); } } else { files.push_back(p.assign(fileinfo.name)); } }while(_findnext(hFile, &fileinfo) == 0); _findclose(hFile); } }
3)處理指定文本文件的內容,保存為字符串。
這裏想特別說的是為了確保上一行的最後一個單詞和本行第一個單詞分割開來,所以采用一行一行讀入,並在行末加空格再連接。
/* ----實現讀取指定文件的功能-----*/ void readtxt(string filename) { ifstream file; file.open(filename.c_str());//註意一定要轉化為 char * string s; while(getline(file, s))//按行讀取 { str=s+‘ ‘+str;//加空格確保分割開行尾和行首的兩個單詞 } transform(str.begin(), str.end(), str.begin(), ::tolower);//將大寫轉化為小寫 file.close(); //關閉文件 }
4)切割字符串並且統計詞頻。
我在看到統計詞頻時首先想到的就是用map,這樣就可以用鍵和值來儲存相應的詞和詞頻。然後處理字符串的時候,順便把第一字符是數字的篩選掉。
nu是處理N的函數,基本構造和處理路徑名的一樣,會返回一個bool值,然後如果有數字就把數值保存在全局變量number1中。
當輸入有N時,需要對詞頻排序,但是map默認的是對鍵排序,所以這裏我把map裏的值和鍵放在vector中,再用sort排序。
這個思路來自 :https://blog.csdn.net/qq_26399665/article/details/52969352
第一次測試這個功能時發現輸出格式不整齊,便對輸出格式做了規範。借鑒自:https://blog.csdn.net/wolinxuebin/article/details/7490113
/* ----實現分割字符串並進行字頻統計的功能----*/ void cutwords() { bool nu = getnumber();//判斷有沒有數字限制 map<string, long> word_count; bool temp=false; //讀取一個數據標誌位 string word; string b; for (long i=0;i<str.length();i++) { while(str[i]>=‘0‘&&str[i]<=‘9‘||str[i]>=‘a‘&&str[i]<=‘z‘) { temp=true; b+=str[i]; i++; } if(temp) { word=b; if(word[0]>‘9‘||word[0]<‘0‘) //判斷第一個字符是不是數字 ++word_count[word]; b=""; word=""; temp=false; } } if(!nu)//如果沒有數字 { cout<<"total "<<word_count.size()<<" words"<<endl; for(map<string,long>::iterator it=word_count.begin() ;it!=word_count.end();it++) cout<<left<<setw(20)<<it->first<<it->second<<endl;//輸出格式為左對齊,寬度為20 } else //如果有數字將map的鍵和值存放在一個pair類型的vector中,用sort排序 { cout<<"Total words is "<<word_count.size()<<endl; cout<<"-----------------"<<endl; vector<PAIR> word_count_vec(word_count.begin(), word_count.end()); //對vector排序 sort(word_count_vec.begin(), word_count_vec.end(), CmpByValue()); for (long i=0;i<number1;i++) cout << left << setw(20) << word_count_vec[i].first << word_count_vec[i].second << endl; } }
5)主函數,負責調用各個模塊,控制流程。
在主函數中對只有文件名,只有文件路徑和兩者都有的不同輸入進行了處理。需要註意的一點是在輸入時如果用cin會導致遇到空格便停止,所以我使用getline來進行輸入。
除此之外,本程序存在一點小bug,就是之前在測試的時候如果最後不輸入空格,會出現亂碼現象,所以我在inputstr後面自行加了一個空格,這樣不會影響程序使用,也不會影響用戶的輸入。
/*-----主函數-------*/ int main() { string allname;//(路徑)+文件名的字符串 getline(cin,inputstr);//讀取輸入(確保空格也讀入) inputstr=inputstr+" ";//不在最後加空格會亂碼(這是本程序的小bug) bool pa = getfilepath();/* 獲取文件路徑*/ bool na = getfilename();/* 獲取文件名*/ if(!pa&&na)//如果只有文件名沒有路徑 { allname=file_name;//就默認是相同目錄下的文件 } else if(!na&&pa)//如果只有路徑沒有文件字 { vector<string> files; string format = ".txt";//選擇文件類型 GetAllFormatFiles(path_name, files,format);//獲取路徑下的所有該類型的文件 sort(files.begin(),files.end(),cmp);//將文件名按字典序排序 allname=path_name+"\\"+files[0]; //將排序第一的文件名賦給字符串 } else if(na&&pa) { allname=path_name+"\\"+file_name; } readtxt(allname);//讀文件 cutwords(); //切割字符串並統計詞頻 getchar(); //防止啟動exe文件完成計算後閃退 return 0; }
四、測試階段:
1)樣例測試:(測試文件中共有17個詞)
2)詩歌測試:(測試文件中共有32個詞)
3)長篇英文小說測試:
3.1 《霧都孤兒》全篇157898個單詞
3.2《哈利波特和火焰杯》全篇198919個單詞
五、PSP階段表格:
六、總結反思
這次的項目用時還是很長的,主要原因在於對語言運用的不夠熟練,在本次作業中需要用到很多c++的stl,比如處理輸出格式輸入格式的,比如讀取文件的等等。分析完功能後在設計函數時為了傳遞數據更簡便,所以定義了一些全局變量,但是在最後復查代碼的時候發現這樣增加了函數之間的耦合性,不利於模快化,最後做了一些修改,但有些地方由於個人水平有限沒能完全改完,日後會再思考如何優化的。
做完作業要求的功能後,我自己也有一些想法,既然已經做完詞頻分析,其實還可以增添一些數據可視化,比如按詞頻高低做成詞雲圖,或者做個折線圖。之前聽到過有人用詞頻分析的方法辨別《紅樓夢》的前八十回和後四十回不是同一個人寫的,覺得這次的作業多加完善其實也可以實現類似的功能。
只是做詞頻分析感覺用c或者java都可以,學過python之後覺得它的第三方庫也有很大作用,比如做語義分析的jieba庫對中文有著很強大的分詞能力,所以之後完善項目做中文詞頻分析時會用python來做。
結合《構建之法》的第三章中初級軟件工程師如何成長來談談我的看法吧。書上說第一點是“積累軟件開發相關的知識”,這個是我現在最應該做的,畢竟下面幾點積累經驗,提升職業技能之類的成長在現在所處環境內有點難以實現。提升技術技能的話應該從語言入手,在項目中練手。不論是c/c++ 、java還是python 都各有優缺,學習完基本語法,就應該找一個相應的項目或者需求去實現一下,在這個實現的過程中才會體現出自己不理解的地方,不懂的地方,誠如老話所言“實踐是檢驗真理的唯一標準”。
軟件工程第1次作業—詞頻統計