結對第二次—文獻摘要熱詞統計及進階需求
- 課程:軟件工程1916|W(福州大學)
- 作業要求:結對第二次—文獻摘要熱詞統計及進階需求
- 結對人員:221600436-許志瀚 :需求分析、軟件測試、代碼Debug、博客編寫
221600437-張易翔 :需求分析、代碼開發、論文數據爬取、代碼整合 - 作業目標:1、基本需求:實現一個能夠對文本文件中的單詞的詞頻進行統計的控制台程序。 2、進階需求:在基本需求實現的基礎上,編碼實現頂會熱詞統計器。
- Github項目地址: 基礎需求 進階實現
Github代碼簽入記錄
PSP表格
PSP2.1 | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | ||
- Estimate | 估計這個任務需要多少時間 | 1275 | 2145 |
Development | 開發 | ||
- Analysis | 需求分析 (包括學習新技術) | 180 | 210 |
- Design Spec | 生成設計文檔 | 100 | 150 |
- Design Review | 設計復審 | 60 | 40 |
- Coding Standard | 代碼規范 (為目前的開發制定合適的規范) | 30 | 20 |
- Design | 具體設計 | 30 | 40 |
- Coding | 具體編碼 | 600 | 520 |
- Code Review | 代碼復審 | 60 | 70 |
- Test | 測試(自我測試,修改代碼,提交修改) | 120 | 980 |
Reporting | 報告 | ||
- Test Report | 測試報告 | 45 | 60 |
- Size Measurement | 計算工作量 | 20 | 25 |
- Postmortem & Process Improvement Plan | 事后總結, 並提出過程改進計划 | 30 | 30 |
合計 | 1275 | 2145 |
解題思路
需求分析
首先,我們組內對任務需求進行分析,結合之前的代碼經驗,將問題划分為兩大步驟。第一是對文件進行讀取,獲得文件內容。第二是對讀取到文件內容進行分析及統計。於是創建出Main、Process、Result三個類,作用分別Main是程序入口、Process對讀取到的文件內容進行分析統計、Result做為結構記錄相應字符數、行數和單詞及其頻數。然后在Process類中統計字符數用String類的Length()算出字符數並記錄,統計行數時通過按行讀取文件計算出行數並記錄,統計單詞時依靠正則表達式匹配單詞、統計頻次並記錄到Map結構中。並作出相應的類圖使需求在類圖都有對應的方法得到實現
最后設計比較刁鑽的測試數據測試代碼正確性、並且在代碼中用時相對較長的部分進行力所能及的代碼優化。
設計實現過程
類圖展示
算法關鍵及流程圖
-字符、行數、單詞統計
程序性能改進及思路
基礎及進階需求均使用工具JProfiler監控處理隨機生成的約700m大小文件分析內存,處理器占用。
通用優化部分
在此次程序編寫的過程中,主要處理程序迭代了兩次.在第一個原型程序時因為采用了按行讀取再按字處理的方式導致效率較為低下,一個字符通常要用幾個IF語句判斷是否符合需求的要求,再加上不利於后續功能擴展便將其重新編寫,采用正則表達式按行處理文件.
不論在基礎還是進階功能的編寫中發現程序運行時要反復調用幾個方法,而在方法中要不斷創建數個Pattern類來構建正則表達式,因此在優化的過程中首先考慮將方法設置為靜態類型,並將幾個Pattern類的正則表達式在Process類構造的時候便進行編譯,提高效率.
測試效果采用一個21M大小左右文件進行
基礎未優化前
基礎優化后:
進階需求采用一個約22M大小文件測試
優化前:
優化后
有一定效果但並不是十分明顯
基礎需求部分改進思路
各方法耗時:
可以發現在哈希表處理的時候耗時占比較多.在程序中為了節約時間我們便直接使用JAVA提供的map結構儲存單詞,數量的鍵值對,但是JAVA本身提供的方法不能很好的滿足我們的需求比如說單詞數量排序的問題,目前解決方案是將MAP導出為一個list表,對此進行排序.在我們規划中是采用一個類似於treemap的順序結構來進行有序插入,在節約排序時間的同時節約大量為了排序重復儲存的內存空間.但是自定義結構需要花費大量時間測試可靠性,並且完成此類型超大文件所需的時間在一分鍾左右,不算太過離譜,於是便計划在完成所有任務后最后進行.
此外由於前期分析得當,提前采用了Bufferedreader以帶緩沖的方式讀取文件,節約了大量的IO時間,使得即便在機械硬盤上文件讀取部分花費時間占比不到10%,沒有進一步優化的必要.
進階需求部分改進思路
各方法耗時:
從圖中可以得出在使用正則表達式完成需求的耗時占比較多,但考慮到需求並未明確提及只處理特定格式的文件,不能使用取巧辦法忽略需求中提及的無關信息,此外考慮到主要處理文件需求為CVPR論文信息,在此類文件通常不大的情況下考慮到緊迫的時間安排不再進一步優化.
爬蟲部分
爬蟲部分程序主要由兩部分組成,首先爬取位於 http://openaccess.thecvf.com/CVPR2018.py 的論文鏈接列表,將其中的論文URL添加到帶爬取URL表中,再從URL列表取出URL不斷爬取論文信息
最開始的原型實現使用單線程進行,由於該網站服務器位於境外,再加上帶爬取URL有千條之多,因此速度十分緩慢.
采用JProfiler分析時間占用,經過團隊團隊討論后發現,第二部分為大量獨立任務構成,十分適合進行多線程優化,並且因為各個結果之間相互獨立,不需要太多的多線程同步處理,還利於程序編寫.因此我們首先對此進行了優化.
優化前:
大約花費293秒
優化后:
大約花費25秒,效率提升十分明顯
在此基礎上,我們進一步分析發現因為受制於該網站服務器位於境外的網絡原因,每個線程進行網絡通信的時候等待時間較久且容易出錯,因此進一步的優化考慮為采用位於境外代理池替換本機進行信息爬取,但受制於成本以及時間並未付諸實施.
關鍵代碼展示
基礎部分封裝的3個API函數
public int getWord_count() { return result.getWord_count(); }
public int getChar_count() { return result.getChar_count(); }
public int getLine_count() { return result.getLine_count(); }
基礎部分統計單詞函數
private static void process_line_withRegularExpression(String str, Result resultClass){
Matcher m=p.matcher(str);
while(m.find()) {
//System.out.println(m.group(2));
resultClass.addWord(m.group(2));
}
}
基礎部分統計字符數代碼
while ((s = bufferedReader.readLine()) != null) {
i++;//行計數
resultClass.char_count_plus(s.length());//統計字符數
s = s.replaceAll("[^\\x00-\\x80]", "");
s=trim(s);//去掉開頭不顯示字符
if(s.length()!=0){//若有可顯示字符則處理
resultClass.line_count_plus();//統計行數
process_line_withRegularExpression(s,resultClass);
}
}
基礎部分單詞數量統計匯總函數
public void addWord(String word) {
String tempWord = word.toLowerCase();
if (resultMap.containsKey(tempWord)) {//resultMap使用HashMap結構
resultMap.put(tempWord, resultMap.get(tempWord) + 1);
word_count_plus();
} else {
resultMap.put(tempWord, 1);
word_count_plus();
}
}
通用單詞數量排序函數
public List<Map.Entry<String, Integer>> sort() {
//從HashMap恢復entry集合
//從resultMap.entrySet()創建LinkedList。我們將排序這個鏈表來解決順序問題。
//我們之所以要使用鏈表來實現這個目的,是因為在鏈表中插入元素比數組列表更快。
List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(resultMap.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
//通過傳遞鏈表和自定義比較器來使用Collections.sort()方法排序鏈表。
//降序排序
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if(o2.getValue()==o1.getValue()) return o1.getKey().compareTo(o2.getKey());
return o2.getValue().compareTo(o1.getValue());
//使用自定義比較器,基於entry的值(Entry.getValue()),來排序鏈表。
}
});
進階部分短語基礎處理函數
private static void process_line_withPhrase(String str, Result resultClass, int number,int weight) {
Matcher m = phrasePattern.matcher(str);
PhraseFactory phraseFactory = new PhraseFactory(number);//短語構造類
PhraseBorder phraseBorder;//result
while (m.find()) {
if (m.start() == 0 || !Character.isDigit(str.charAt(m.start() - 1))){
//判斷單詞開頭是否為數字
resultClass.word_count_plus();
if ((phraseBorder = phraseFactory.storeBorder(m.start(), m.end())) != null) {
//儲存單詞位置
int newEnd = trimTail(str, phraseBorder.start, phraseBorder.end);
//System.out.println(str.substring(phraseBorder.start, newEnd));
resultClass.addPhrase(str.substring(phraseBorder.start, newEnd), weight);
//添加短語至結果集
}
}
}
}
短語構造函數
public PhraseBorder storeBorder(int start, int end) {//添加新的單詞位置
if (!isFull()) {
if (isFormerEnd(start)) {
pool[iterator].start = start;
pool[iterator].end = end;
addIterator();
} else {
clear();
pool[iterator].start = start;
pool[iterator].end = end;
addIterator();
}
} else {
if (isFormerEnd(start)) {
pool[iterator].start = start;
pool[iterator].end = end;
addIterator();
int outputStart = pool[startPoint].start;
addStartPoint();//后移開頭指針等於輸出
return new PhraseBorder(outputStart, end);
} else {
clear();
pool[iterator].start = start;
pool[iterator].end = end;
addIterator();
}
}
return null;
}
爬蟲代碼
public int initialization(String URL) {//獲得待爬取URL列表
try {
Document document = Jsoup.connect(URL).timeout(1000 * 60).maxBodySize(0).get();//
Elements links = document.getElementsByTag("a");
int i = 0;
for (Element link : links) {
String href = link.attr("href");
if (href.contains("content_cvpr_2018/html/")) {
URLs[i] = "http://openaccess.thecvf.com/" + href;
System.out.format("%d:%s\n", i, URLs[i]);
++i;
}
}
return URL_size = i;
} catch (IOException e) {
e.printStackTrace();
}
return -1;
}
public void workMethod() {//爬取管理方法
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < URL_size; i++) {
cachedThreadPool.execute(() -> getContent(getURL()));
}
try {
bufferedWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
cachedThreadPool.shutdown();
}
public void getContent(String URL) {//獲得內容方法
try {
Document document = Jsoup.connect(URL).timeout(1000 * 600).maxBodySize(0).get();//
Elements links = document.getElementsByTag("div");
String content = null, title = null;
for (Element link : links) {
switch (link.id()) {//case "authors"://and time
case "papertitle":
title = link.text();
break;
case "abstract":
content = link.text();
break;
}
}
synchronized (this) {
System.out.println(outputNumber);
System.out.println();
bufferedWriter.write(String.valueOf(outputNumber));
bufferedWriter.newLine();
System.out.format("Title: %s\n", title);
bufferedWriter.write(String.format("Title: %s\n", title));
System.out.format("Abstract: %s\n", content);
bufferedWriter.write(String.format("Abstract: %s\n", content));
bufferedWriter.newLine();
bufferedWriter.newLine();
outputNumber++;
}
System.out.println();
} catch (IOException e) {
e.printStackTrace();
}
}
public synchronized String getURL() {//獲得待爬取URL
if (currentURL < URL_size) return URLs[currentURL++];
else return null;
}
爬蟲結果展示:
單元測試
基礎測試部分使用C++語言編寫了一個隨機生成字符的程序,隨機生成行數、字符串長度,然后寫入到input.txt文件中。在生成字符串的同時,記錄生成字符數,行數以及定義上的空行數。然后將生成的input.txt文件做為WordCount程序的測試文件,然后在Eclipse中使用Junit進行單元測試。
#include<cstdio>
#include<time.h>
#include<stdlib.h>
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
srand(time(0)); //產生隨機化種子
int count = 0;
int count1 = 0;
int n = rand() % 1000+99;
//在1000-99的范圍內隨機產生字符串個數
FILE *fp = fopen("input.txt","w");
int temp=n;
while (temp--) //依次產生n個字符串
{
int k = rand() % 150 + 0; //隨機生成一個字符串的長度
if(k==0){
count1++;
}
count+=k;
for (int i = 0; i < k; i++)
{
int x;
x = rand() % (127 - 32) + 32;
fprintf(fp, "%c", x); //將x轉換為字符輸出
}
fprintf(fp, "\n");
}
printf("line: %d", n );
printf("空白line: %d", count1 );
printf("\n character: %d\n",count);
return 0;
}
Junit單元測試截圖
進階需求采用官網論文文件測試
注:官網爬取結果存在非ASCII字符且位於兩個合法單詞之間的情況, 本次程序對於此類情況按兩個單詞處理
若按一個單詞計算此類情況單詞數量應少掉10個左右
結果
測試數據構造思路
-統計行數
構造思路:1.中間增加空白行
2.單行放入定義空白符
-統計單詞
構造思路:1.多種不合法單詞,如1aaaa、the、abc123a等
2.分割符分割單詞,如task-masn、mesk--a123等
-統計詞組
構造思路:1.單詞間添加非法單詞,如aaaa in bbbb等
2.單詞間添加分割符,如aaaa(%bbbb等
-極端情況處理
構造思路:生成大型文本文件(738m)測試程序穩定性
部分測試數據概覽:
character: 37854 line: 500 無效空白line: 3
character: 75998 line: 1016 無效空白line: 6
遇到的困難及隊友互評
遇到的困難及解決
在這次結對任務中遇到的困難是比較多的,首先在閱讀任務、分析需求的時候就出現了相當多的分歧,分隔符划分、詞組匹配等。相信這也不只是我們遇上的困難,從微信群里就可以看出,這一點可以說是全班同學的共同問題。這一部分的解決方法就是討論,不止結對兩人間討論,群內討論、與其他小組進行討論並且交換測試數據互相糾錯。其次是爬蟲部分,因為之前沒有接觸過,所以是現學的技巧。因為時間限制,所以這一部分還是有點難度的。最后是代碼Debug,因為在需求分析時的出現的問題,在后面代碼Debug的時候花費了相當多的時間,在給出的PSP表中可以看出,原本預計120分鍾要完成的測試(自我測試,修改代碼,提交修改)項中,實際花費時間拉長到了980分鍾。
最后此次作業本來在規划時划撥了完整的一天時間進行代碼的優化,但是需求一直不斷在變化,每天在完成新功能的同時還要對之前完成的部分進行修改以適應新的需求.而且由於許多需求此前定義不明確導致我們在需求分析時考慮的情況不夠貼近實際,在編碼時發現此前的需求分析及功能設計不能滿足需要,需要重新構建,並且代碼也得推倒重寫,浪費了大量時間
而且由於需求定義不明,導致我們對部分情況的處理結果進行了討論並定義當前程序存在BUG,但是等到我們花費大量時間查找文件內容並定位到具體導致BUG原因,隨后又花費一定時間進行修復,擠占了本就為數不多的代碼測試時間,結果一覺醒來發現做了無用功?
隊友互評
在這次的結對任務中,我的隊友起到了一個中流砥柱的作用。他不僅有很強的代碼開發能力,編寫的代碼思路清晰,對我們后面的代碼測試、修改有很大幫助。而且在溝通交流中可以清晰的表達自己的想法思路,同時又能快速理解我所表達的思路觀點,因此在溝通交流上可以說是配合的相當愉快。除此之外,他還擁有很強的自學能力,能夠在短時間內學習爬蟲技術,並編寫爬蟲程序實現對給定網站論文信息的爬取。最后,在后期的代碼測試、Debug部分,會廢寢忘食的修改錯誤代碼。針對代碼優化部分,他總是能想出優秀的方案來實現對代碼的優化,提高程序的效率。可以說是有能力還肝,我要向他看齊。