1. 程式人生 > >2018軟工實踐第三次作業——結對作業2

2018軟工實踐第三次作業——結對作業2

發揮 prev 不能 correct 圖片 rip itl prime tab

結對成員:031602631 蘇韞月、031602147 鄭愈明
博客鏈接:

  • 結對同學的博客鏈接
  • 本作業博客的鏈接

Github項目地址
附加題相關文件提取碼:jbj4
具體分工

  • 蘇韞月:爬蟲實現,論文列表爬取實現、附加題功能實現、WordCount基本函數功能實現
  • 鄭愈明:命令行參數功能實現、詞組統計功能實現、單元測試、性能分析

代碼規範

  • 命名規範
    • 變量——Camel形式;所有類型/類/函數名——Pascal形式。
    • 類和函數名應根據各自的功能命名,要求一眼就知道是做什麽的
  • 註釋規範
說明性文件頭部註釋
註釋必須列出:內容、功能,頭文件的註釋中還應有函數功能簡要說明。
/**
 * File name:      // 文件名
 * Description:    // 說明此程序文件完成的主要功能
 * Function List:  // 主要函數列表,每條記錄應包括函數名及功能簡要說明
 1.   ....
 */
源文件頭部註釋
源文件頭部列出:模塊功能、主要函數及功能。
/**
 * FileName: test.cpp
 * Description:     // 模塊描述
 * Function List:   // 主要函數及其功能
    1. -------
 */ 
  • 書寫風格規範
    • 一行代碼的長度不要超過VS2017的屏幕可顯示範圍(110個字符)
    • 縮進采用VS2017默認格式
    • 凡是用到{}的地方,要使“{”、“}”獨占一行
    • 不允許多條語句在同一行
    • 有疑問的代碼、測試用的代碼段、函數之間、或者什麽其他的地方,用註釋/**/或//做出分割線,標註清楚功能、疑問、以後想要擴展的內容等等
    • 短註釋可以跟在一行代碼後面,長的註釋比如功能說明,要在函數前獨占一塊
    • 不同的函數之間要有空行分割,函數內部功能相差較大的也要用空行分割
  • 錯誤處理
    • 文件處理要做好基本的排錯處理

一、PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鐘) 實際耗時(分鐘)
Planning 計劃 20 10
? Estimate ? 估計這個任務需要多少時間 20 10
Development 開發 1340 1790
? Analysis ? 需求分析 (包括學習新技術) 180 360
? Design Spec ? 生成設計文檔 20 20
? Design Review ? 設計復審 10 60
? Coding Standard ? 代碼規範 (為目前的開發制定合適的規範) 30 30
? Design ? 具體設計 60 60
? Coding ? 具體編碼 800 300
? Code Review ? 代碼復審 60 480
? Test ? 測試(自我測試,修改代碼,提交修改) 200 480
Reporting 報告 85 370
? Test Repor ? 測試報告 30 320
? Size Measurement ? 計算工作量 10 20
? Postmortem & Process Improvement Plan ? 事後總結, 並提出過程改進計劃 45 30
合計 1445 2170

二、需求分析

1.爬取論文信息

CVPR2018官網爬取今年的論文列表,輸出到result.txt,內容包含論文題目、摘要,格式如下:

  • 為爬取的論文從0開始編號,編號單獨一行
  • 兩篇論文間以2個空行分隔
  • 在每行開頭插入“Title: ”、“Abstract: ”(英文冒號,後有一個空格)說明接下來的內容是論文題目,或者論文摘要
  • 後續所有字符、單詞、有效行、詞頻統計中,論文編號及其緊跟著的換行符、分隔論文的兩個換行符、“Title: ”、“Abstract: ”(英文冒號,後有一個空格)均不納入考慮範圍
2.命令行參數進階設定
  • -i 參數設定讀入文件的存儲路徑
  • -o 參數設定生成文件的存儲路徑
  • -w 參數設定是否采用不同權重計數
    • 與數字 0|1搭配使用,0 表示屬於 Title、Abstract 的單詞權重相同均為 1 ;1 表示屬於 Title 的單詞權重為10,屬於Abstract 單詞權重為1。
  • -m 參數設定統計的詞組長度
    • -m 參數與數字配套使用,用於設置詞組長度
  • -n 參數設定自定義詞頻統計輸出的單詞數量
    • -n 參數與數字搭配使用,用於限制最終輸出的單詞(詞組)的個數,表示輸出頻數最多的前[number]個單詞(詞組)
3. 多參數的混合使用
  • 實際測試時,在一句命令行語句中
    • -i 、-o 、-w 參數一定會出現
    • -m、-n參數可能都不出現,可能只出現一個,也可能都出現
    • 未出現-m參數時,不啟用詞組詞頻統計功能,默認對單詞進行詞頻統計
    • 未出現-n參數時,不啟用自定義詞頻統計輸出功能,默認輸出10個
  • 參數之間的順序並不固定
4. 附加題

在博客中詳細描述,並在博客中附件(.exe及.txt)為證。附加功能的加入不能影響上述基礎功能的測試,分數取決於創意和所展示的完成度。
要求:發揮個人的奇思妙想,對論文列表進行更多的挖掘並進行數據分析,為你們舉幾個栗子:

  • 從網站綜合爬取論文的除題目、摘要外其他信息,如:論文類型、作者、作者單位等等
  • 分析論文作者的所屬地,哪些國家、哪些高校發表的論文比較多
  • 分析論文列表中各位作者之間的關系,論文A的第一作者可能同時是論文B的第二作者,不同論文多位作者之間可能存在著聯系
  • 對數據的圖形可視化做出一些努力,比如對上一條功能可以形成關系圖譜

5.一些說明

以下為助教的補充說明:

技術分享圖片

三、解題思路描述與設計實現說明

1. 爬蟲使用

本次爬取CVPR論文title和abstract的爬蟲代碼使用的是C++實現。

具體的流程圖如下:
技術分享圖片

部分代碼展示:

/*******************解析論文的HTML*********************/
bool Extract(int count, char * &response)
{
    string httpResponse = response;
        stringstream ss;
        ss << count;
        string paper = ss.str() + "\r\n";
        const char *p = httpResponse.c_str();
        /****************************提取title*********************************/
        char t[] = "papertitle\">\n";
        char *tag = t;
        const char *pos = strstr(p, tag);
        pos += strlen(tag);
        const char * nextQ = strstr(pos, "<");
        if (nextQ)
        {
            char * title = new char[nextQ - pos + 1];
            sscanf(pos, "%[^<]", title);
            paper += "Title: ";
            paper += title;
            paper += "\r\n";
            delete[] title;
        }
        /****************************提取abstract*********************************/
        char t2[] = "abstract\" >\n";
        char *tag2 = t2;
        const char *pos2 = strstr(p, tag2);
        pos2 += strlen(tag2);
        const char * nextQ2 = strstr(pos2, "<");
        if (nextQ2)
        {
            char * abstract = new char[nextQ2 - pos2 + 1];
            sscanf(pos2, "%[^<]", abstract);
            paper += "Abstract: ";
            paper += abstract;
            paper += "\r\n";
            delete[] abstract;
        }
    ... ...
  
   
/*******************解析助教提供的頁面的論文的URL*********************/
void ParsepaperUrl(char * &response)
{
    int bytes;
    int count = 0;
    string httpResponse = response;  
    const char *p = httpResponse.c_str();  
    char t[] = "href=\"";
    char *tag = t;
    const char *pos = strstr(p, tag);
    while (pos)
    {
        pos += strlen(tag);
        const char * nextQ = strstr(pos, "\"");
        if (nextQ) {
            char * url = new char[nextQ - pos + 1];
            sscanf(pos, "%[^\"]", url);
            if (strstr(url, "html")) 
            {
                string paperUrl = "http://openaccess.thecvf.com/";
                paperUrl += url;
                if (strstr(url, "\r\n1000\r\n"))
                {
                    int pos = 0;
                    while ((pos = paperUrl.find("\r\n1000\r\n")) != -1)
                    {
                        paperUrl.erase(pos, strlen("\r\n1000\r\n"));
                    }
                }
    ... ...
2. 代碼組織與內部實現設計(類圖)

技術分享圖片

3. 說明算法的關鍵與關鍵實現部分流程圖
  • 詞組統計模塊——對文件數據的處理

技術分享圖片

  • 詞組統計模塊——詞組判定及統計

技術分享圖片

四、附加題設計與展示

我們參考助教在附加題位置列出的文檔格式,對數據進行進一步的爬取。
我們起先采用的也是C++ 實現,先將author和pdf_link的內容爬取下來。但是在助教提供的網址裏根本找不到有關type的信息,因此我們歷盡千辛萬苦,終於在CVPR 2018 裏找到了type。本來以為用C++的代碼爬取就可以了,沒想到這個網站是動態生成的,無奈我們才疏學淺,只好轉為求助python的selenium。
我們發現這個網站的論文順序和助教提供的那個網站的論文順序一致,但是又缺少abstract等信息,因此“偷工減料”直接按順序爬取type的信息,最後再用together.cpp將兩份文檔的內容整合在一起。

  • python部分的示例代碼如下:
Oral=re.compile(r'O\d')
Spotlight=re.compile(r'S\d')
Poster=re.compile(r'P')

f = open('./Only_Type.txt', 'a',encoding='utf-8')

for t in soup.find_all('a',class_='text-bold ng-binding'):
     k=t.parent.parent.find('h3').text
     if re.search('O',k):
         f.write('Oral\n')
     elif re.search('S\d',k):
         f.write('Spotlight\n')
     elif re.search('P',k):
         f.write('Poster\n')

f.close()
  • 爬取結果整合:

技術分享圖片

2.

此外,在CVPR 2018裏,我們還發現每個作者後面都被標註上了所屬的大學或研究機構,有的還有國家信息。因此我們同樣使用python爬取這部分信息,保存在文檔裏面。

  • 爬取結果:
    技術分享圖片

然後將大學或研究機構都提取出來進行排序,輸出top10這樣,實現上是以WordCount.cppSword.cpp為基礎進行一些修改。

  • 輸出結果:
    技術分享圖片

  • python部分的示例代碼如下:

 f = open('./Authors.txt', 'a',encoding='utf-8')

 for author in soup.find_all('em', class_='ng-binding'):
     f.write(author.text)

 f.close()
3.

我們利用Crawler.cpp將2013~2018年的論文的title和abstract全爬下來,放在result20xx.txt文件裏,並用WordCount.cpp對每年的爬取結果進行處理,分別選用的詞組長度為2~5生成Top10的高頻詞組,然後選擇其中靠譜的Top5,利用Top.pyTrend.py生成可視化數據。

部分結果如下所示:
技術分享圖片

技術分享圖片

五、關鍵代碼解釋

以下對詞組統計phrase.cpp功能模塊進行解釋:

Psort::PS()

1.結構體PWord:
  • 思路:

    用於存儲合法單詞和合題意的分隔符,f用於記錄區分合法單詞、分隔符串以及間斷符。

  • 代碼展示如下:

struct  PWord {
  string pw;
  short int f;      //Is letter:1;Is separator:0 ; Is break point:-1; 
};
2.文件數據處理:
  • 思路:

    對每一個字符進行處理,區分開合法單詞、分隔符串與間斷符(即非法單詞),將其按序分別插入Title_Phrase隊列與Abstract_Phrase隊列。

  • 需要註意的問題:

    論文標準格式添加的title與abstract的需要詞組分別統計,同時title與abstract不計入統計。

  • 解決方案:

    flag標誌單詞出現在title段orabstract段;matchflag標誌title與abstract的匹配(類似括號匹配)。

  • 代碼展示如下:
  while (fin.good())
  {
      fin >> c;
      if ((c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122))
      {
          if (c >= 65 && c <= 90)
              c += 32;
          word.pw.push_back(c);

          //Judge separator, and join into phrase queue
          if (wordflag == 1 && separator.pw.length()>0)
          {
              if (flag == 0)
                  phrase_t.push(separator);
              else if (flag == 1)
                  phrase_a.push(separator);
          }
          separator.pw = "";
      }
      else
      {
          if (c != 13 && c != 10)
          {
              separator.pw.push_back(c);
          }

          //Judge word, and join into word queue
          if (word.pw.length() >= 4)
          {
              /**********************Is word******************************/
              if ((word.pw[0] >= 97 && word.pw[0] <= 122) && (word.pw[1] >= 97 && word.pw[1] <= 122) && (word.pw[2] >= 97 && word.pw[2] <= 122) && (word.pw[3] >= 97 && word.pw[3] <= 122))
              {
                  if (word.pw == "title")
                  {
                      matchflag = 1;
                      wordflag = 0;
                      flag = 0;
                      phrase_a.push(f);
                  }
                  else if (word.pw == "abstract")
                  {
                      if (matchflag)         //Is 'Abstract' 
                      {
                          matchflag = 0;
                          wordflag = 0;
                          flag = 1;
                          phrase_t.push(f);
                      }
                      else                   //Is 'abstract'
                      {
                          wordflag = 1;
                          if (flag == 0)
                              phrase_t.push(word);
                          else if (flag == 1)
                              phrase_a.push(word);
                      }
                  }
                  else
                  {
                      wordflag = 1;
                      if (flag == 0)
                          phrase_t.push(word);
                      else if (flag == 1)
                          phrase_a.push(word);
                  }
              }
              else
              {
                  if (flag == 0)
                      phrase_t.push(f);
                  else if (flag == 1)
                      phrase_a.push(f);
                  wordflag = 0;
              }
          }
          else if (word.pw.length() == 0)
          {

          }
          else
          {
              if (flag == 0)
                  phrase_t.push(f);
              else if (flag == 1)
                  phrase_a.push(f);
              wordflag = 0;
          }
          word.pw = "";
      }
  }
3.詞組字典的構建:
  • 思路:

    分別對Title_Phrase隊列與Abstract_Phrase隊列進行詞組檢索,符合要求的詞組序列便加入map——phrase_count中,便於之後的排序操作。以Title_Phrase隊列處理為例,Abstract_Phrase隊列同理。

  • 代碼展示如下:

map初始化:

//*****************For  phrase************************//
  std::map<std::string, size_t> phrase_count;
  string P;
  P = "";      //Story phrase temporary

詞組構建:

(1)start==0:初始構建詞組或檢索到間斷符重新構建詞組,為方便後續構建詞組,構建成功時同步記錄詞組中第二個單詞首字母的位置、P記錄最後加入字典的詞組字符串。

(2)start==1:至少為第二次構建詞組,此時只需將P中的第一個單詞及前兩個單詞之間的分隔符(如果存在)去掉,並讀取下一個單詞加入P即可。

(3)重復以上步驟,直到Title_Phrase隊列為空。

  while (!phrase_t.empty())
  {

      if (start == 1)
      {
          if (!flags)
          {
              P = P.substr(p);           //Delete the first word
          }
          PWord sign = phrase_t.front();
          phrase_t.pop();
          if (sign.f == -1)              //break point 
          {
              start = 0;
              continue;
          }
          else if (sign.f == 1)          //word component with letter 
          {
              if ((P[P.size()] >= 48 && P[P.size()] <= 57) || (P[P.size()] >= 65 && P[P.size()] <= 90) || (P[P.size()] >= 97 && P[P.size()] <= 122))
              {
                  P.append(" " + sign.pw);
              } 
              else                       //privious is separator or without word component with letter
              {
                  P.append(sign.pw);
              }
              bool flagi = 1;            //Flag the second word witch have not be marked yet

              for (int i = 0; i < P.size(); i++){???}//seach for the second word's position
              if (weight == 1)
                {
                    phrase_count[P] += 10;
                }
                else
                    ++phrase_count[P];
              flags = 0;
          }
          else                          //(sign.flag==0)//separator 
          {
              flags = 1;
              P.append(sign.pw);
          }
      }
      else                              //Constitute in the first time or once again
      {
          P = "";
          short int l = len;
          short int flagl = 0;          //Is word component by letter or not
          flag = 1;                     //Use the "flag" previous to Judge the length of phrase is enough or not
          while (l)
          {
              PWord sign = phrase_t.front();
              phrase_t.pop();
              if (sign.f == -1)          //separator
              {
                  flag = 0;
                  flagl = 0;
                  break;
              }
              else if (sign.f == 1)      //Is word component by letter 
              {
                  if (flagl)
                  {
                      P.append(" " + sign.pw);
                  }
                  else                   //privious is separator or without word component with letter 
                  {
                      flagl = 1;
                      P.append(sign.pw);
                  }
                  l--;
              }
              else //(sign.flag==0)      //The string with separator 
              {
                  if (flagl)             //Is word component by letter
                  {
                      flagl = 0;
                      P.append(sign.pw);
                  }
              }
          }
          if (flag)
          {
              if (weight == 1)
                {
                    phrase_count[P] += 10;
                }
                else
                    ++phrase_count[P];
              start = 1;
              bool flagi = 1;             //Flag the second word witch have not be marked yet
              for (int i = 0; i < P.size(); i++){???}//seach for the second word's position
          }
      }
  }

詞組排序:

利用sort函數進行排序。

  //*********Sort**********//
  vector<PAIR>phrase_v1(phrase_count.begin(), phrase_count.end());
  sort(phrase_v1.begin(), phrase_v1.end(), CmpByValue());
  phrase_v = phrase_v1;

六、性能分析與改進

輸入文件為爬取得到的標準格式文檔,大小為1195KB,設定參數:-w 1 -m 3 -n 20
運行得到如下結果:

1.性能分析圖

技術分享圖片

2.程序中消耗最大的函數

技術分享圖片

3.改進的思路

一開始使用的是從map中搜索number次頻數最高的單詞及詞組進行詞頻輸出;後來改為直接使用sort()函數,程序運行時間從25.233秒減少為23.132秒(提高了8%的運行速度)。

4.改進後的性能分析圖

技術分享圖片

七、單元測試

1.設計的十個單元測試如下:
單元測試名稱 測試內容 被測試實例
NullFile 打開內容為空的文件 測試全部
Countcorrect_l 正確統計行數 Clines.cpp
Countcorrect_c 正確統計字符 Ccharacter.cpp
CountWordscorrect 能正確正確統計單詞數 Cwords.cpp
WordFrequency 僅包含同一個有效單詞,但存在多個大小寫混用的版本,權重選項為0 Swords.cpp
WordFrequency 僅包含同一個有效單詞,但存在多個大小寫混用的版本,權重選項為1 Swords.cpp
WordFrequency 包含多個有效單詞,權重選項為0 Swords.cpp
WordFrequency 包含多個有效單詞,權重選項為1 Swords.cpp
CountPhrase 正確統計大小寫英文單詞的詞頻,只含有合法單詞 Phrase.cpp
CountPhrase 正確統計大小寫英文單詞的詞頻,含有合法單詞、非法單詞及分隔符 Phrase.cpp
2.單元測試結果

技術分享圖片

3.項目部分單元測試代碼
namespace WordFrequency   //正確統計大小寫英文單詞的詞頻
{
    TEST_CLASS(UnitTest1)
    {
    public:

        TEST_METHOD(TestMethod1)
        {
            // TODO: 在此輸入測試代碼
            Wsort s("CountFrequency_1.txt", "OutFrequency_1.txt", 0, 1);  //僅包含同一個有效單詞,但存在多個大小寫混用的版本,權重選項為0,統計最高詞頻
            s.CS();
            int s_result = 11;
            Assert::AreEqual(s.Get(), s_result);
        }
        TEST_METHOD(TestMethod2)
        {
            // TODO: 在此輸入測試代碼
            Wsort s("CountFrequency_1.txt", "OutFrequency_1.txt", 1, 1);  //僅包含同一個有效單詞,但存在多個大小寫混用的版本,權重選項為1,統計最高詞頻
            s.CS();
            int s_result = 20;
            Assert::AreEqual(s.Get(), s_result);
        }
        TEST_METHOD(TestMethod3)
        {

            // TODO: 在此輸入測試代碼
            Wsort s("CountFrequency_3.txt", "OutFrequency_3.txt", 0, 1);  //包含多個有效單詞,權重選項為0,統計最高詞頻
            s.CS();
            int s_result = 3;
            Assert::AreEqual(s.Get(), s_result);
        }
        TEST_METHOD(TestMethod4)
        {

            // TODO: 在此輸入測試代碼
            Wsort s("CountFrequency_3.txt", "OutFrequency_3.txt", 1, 1);  //包含多個有效單詞,權重選項為1,統計最高詞頻
            s.CS();
            int s_result = 21;
            Assert::AreEqual(s.Get(), s_result);
        }
    };
}
4.代碼覆蓋率

未覆蓋到的代碼基本都是異常錯誤處理的代碼

技術分享圖片

八、Github的代碼簽入記錄

技術分享圖片

技術分享圖片

九、遇到的代碼模塊異常或結對困難及解決方法

1.詞組統計模塊的設計

做需求分析時,一開始作業中對於詞組統計部分描述的不夠清晰,我們的理解也不到位,因此最開始對此模塊的設計與實際要求距離較遠;在經過助教的詳細說明後,我們重新做了需求分析,修改模塊代碼,成功解決問題。

2.數據爬取

在做數據爬取時,發現CVPR官網爬取到的論文的url會被隨機插入數字1000作為幹擾項,導致異常,後來對爬取的url進行裁切解決問題。在用C++爬蟲對附加題的信息進行爬取時,得到400錯誤,但網頁可正常打開,分析後判斷是動態生成的網頁,通過python解決。

十、互吹彩虹屁時間

隊友相當好,特別積極,我跟她說:你負責一下這塊內容吧,經常很爽快地答應了。
我們之間交流也很頻繁,有問題就討論,反正不會玩消失就是了。
而且也不會抱怨這抱怨那的,特別特別好的合作夥伴!
我這個人算是比較粗心又沒什麽耐心的,差不多4號的時候我就不想再做這個作業了,那個時候我們的工作也算是做完了(我覺得)
但是國慶回來,她就說還要再改進WordCount,然後還真的又發現了Bug
我就覺得吧,細心有耐心的人真的好可怕。。。
怎麽說,有這樣一個人來做隊友,很靠譜!

學習進度條

第N周 新增代碼(行) 累計代碼(行) 本周學習耗時(小時) 累計學習耗時(小時) 重要成長
1 900 900 40 40 復習了C++ Primer Plus,學習單元測試等代碼分析方法
2 0 900 6 46 學習《構建之法》3~8章,學習使用Axure、Min構建原型和導圖
3 1000 1900 30 76 學習C++實現爬蟲,復習python實現爬蟲
4 0 1900 40 116 學習itchat等語聊機器人API的使用

2018軟工實踐第三次作業——結對作業2