個人作業——統計多個文本文件中的單詞及詞組出現頻率


作業要求

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-Za-z

字母數字符號:A-Za-z0-9

分割符:空格,非字母數字符號

例如:”file123”是一個單詞,”123file”不是一個單詞。fileFileFILE是同一個單詞

如果兩個單詞只有最后的數字結尾不同,則認為是同一個單詞,例如,windowswindows95windows7是同一個單詞,iPhone4IPhone5是同一個單詞,但是,windowswindows32a是不同的單詞,因為他們不是僅有數字結尾不同

輸出按字典順序,例如,windows95windows98windows2000同時出現時,輸出windows2000

詞組的定義:windows95 goodwindows2000 good123,可以算是同一種詞組按照詞典順序輸出。

c) 輸入文件名以命令行參數傳入

d) 輸出文件result.txt

characters: number

words: number

lines: number

<word>: number

<word>為文件中真實出現的單詞大小寫格式,例如,如果文件中只出現了Filefile,程序不應當輸出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;
View Code

 

 

代碼實現

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]);
View Code

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;
}
View Code

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);
}
View Code

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;
}
View Code

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;
            }
        }
    }
}
View Code

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;
View Code

 

 

性能分析

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


免責聲明!

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



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