作業要求
1. 對源文件(*.txt,*.cpp,*.h,*.cs,*.html,*.js,*.java,*.py,*.php等)統計字符數、單詞數、行數、詞頻,統計結果以指定格式輸出到默認文件中,以及其他擴展功能,並能夠快速地處理多個文件
2. 使用性能測試工具進行分析,找到性能的瓶頸並改進
3. 對代碼進行質量分析,消除所有警告
4. 設計10個測試樣例用於測試,確保程序正常運行(例如:空文件,只包含一個詞的文件,只有一行的文件,典型文件等等)
功能要求
1. 統計文件的字符數
2. 統計文件的單詞總數
3. 統計文件的總行數
4. 統計文件中各單詞的出現次數
5. 對給定文件夾及其遞歸子文件夾下的所有文件進行統計
6. 統計兩個單詞(詞組)在一起的頻率,輸出頻率最高的前10個
注意:
a) 空格,水平制表符,換行符,均算字符,需要統計的字符ASCII碼值范圍:32-126,其他字符均可視為分隔符
b) 單詞的定義:至少以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫
英文字母:A-Z,a-z
字母數字符號:A-Z,a-z,0-9
分割符:空格,非字母數字符號
例如:”file123”是一個單詞,”123file”不是一個單詞。file,File和FILE是同一個單詞。
如果兩個單詞只有最后的數字結尾不同,則認為是同一個單詞,例如,windows,windows95和windows7是同一個單詞,iPhone4和IPhone5是同一個單詞,但是,windows和windows32a是不同的單詞,因為他們不是僅有數字結尾不同
輸出按字典順序,例如,windows95,windows98和windows2000同時出現時,輸出windows2000
詞組的定義:windows95 good, windows2000 good123,可以算是同一種詞組。按照詞典順序輸出。
c) 輸入文件名以命令行參數傳入
d) 輸出文件result.txt
characters: number
words: number
lines: number
<word>: number
<word>為文件中真實出現的單詞大小寫格式,例如,如果文件中只出現了File和file,程序不應當輸出FILE,且<word>按字典順序(基於ASCII)排列,上例中程序應該輸出File: 2
e) 根據命令行參數判斷是否為目錄
初步思路
1.文件遍歷:采用深度優先搜索的方法遞歸遍歷根目錄下的所有文件
2.行數統計:以按行讀取的方式讀取文本文件,每次成功讀取后對應的counter+1,直至文件末尾
3.字符數統計:判斷字符的ASCII碼范圍是否是32-126,若是則對應的counter+1,若不是則繼續看下一個字符
4.單詞統計:根據要求,只有由至少4個英文字母打頭的連續英文、數字字符串才算做一個單詞,並且單詞實體忽略數字尾端以及不區別大小寫字母,因此可以用單詞的前四個字母計算出偏移量,並根據這個偏移量將單詞放入哈希散列表中,同時每個單詞應具有最簡形式、已統計數量等屬性,故采用結構體的形式存儲
5.詞組統計:一個詞組由兩個單詞構成,故可以用指向兩個單詞結構體的指針表示詞組

typedef struct My_Word_Class { string word; string trimmed; //word without digital suffix and capital character intptr_t number; struct My_Word_Class *next; }MyWC; typedef struct My_Group_Class { MyWC *firstWord, *secondWord; intptr_t number; struct My_Group_Class *next; }MyGC;
代碼實現
1.宏定義與全局變量

#define WORD_LIMIT 4 #define HASH_CAPACITY 456976 #define DIGIT_FIRST 17576 #define DIGIT_SECOND 676 #define DIGIT_THIRD 26 #define MOST_FREQUENT_NUM 10 long charactNum = 0, wordNum = 0, textLine = 0; MyWC *(ptrOfWord[HASH_CAPACITY]); MyGC *(ptrOfGroup[HASH_CAPACITY]);
2.打開單個文件或遍歷文件夾下所有文件

vector<char *> getFilesList(const char *dir) { vector<char *> allPath; string dirNew; dirNew = dir; dirNew += "\\*.*"; //add "\*.*" to our directory for the first search intptr_t handle; //file's handle _finddata_t findData; //file's structure handle = _findfirst(dirNew.c_str(), &findData); //get file's handle if (-1 == handle) { dirNew = dir; handle = _findfirst(dirNew.c_str(), &findData); if (-1 == handle) { cout << "can not find the file : " << dir << endl; return allPath; } else { allPath.push_back(const_cast<char *>(dir)); return allPath; } } do { if (findData.attrib & _A_SUBDIR) //check of subdirectory { if (strcmp(findData.name, ".") == 0 || strcmp(findData.name, "..") == 0) continue; //add "\" to our directory for next search dirNew = dir; dirNew += "\\"; dirNew += findData.name; vector<char *> tempPath = getFilesList(dirNew.c_str()); //recursive search for subdirectory allPath.insert(allPath.end(), tempPath.begin(), tempPath.end()); //add files in subdirectory to the end of our list } else { string filePath; filePath = dir; filePath += '\\'; filePath += findData.name; char *cString_filePath = new char[filePath.size()]; //change to cString type strcpy_s(cString_filePath, filePath.size() + 1, filePath.c_str()); allPath.push_back(cString_filePath); //add a file to the end of our list } } while (0 == _findnext(handle, &findData)); _findclose(handle); //close handle search return allPath; }
3.統計字符數、行數

bool Statistics_of_File(char *filePath) { ifstream operateFile; operateFile.open(filePath, ios::in); if (!operateFile.is_open()) //file open checking { cout << "can not open this file : " << filePath << endl; return(false); } string getWordL; string getWordN; MyWC *getFirst = NULL, *getSecond = NULL; intptr_t counter = 0; char singleCharact = 0; string singleLine; intptr_t flag = 1; //flag of word group while (!operateFile.eof()) { getline(operateFile, singleLine); textLine++; if (operateFile.eof() == NULL) singleLine += " "; for (size_t i = 0; i < singleLine.size(); i++) { singleCharact = singleLine[i]; if (singleCharact >= 32 && singleCharact <= 126) charactNum++; else singleCharact = ' '; if (isalpha(singleCharact) || (counter >= WORD_LIMIT && isdigit(singleCharact))) { counter++; if (flag) getWordL += singleCharact; else getWordN += singleCharact; } else { if (counter >= WORD_LIMIT) { if (flag) { getFirst = putIntoWordList(getWordL); flag = 0; counter = 0; getWordL = ""; } else { getSecond = putIntoWordList(getWordN); putIntoGroupList(getFirst, getSecond); getFirst = getSecond; counter = 0; getWordN = ""; } } else { counter = 0; getWordL = ""; getWordN = ""; } } } } operateFile.close(); return(true); }
4.統計單詞數,並放進哈希散列表

MyWC *putIntoWordList(string getWord) { size_t offset = 0; string trimmedWord; MyWC *ptrTemp = NULL; offset = (tolower(getWord[0]) - 97) * DIGIT_FIRST; offset += (tolower(getWord[1]) - 97) * DIGIT_SECOND; offset += (tolower(getWord[2]) - 97) * DIGIT_THIRD; offset += tolower(getWord[3]) - 97; wordNum++; for (size_t i = getWord.size() - 1; i >= 0; i--) { if (isalpha(getWord[i])) { trimmedWord = ""; for (size_t j = 0; j <= i; j++) { trimmedWord += tolower(getWord[j]); } break; } } if (ptrOfWord[offset] == NULL) { ptrOfWord[offset] = new MyWC; ptrOfWord[offset]->word = getWord; ptrOfWord[offset]->trimmed = trimmedWord; ptrOfWord[offset]->number = 1; ptrOfWord[offset]->next = NULL; ptrTemp = ptrOfWord[offset]; } else { ptrTemp = ptrOfWord[offset]; while (1) { if (ptrTemp->trimmed == trimmedWord) { if (strcmp(ptrTemp->word.c_str(), getWord.c_str()) == 1) ptrTemp->word = getWord; ptrTemp->number++; break; } if (ptrTemp->next == NULL) { ptrTemp->next = new MyWC; ptrTemp = ptrTemp->next; ptrTemp->word = getWord; ptrTemp->trimmed = trimmedWord; ptrTemp->number = 1; ptrTemp->next = NULL; break; } else { ptrTemp = ptrTemp->next; } } } return ptrTemp; }
5.將詞組放進哈希表

void putIntoGroupList(MyWC *getFirst, MyWC *getSecond) { size_t offset = 0; string wordF, wordS; wordF = getFirst->trimmed; wordS = getSecond->trimmed; MyGC *ptrTemp = NULL; offset = (tolower(getFirst->word[0]) - 97) * DIGIT_FIRST; offset += (tolower(getFirst->word[1]) - 97) * DIGIT_SECOND; offset += (tolower(getSecond->word[0]) - 97) * DIGIT_THIRD; offset += tolower(getSecond->word[1]) - 97; if (!ptrOfGroup[offset]) { ptrOfGroup[offset] = new MyGC; ptrOfGroup[offset]->firstWord = getFirst; ptrOfGroup[offset]->secondWord = getSecond; ptrOfGroup[offset]->number = 1; ptrOfGroup[offset]->next = NULL; ptrTemp = ptrOfGroup[offset]; } else { ptrTemp = ptrOfGroup[offset]; while (1) { if ((ptrTemp->firstWord)->trimmed == wordF && (ptrTemp->secondWord)->trimmed == wordS) { ptrTemp->number++; break; } if (!ptrTemp->next) { ptrTemp->next = new MyGC; ptrTemp = ptrTemp->next; ptrTemp->firstWord = getFirst; ptrTemp->secondWord = getSecond; ptrTemp->number = 1; ptrTemp->next = NULL; break; } else { ptrTemp = ptrTemp->next; } } } }
6.輸出

ofstream outputFile("result.txt"); if (outputFile.is_open()) { outputFile << "characters: " << charactNum << endl; outputFile << "words: " << wordNum << endl; outputFile << "lines: " << textLine << endl << endl; outputFile << "the top ten frequency of word :" << endl; for (size_t i = 0; i < MOST_FREQUENT_NUM; i++) { intptr_t max = 0; string printWord; MyWC *ptrTemp = NULL; MyWC *clear = NULL; for (intptr_t j = 0; j < HASH_CAPACITY; j++) { ptrTemp = ptrOfWord[j]; while (ptrTemp != NULL) { if (ptrTemp->number > max) { max = ptrTemp->number; printWord = ptrTemp->word; clear = ptrTemp; } ptrTemp = ptrTemp->next; } } if (printWord != "") { clear->number = -1; outputFile << printWord << " " << max << endl; } else outputFile << "null" << endl; } outputFile << endl << "the top ten frequency of phrase :" << endl; for (size_t i = 0; i < MOST_FREQUENT_NUM; i++) { intptr_t max = 0; MyWC *printPhraseL = NULL, *printPhraseN = NULL; MyGC *ptrTemp = NULL; MyGC *clear = NULL; for (size_t j = 0; j < HASH_CAPACITY; j++) { ptrTemp = ptrOfGroup[j]; while (ptrTemp != NULL) { if (ptrTemp->number > max) { max = ptrTemp->number; printPhraseL = ptrTemp->firstWord; printPhraseN = ptrTemp->secondWord; clear = ptrTemp; } ptrTemp = ptrTemp->next; } } if (printPhraseL != NULL) { clear->number = -1; outputFile << printPhraseL->word << " " << printPhraseN->word << " " << max << endl; } else outputFile << "null" << endl; } } cout << "success!" << endl;
性能分析
CPU性能及各函數調用時間
熱行分析
可以看到,將單詞、詞組放進對應的哈希表中是最耗費時間的,原因可能是一些單詞/詞組會放進同一拉鏈表,新來的單詞/詞組要和已經存在的詞組進行比較,而比較是一件好費時間的事情。
也就是說,這種哈希函數是不夠好的,不能將相似的單詞分散到不同的槽中,對應的解決方法我想到了如下兩種方式:
(1)改進哈希函數,增加槽的數量,減少沖突發生的幾率;
(2)改用其他的數據結構,比如<key,value>鍵值對。
白盒測試
sample 1:空文件
sample 2:只含一個單詞的文件
sample 3:只含一個換行符的文件
sample 4:含許多類似單詞的文件
sample 5:錯誤文件路徑
sample 6:空文件夾
sample 7:多個文件
sample 8:多個文件夾/子文件夾
sample 9:奇怪的文件
sample 10:助教的測試文件
總結&收獲
這次作業從題目本身來講不是很難,但想寫一個健壯的程序確實不易。
同時,這是我第一次嘗試用C++來寫程序,雖然有一些不規范的地方,但通過上網查資料解決問題,我還是把程序趕出來了。
之前,我一直都是在編譯器上運行,沒有使用過命令行,也很少讀寫文件,這次的作業鍛煉了我在多平台調試以及文件操作的能力。
附:PSP表
Status | Stages | 預估耗時/min | 實際耗時/min |
Accept | 【計划】 Planning | 60 | 80 |
Accept | ——估計時間 Estimate | 60 | 90 |
Accept | 【開發】 Development | 800 | 1200 |
Accept | ——需求分析 Analysis | 10 | 0 |
Accept | ——設計文檔 Design Spec | 40 | 20 |
Accept | ——設計復審 Design Review | 10 | 10 |
Accept | ——代碼規范 Coding Standard | 10 | 10 |
Accept | ——具體設計 Design | 60 | 30 |
Accept | ——具體編碼 Coding | 480 | 600 |
Accept | ——代碼復審 Code Review | 60 | 120 |
Accept | ——測試 Test | 120 | 120 |
Accept | 【記錄用時】 Record Time Spent | 20 | 10 |
Accept | 【測試報告】 Test Report | 60 | 40 |
Accept | 【算工作量】 Size Measurement | 20 | 20 |
Accept | 【總結改進】 Postmortem | 40 | 30 |
Accept | 【合計】 Summary | 1850 | 2380 |