這個做業屬於哪一個課程:軟件工程 1916 | Whtml
這個做業要求在哪裏:結對第二次——文獻摘要熱詞統計及進階需求java
結對學號:221600126 劉忠燏, 021600823 餘秉鴻git
這個做業的目標:完成做業要求中的基本需求和進階需求;熟悉 Git 和 GitHub 的使用;學習並掌握單元測試技巧;藉助單元測試,適當重構部分代碼github
Fork 的 GitHub 項目地址:PairProject2-Javaweb
GitHub 的簽入記錄apache
本次結對做業中,我和隊友之間的分工是這樣的:api
因爲這種分工,普通需求和進階需求的倉庫其實是分別提交的,雖然我兩個倉庫都 Fork 了,但實際上只有進階需求的倉庫是有提交記錄的。網絡
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 20 | 20 |
· Estimate | · 估計這個任務須要多少時間 | 20 | 20 |
Development | 開發 | 730 | 1120 |
· Analysis | · 需求分析(包括學習新技術) | 90 | 95 |
· Design Spec | · 生成設計文檔 | 30 | 35 |
· Design Review | · 設計複審 | - | |
· Coding Standard | · 代碼規範(爲目前的開發制定合適的規範 | 30 | 30 |
· Design | · 具體設計 | 120 | 240 |
· Coding | · 具體編碼 | 400 | 600 |
· Code Review | · 代碼複用 | - | |
· Test | · 測試(自我測試、修改代碼、提交修改) | 60 | 120 |
Reporting | 報告 | 65 | 65 |
· Test Report | · 測試報告 | 20 | 20 |
· Size Measurement | · 計算工做量 | 10 | 10 |
· Postmortem & Process Improvement | · 過後總結,並提出過程改進計劃 | 35 | 35 |
合計 | 815 | 1205 |
在做業要求發佈以後,我和個人隊友大體商量了一下,最後肯定下來就是他完成基礎需求的部分,我使用他的代碼完成進階需求(實際上他不只完成了基礎需求,還給進階需求預留了 API)。因而留給個人任務就只有爬蟲以及命令行解析。命令行解析的部分原本想本身寫,可是又擔憂本身在爬蟲上花費太多時間(實際上當我完成爬蟲部分的代碼後我發現本身的時間可能很少了),因而再一次藉助了一個開源的庫 Apache Common CLIs 完成了命令行解析的部分。WordCount 核心代碼的實現能夠在我隊友的博客裏找到。多線程
在做業截止以前,我本身也想到了一個 WordCound 的實現思路,只不過想到的太遲,來不及寫代碼了。我就在博客裏簡單講一下個人思路吧(可能其餘大佬也想到這種方案了):個人思路就是模仿 XML 的 SAX 解析模式,也就是將文檔以流的形式讀入,一次解析一行,分別在解析新行,解析空白符,解析單詞等狀況時產生一個對應的事件,並由單獨的事件處理程序處理。畫成類圖的話,WordCount 的結構以下圖所示:app
最後,進階需求的內容我並無所有完成。我沒有實現 -m
的功能(即詞組解析),短期內我想不出什麼方法來完成這個需求(若是最後的詞組沒有要求帶分隔符輸出的話,我認爲實現起來更容易些)
這部分任務,在整個進階需求中,算是相對比較簡單的。我原本想使用 Python,並使用 Python 的 urllib
庫和 BeautifulSoup
庫完成。不過在翻閱了相關資料後,我被相關的 API 搞得暈頭轉向,遂另尋方法。這裏的爬取,是從 CVPR 的網頁上獲取相關信息,因而使用查看論文頁的網頁結構,發現網頁的結構十分清楚,以其中一篇論文爲例:
<!-- 省略部分代碼…… --> <div id="papertitle">Embodied Question Answering</div> <!-- 省略部分代碼…… --> <div id="abstract"> We present a new AI task -- Embodied Question Answering (EmbodiedQA) -- where an agent is spawned at a random location in a 3D environment and asked a question ("What color is the car?"). In order to answer, the agent must first intelligently navigate to explore the environment, gather necessary visual information through first-person (egocentric) vision, and then answer the question ("orange"). EmbodiedQA requires a range of AI skills -- language understanding, visual recognition, active perception, goal-driven navigation, commonsense reasoning, long-term memory, and grounding language into actions. In this work, we develop a dataset of questions and answers in House3D environments, evaluation metrics, and a hierarchical model trained with imitation and reinforcement learning. </div> <!-- 省略部分代碼…… -->
上例中,論文的 Title 存放在 id
爲 papertitle
的 <div>
標籤裏,Abstract 存放在 id
爲 abstract
的 <div>
標籤裏,爬取起來可謂是很是容易。因而最後決定使用 Java 實現,爬蟲程序使用了一個開源項目 Jsoup(項目地址:GitHub,採用 MIT 協議),這個庫提供了對 HTML 文檔解析的支持,並提供一系列 API 用於提取數據(就像使用 DOM 同樣)。具體思路是先從首頁爬取每篇論文的連接,再分別訪問每篇論文的網頁獲取相關信息。
/** * PaperSniffer - Collect paper information from given website. * * @author Liu Zhongyu */ public class PaperSniffer { private List<String> paperUrls = null; private List<PaperContent> paperContents = null; /** * Return a list of paper URLs. */ public List<String> getPaperUrls() { if(paperUrls != null) return paperUrls; paperUrls = new LinkedList<>(); try { // 被註釋的代碼存在一個隱藏問題,下面未註釋的代碼爲正確代碼,詳情見後文 // Document document = Jsoup.connect(SnifferConfig.START_URL).get(); Document document = Jsoup.connect(SnifferConfig.START_URL).maxBodySize(0).get(); // document 的 select 方法接受一個字符串參數,字符串內容爲 CSS 選擇器 // 在 CVPR 2018 的首頁上,使用 "dt.ptitle > a" 選擇全部包含論文連接的 <a> 標籤 Elements linkElements = document.select(SnifferConfig.PAPER_URL_QUERY); for(Element link : linkElements) paperUrls.add(link.attr("href")); } catch (IOException e) { e.printStackTrace(); } return paperUrls; } /** * Return a list of PaperContent. * Each PaperContent object contains the titles and abstract of a paper. */ public List<PaperContent> getPaperContents() { if(paperContents != null) return paperContents; paperUrls = getPaperUrls(); paperContents = new LinkedList<>(); for(String paperUrl : paperUrls) { // visit every paper's web page to grab its title and abstract try { // 對每篇論文的 URL,打開該連接 Document document = Jsoup.connect(SnifferConfig.URL_BASE + paperUrl).get(); // 使用 CSS 選擇器語法選擇論文的標題節點和摘要節點,其中: // PAPER_TITLE_QUERY ----> "#papertitle" // PAPER_ABSTRACT_QUERY ----> "#abstract" Element titleNode = document.select(SnifferConfig.PAPER_TITLE_QUERY).first(); Element abstractNode = document.select(SnifferConfig.PAPER_ABSTRACT_QUERY).first(); // 提取這兩個節點的文本內容,建立一個 PaperContent 對象,加入結果列表 PaperContent paper = new PaperContent(titleNode.text(), abstractNode.text()); paperContents.add(paper); } catch (IOException e) { e.printStackTrace(); } } return paperContents; } }
程序編譯完成,也能夠正常運行,但須要等很長時間才能出結果。個人網絡訪問 CVPR 的網站仍是比較慢的,上述代碼中,對論文的爬取是單線程的,這影響了整個程序的運行時間。遂嘗試加入多線程支持,改動後的代碼以下(只展現修改後的方法)
/** * Return a list of PaperContent. * Each PaperContent object contains the titles and abstract of a paper. */ public List<PaperContent> getPaperContents() { if(paperContents != null) return paperContents; paperUrls = getPaperUrls(); paperContents = new LinkedList<>(); for(String paperUrl : paperUrls) { // visit every paper's web page to grab its title and abstract // create a thread for each paperUrl Runnable thread = new Runnable() { @Override public void run() { try { Document document = Jsoup.connect(SnifferConfig.URL_BASE + paperUrl).get(); Element titleNode = document.select(SnifferConfig.PAPER_TITLE_QUERY).first(); Element abstractNode = document.select(SnifferConfig.PAPER_ABSTRACT_QUERY).first(); PaperContent paper = new PaperContent(titleNode.text(), abstractNode.text()); synchronized (LOCK) { paperContents.add(paper); } } catch (IOException e) { e.printStackTrace(); } } }; thread.run(); } return paperContents; }
簡單地爲採集一篇論文信息的操做建立了線程,固然,在對 paperContents
的寫入上須要加鎖以免一些問題。我作過簡單的性能測試,爬取一樣數量的論文,單線程用時 211.73s,多線程用時 130.655s(單次運行時間),可見多線程所帶來的提高仍是比較明顯的。
可是和其餘同窗的爬取結果一比較,才發現我爬取的內容少了一半,經調試,發如今獲取論文連接時就只獲取了一半的連接,百思不得其解。最後仍是一個使用了相同的庫的同窗告訴我 Jsuit.connect()
建立連接時,其默認只獲取 1M 左右的信息,而 CVPR 的論文頁已經超出了這個大小,解決方案是在建立連接時,使用 Jsuit.connect().maxBodySize(0)
來解除這個內存限制1,修改代碼以後,爬取的論文列表纔算完整。
這部分我以爲作得不是很好。原本個人想法是走測試驅動開發的。但我在和隊友討論思路的時候我忘了提出來了,中間的幾天又光顧着注意實現的細節,結果到最後這部分單元測試變成了先寫代碼後測試,而後順帶幫隊友的代碼進行 Code Review。具體作的話,就是我看我隊友代碼裏的一個方法,而後根據隊友的註釋寫測試用例,最後運行單元測試。單元測試主要是針對隊友的代碼中一個實現了相似 C 語言中的 ctype.h
的類中的方法進行測試,好比:
@Test public void testIsNotDigitOrAlpha() { for(int i = 0; i < 26; i++) { assertFalse(String.format("%c: ", 'A' + i), c.isNotDigitOrAlpha((char)('A' + i))); assertFalse(String.format("%c: ", 'a' + i), c.isNotDigitOrAlpha((char)('a' + i))); } for(int i = 0; i < 9; i++) { assertFalse(String.format("%c: ", '0' + i), c.isNotDigitOrAlpha((char)('0' + i))); } assertTrue("â: ", c.isNotDigitOrAlpha('â')); assertTrue("é: ", c.isNotDigitOrAlpha('é')); }
上述方法是判斷一個字符是不是非字母數字字符。因而測試用例就覆蓋了全部的英文字母和數字,至於最後兩個法文字母,是從爬蟲的結果裏找到了,將其做爲一個非英文字母的用例。
@Test public void testIsWord() { assertTrue(c.isWord("apple")); assertFalse(c.isWord("foo")); assertFalse(c.isWord("123foo")); assertFalse(c.isWord("Café")); assertTrue(c.isWord("file123")); assertFalse(c.isWord("Mâché")); }
剛纔那個例子,裏面的狀況是能夠列舉的,而上面的被測方法是判斷一個字符串(僅由小寫字母和數字構成)是不是合法的單詞,隊友說傳入這個方法的字符串是已經被分割符分割並所有轉成小寫後的單詞,因此以上的樣例大概能覆蓋到各類狀況。
儘管如此,單元測試仍是在必定程度上發揮了做用,主要是在重構上面,個人隊友多是寫 C 寫習慣了吧,對於字符類型的判斷用的是 C 的那一套,代碼裏 Magic Number 處處亂飛,有了單元測試後我就能夠比較放心地去重寫這部分代碼(測試可以保證重構後的代碼功能不變)。
我在寫單元測試的時候,也是順便幫隊友的代碼進行 Code Review,例如上述測試方法中被測試的一個 isNotDigitOrAlpha
方法,其最先的名稱叫 isCharacter
,在寫單元測試的時候我發現了這個問題,並將其改爲了一個更合理的名字。
本身在此次做業中,有不少事情沒有考慮周全,在完成做業的過程當中交流得還不夠。我以爲若是溝通得再頻繁一些,開發過程當中出現的問題會提前暴露出來,至少給解決問題留下餘地(好比說將測試驅動開發帶到此次做業裏來,後期出現嚴重 bug 的機率會有所下降)
我對我隊友的評價仍是很好的。他的代碼寫得還能夠,但有一個問題是他對於一些工具和框架(好比 Git 和 JUnit)不是很熟,還須要學習。他身上有一點值得我去學習的地方是他會嘗試着本身造輪子(好比在 WordCount 裏本身實現了堆排序),而我則更傾向於使用現有的庫
https://jsoup.org/apidocs/org/jsoup/Connection.html#maxBodySize-int-↩