作業格式
這個作業屬于哪個課程 | 軟體工程1916-W(福州大學) |
---|---|
這個作業要求在哪裡 | 結對第二次—文獻摘要熱詞統計及進階需求 |
結對學号 | 221600414、221600417 |
Github項目位址 | PairProject1-Java 、PairProject2-Java |
這個作業的目标 | 根據需求進行子產品化編碼,并進行完善和單元測試,熟悉項目開發流程 |
其他參考文獻 | [1]鄒欣.建構之法[M] |
Github代碼簽入記錄
PairProject1-Java:

PairProject2-Java:
具體分工
黃樂興:
- 基本需求項目和進階項目的編寫;代碼調優;
馮凱:
- 需求分析;附加題編寫;單元測試;文檔書寫;
解題思路描述
1.WordCount:
- 初期:當我看到這個題目資訊時,就發現這次的需求并不簡單,甚至很難了解。是以,我花了數天的時間不斷和同學一起探讨需求,了解思路,明白每一點需求中的具體含義。因為, 如果需求無法正确地解讀,寫出的程式代碼也會有N多個坑,而填坑的過程花費的時間将會遠遠超過挖坑的時間。
- 中期:對于一些功能點的實作,例如:檔案的讀寫,由于使用的頻率較低,API使用早已忘記。采用面向搜尋引擎程式設計,Google + StackOverFlow 提問式搜尋,短時間最大效率學習相關API。 而對于一些隐約在腦海記得的API,直接滑鼠點選相關類檢視其中的源碼,配合源碼的注釋,進而再次掌握這個API。
2.論文資訊爬取:
- 工具:Jsoup
- 思路:通過 Jsoup 請求指定url(http://openaccess.thecvf.com/CVPR2018.py),擷取傳回 Document 對象。接着定位在 dl 标簽(論文資訊所在位置),使用一個循環,擷取每一個篇論文資訊。其中,每一篇的論文資訊的标題位于第一個 a 标簽的文本資訊中,而摘要資訊url位于這個标簽的 href 屬性中。繼續通過通路這個摘要資訊url,并爬取 id 為 abstract 的标簽的文本資訊即摘要資訊。
設計實作過程
1.代碼組織:
主要分為兩個類,Lib類和Main。其中,Lib類作為一個程式的功能庫,向上提供基礎的API;而Main類主要為一個程式的入口,通過調用Lib類的API完成程式功能。函數細分至每個功能點的定義,例如,判斷字元為分隔符封裝為一個函數;并對于幾大獨立功能封裝其相應的函數。對于一些較為複雜關鍵的函數,畫出了相應的流程圖,以便日後的維護以及糾錯。
2.單元測試:
為了友善我們的”測試工程師“有一個良好的測試體驗,我在不影響結果的情況下修改Lib類的相關函數,并打包成一個 Jar包;除此之外,編寫了一個基于JUnit的單元測試模闆類。”測試工程師“隻需通過CV大法導入 JAR 包 和測試模闆類,将Jar包添加至項目依賴中,安裝IDEA的JUnit插件,三步操作即可上手測試。
當然,簡單的單元測試還是不夠的。每一個小功能的正确并不能反映全局的正确性,興許哪一個的邏輯在某種關聯的情況下引發出不一樣的效果。這時我們就要上內建測試了,但由于時間的關系,沒有采用架構進行內建測試,而是直接人工執行+人工校驗結果。
3.關鍵算法及流程圖
指令行參數處理:
一種方式是周遊 String[] args,每次擷取兩個字元串,并通過值來進行相應的處理。但這種方式需要寫大量的if else 分支判斷條件,每次增加新的參數,還必須修改原有的代碼,不符合開閉原則。經過分析之後,我們得出第二個更為合理的方法,使用一個Map對指令行參數進行封裝。後續對于指令行的查找隻需通過Map.get(),且新增參數後也不必修改之前的代碼。
單詞計數:
定義兩個輔助變量,letterCheck 預設為-4,letterCheckAble 預設為 true。第一個變量用于判斷字首字母數是否大于等于4,第二個變量用于是否需要進行單詞檢測。逐個字元周遊字元串,當檢測到非字母時,判斷letterCheck是否為0,如果為0則将 letterCheckAble 指派為 false,關閉單詞檢測,直到遇到一個分隔符則再次打開檢測開關;當檢測到字母時,将 letterCheck 自增直至為0;當檢測到分隔符時,判斷單詞檢測是否為打開狀态且letterCheck 為0,如果是的話則把單詞數自增。
長度為N詞組的提取處理:
第一步:對字元串進行切割,分為兩類,一類為分隔符字元串,另一個為非分隔符字元串。具體操作為,使用Matcher 正規表達式,不斷比對相應的字元串,并放在一個字元串連結清單中。切割完之後,可以得到一個分隔符字元串連結清單和非分隔符字元串連結清單。
第二步:使用雙指針L和R,R從0開始到非分隔符字元串連結清單 list 的尾部,不斷周遊。在每次的周遊中,判斷 list.get(R) 是否為單詞。如果為單詞并且 R-L+1 == N,則已經找到一個合法詞組的坐标範圍(L-R),進而合并這些單詞作為詞組,放置在Map中;如果不為單詞,則将L指派為R+1,使得下一次R周遊的時候指針L和R再次重疊在一個地方。
性能分析與改進
性能分析圖以及消耗最大的函數:
此圖為通過 JProfiler 調優工具擷取。占用時間較長的大部分為系統庫函數,前幾個函數中隻有三個出現在代碼中。消耗最大的函數應該為 FileOutputStream.close() ,目前尚不清楚為啥占用時間較長。而執行次數最多的為 String.charAt()
改進的思路:
代碼優化:
初始化 BufferedReader 的預設大小為檔案長度,這樣隻需一次IO即可将整個檔案讀取進記憶體,而之前的預設大小是固定的。但在幾次嘗試之後,并沒有時間上的增進,可能是檔案不夠大的原因。
算法優化:
1.多個字元串尋找連續的長度N的合法詞組。使用雙指針進行搜尋,可以減少判斷的次數。
2.單詞計數。使用一個正規表達式進行全文比對,搜尋效率較高。
關鍵代碼展示與說明
1.MAP 自定義排序 + 分割 + 輸出
對于這個需求,可以聯想到 JAVA8 的一個新特性,流處理。将集合看做為一個流,流在管道運輸中加入各種處理,例如排序,限制,循環等,即可在較少的代碼量中完成一個複雜的功能。
// 排序Map并輸出
static void sortMapAndOut(Map<String, Integer> map, StringBuilder builder) {
map.entrySet()
.stream()
.sorted((e1, e2) -> {
int cmp = e2.getValue().compareTo(e1.getValue());
if (cmp == 0) return e1.getKey().compareTo(e2.getKey());
else return cmp;
})
.limit(10)
.forEach(o -> builder.append("<").append(o.getKey()).append(">").append(": ").append(o.getValue()).append("\n"));
}
2.長度為N詞組的提取處理:
主要的思路已經在上面的關鍵算法中進行展示,下面給出具體的代碼以及一些輔助函數的思路。
// 提取詞組,擷取單詞數
static int countWord(String s, int w, int len, Map<String, Integer> map) {
int wordNum = 0;
boolean isDivBegin = isDivision(s.charAt(0));
List<String> titles = cutStr(s, DIV_RE);
List<String> titles2 = cutStr(s, NOT_DIV_RE);
for (int i = 0, j = i; i < titles.size(); i++) {
if (isWord(titles.get(i))) {
wordNum++;
if ((i - j + 1) == len) {
String word = getWord(isDivBegin, titles, titles2, j, i).toLowerCase();
map.merge(word, w, (a, b) -> a + b);
j++;
}
} else {
j = i + 1;
}
}
return wordNum;
}
此輔助函數為拼接i-j範圍的合法單詞以及分割符字元串。
這裡存在兩個連結清單,其中s為合法單詞連結清單,而s2為分割字元串連結清單。通過下标之間的關系我們可以得出一個結論,當未切割字元串的第一個字元為字母時,拼接過程中切割字元串的下标等于j,而當第一個字元為非字母時,切割字元串的小标等于j+1。由此,我們可以通過這個規律對拼接這兩個連結清單。
// 拼接字元串
private static String getWord(boolean isDivBegin, List<String> s, List<String> s2, int j, int i) {
StringBuilder builder = new StringBuilder();
int offset = isDivBegin ? 1 : 0;
while (j <= i) {
builder.append(s.get(j));
if (j != i) builder.append(s2.get(j + offset));
j++;
}
return builder.toString();
}
部分測試代碼展示與說明
1.基礎需求單元測試
分别針對檔案中的字元數、有效單詞書以及字典序的單詞頻數做不同的單元測試。每個測試方面都帶有5個以上的測試點,覆寫大多數可能出現的情況。
/***********部分單元測試代碼****************/
private void newFile(String s) throws IOException {
BufferedOutputStream bf = new BufferedOutputStream(new FileOutputStream(TEST_FILE_NAME));
bf.write(s.getBytes());
bf.flush();
}
/*
* **測試檔案中字元的個數**
* 主要測試點:轉義字元、字母、數字及其他字元任意組合的個數
* 例:\\\"123abc!@#
* */
@Test
void testCharNum2() throws IOException {
newFile("\"\'\\26384 hfJFD *-.@!");
int charNum = CountUtil.getCharNum(TEST_FILE_NAME);
Assertions.assertEquals(20, charNum);
}
/*
* **測試檔案中單詞的個數**
* 主要測試點:不能以數字開頭,字母(4個開頭)和數字的任意組合,以特殊字元分割,不區分大小寫
* 例:file12desk%losses225
* */
@Test
void testWordNum3() throws IOException {
newFile("c2ools DisCount23-hayerS SELLER*CANcels#GAY9220^ 89NAVY!!)(SwingS=flying290");
int letterNum = CountUtil.getLetteryNum(TEST_FILE_NAME);
Assertions.assertEquals(6, letterNum);
}
/*
* **測試檔案中各單詞出現的次數**
* 主要測試點:至少以4個字母開頭,不區分大小寫,後跟字母和數字的任意組合,以特殊字元分割
* */
@Test
void testMaxWord4() throws IOException {
newFile("sex23 gold89&numbers&&&&90byes (cLicks009(clicks009)gold89 sexx )) shopping-NUMBERS265clicls");
LinkedHashMap<String, Integer> result = CountUtil.getMaxLetter(TEST_FILE_NAME);
System.out.println(result);
Assertions.assertEquals(1, 0);
}
初期測試過程中出現了一些BUG,測試失敗。
在經過幾次的調試和修改之後,終于全部通過測試,哈哈。
2.進階需求測試
在初步完成了進階需求之後,我們根據課程作業的要求,自己手動編寫了十餘個測試檔案,從最基礎的字元、單詞到複雜的長篇文章,使用指令行一一去測試,然後将測試的結果儲存在result。txt中,最後将測試結果和正确答案作對比,然後再進一步去做優化和調試,保證結果的一緻性。
遇到的困難及解決方法
HLXING:
- 需求不明,遲遲無法了解。通過微信提問的方式+同學之間交流解決。
- 爬蟲擷取的資料少了一半。Google 提問,發現是這個爬蟲庫的 API 存在設計上的缺陷,反人類的預設響應資料包大小1MB的設定,隻需加個
即可解決這個問題。maxBodySize(0)
- 測試結果與其它同學不符合。通過折半糾錯法,不斷删減輸入檔案的内容,最後确定問題出現在一個非ASCII字元(中文下的上引号)
KAI:
- 在搞測試的時候,剛開始設計的測試樣例都比較普通,沒有針對性,是以很難測出程式中存在的問題。在看了一些别的組的測試樣例之後,有了靈感,寫出了好多針對字元、單詞、字典序排序的樣例,這些樣例針對最可能出現問題的地方進行測試,果然,發現了不少的BUG,最終得以解決完善。
- 爬取資料進行資料挖掘分析的時候,将爬取的資料存放在檔案中。面對雜亂無章的資料,不知道如何從它們中抽取有用資訊。在經過一番思考之後,将不同類型的資料進行結構化存儲,然後按照不同的類别,從它們中提取有用資訊,去除無用資訊,最後加以可視化處理,将它們之間的關系很清晰的展示出來。
附加題設計與展示
1.設計思路:
用爬蟲将CVPR2018的官網(http://openaccess.thecvf.com/CVPR2018.py)的所有的論文題目清單以及相關作者的清單爬取下來,然後将爬取下來的資料結構化處理,然後儲存到文本中。之後,将内部資料結合外部爬取的資料,以及利用他們之間的聯系,充分挖掘其中隐藏的資料,并借助資料可視化技術将他們表示出來。
2.代碼展示:
"""**************爬取CVPR首頁源碼************"""
import requests
BASE_URL = "http://openaccess.thecvf.com/CVPR2018.py"
try:
html = requests.get(BASE_URL)
with open("index.html", "wt", errors="ignore") as f:
f.write(html.text)
except Exception as e:
print(str(e))
将論文标題加以切詞處理,然後根據其出現的頻率,繪制熱點研究方向的詞雲圖
"""***********部分繪圖代碼***********"""
with open("titles.txt", encoding="utf-8")as file:
text = file.read()
words = chinese_jieba(text)
wordcloud = WordCloud(font_path="C:/Windows/Fonts/simhei.ttf",
background_color="white", width=800,
height=400, max_words=50, min_font_size=8).generate(words)
image = wordcloud.to_image()
image.show()
利用資料挖掘技術,将論文發表所在的學術機構(高校、研究院、實驗室)所發表的論文數量加以統計,繪制柱狀圖,從一方面展示展示這些學術機構的研究能力。
"""***********部分繪圖代碼***********"""
def collect_univ():
univ_count = {}
with open("university_count.txt", "rt", encoding='utf-8') as f:
word = f.readline()
while word:
count = univ_count.get(word.strip(), None)
if count:
univ_count[word.strip()] += 1
else:
univ_count.setdefault(word.strip(), 1)
word = f.readline()
return sorted(univ_count.items(), key=lambda x: x[1], reverse=True)
def draw_graph():
univ_list = dict(collect_univ())
x = list(univ_list.keys())[:8]
y = list(univ_list.values())[:8]
plt.bar(x, y, alpha=1.0, width=0.7, color=(0.1, 0.5, 0.8), label=None)
plt.xlabel("Research Institute", fontsize=15)
plt.xticks(x, rotation=20)
plt.ylabel("Number of papers", fontsize=15)
plt.tick_params(axis='x', labelsize=7)
通過所有論文的作者之間的關系,将各個作者參與發表的論文數量加以統計,展示出科研能力比較強的一些作者。
def load_author():
soup = BeautifulSoup(open("../CVPR_Spider/index.html"), "html.parser")
authors = soup.find_all('a', href="#")
f = open("authors.txt", "wt", encoding='utf-8')
for author in authors:
filtered_author = author.text.replace(' ', '').replace('\n', ' ')
f.write(filtered_author + '\n')
f.close()
def analyse_authors():
author_count = {}
f = open("authors.txt", "rt", encoding='utf-8')
author = f.readline()
while author:
count = author_count.get(author.strip(), None)
if count:
author_count[author.strip()] += 1
else:
author_count.setdefault(author.strip(), 1)
author = f.readline()
return sorted(author_count.items(), key=lambda x: x[1],reverse=True)
通過爬取往年的CVPR論文發表數量,可以看出在計算機視覺和模式識别方面的研究投入呈逐年增長的趨勢。
3.相關附件:源碼和圖表
PSP表格
PSP 2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 1000 | 1800 |
Estimate | 估計這個任務需要多少時間 | 600 | 750 |
Development | 開發 | 700 | |
Analysis | 需求分析 (包括學習新技術) | 500 | |
Design Spec | 生成設計文檔 | 50 | 60 |
Design Review | 設計複審 | 30 | |
Coding Standard | 代碼規範 (為目前的開發制定合适的規範) | 70 | |
Design | 具體設計 | 200 | 250 |
Coding | 具體編碼 | 400 | |
Code Review | 代碼複審 | ||
Test | 測試(自我測試,修改代碼,送出修改) | ||
Reporting | 報告 | 90 | |
Test Report | 測試報告 | ||
Size Measurement | 計算工作量 | 20 | |
Postmortem & Process Improvement Plan | 事後總結, 并提出過程改進計劃 | 10 | |
合計 | 1500 |
評價你的隊友
1.值得學習的地方
- 程式設計能力很強,作業的需求是在他的帶領下完成分析,在程式設計過程中出現了不少BUG,他能夠認真分析代碼中出現的問題,幫助我們解決其中的問題。
- 善于分析問題,在作業需求不明确的情況下,能夠針對其中的需求,加以分析,使需求變得明确。
- 解題思路清晰,動手能力很強,能夠有條不紊地去完成工作。
2.需要改進的地方
- 細心程度需要提高。
項目總結
在這次的項目中,我獲得了性能改進以及單元測試的能力。JProfiler是一個易用的 Java 性能分析工具,通過 CPU 占用時長以此得出函數執行的時間,找出性能瓶頸地方,且加以改進。而 JUnit 是一個實用的單元測試庫,可以編寫代碼進行測試,代替以往的人工測試,省時省力。除此之外,項目的難度也提高了我問題分析能力,邏輯推理能力,能夠對一個問題加以拆解,最終解決。相比于上次的結對程式設計,我和隊友的配合能力也逐漸提高,不再是以往的無頭蒼蠅式地工作,而是對任務的配置設定有了更好地把握,能夠發揮出兩個人所擅長的地方,以此提高整個項目的工作效率。
這次作業相比上次作業,無論是在工作量還是代碼量都比上次多了不少。雖然花費了一周時間(每天三個小時以上)去完成這次作業,欣慰的是,在這個過程中我學習到了很多東西,使我受益匪淺。首先,在寫作業的初期,由于需求的不明确,前前後後出現了許多問題,不斷去問助教關于需求的問題,因為我明白,搞懂需求,永遠是軟體開發的第一步,也是最重要的一步,邁出了這一步,其餘的工作才能順利的進行。其次,在編碼過程中,我學會了去主動使用單元測試來進行代碼功能的測試,在以前的編碼過程中,都是邊寫代碼邊測試,沒有養成做完整體系的單元測試的習慣,這可能會導緻後期代碼出現一些預料之外的BUG,是以這個好習慣要保持下去,帶到以後的工作崗位上去。