所屬課程 | 軟件工程1916|W(福州大學) |
做業要求 | 結對第二次—文獻摘要熱詞統計及進階需求 |
結對學號 | 221600327、221600329 |
基本需求Github項目地址 | PairProject1-Java |
進階需求Github項目地址 | PairProject2-Java |
做業目標 | 運用結對編程完成做業,加強團隊協做能力 |
參考文獻 | 《構建之法》、《數據結構與算法分析 Java語言描述》 |
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
Planning | 計劃 | 20 | 30 |
• Estimate | • 估計這個任務須要多少時間 | 20 | 30 |
Development | 開發 | 1060 | 1290 |
• Analysis | • 需求分析 (包括學習新技術) | 60 | 120 |
• Design Spec | • 生成設計文檔 | 10 | 20 |
• Design Review | • 設計複審 | 30 | 20 |
• Coding Standard | • 代碼規範 (爲目前的開發制定合適的規範) | 10 | 10 |
• Design | • 具體設計 | 30 | 40 |
• Coding | • 具體編碼 | 800 | 960 |
• Code Review | • 代碼複審 | 60 | 60 |
• Test | • 測試(自我測試,修改代碼,提交修改) | 60 | 60 |
Reporting | 報告 | 80 | 120 |
• Test Report | • 測試報告 | 40 | 60 |
• Size Measurement | • 計算工做量 | 10 | 20 |
• Postmortem & Process Improvement Plan | • 過後總結, 並提出過程改進計劃 | 30 | 40 |
合計 | 1160 | 1440 |
characters: number
words: number
lines: number
: number html
: number
...
1.接口封裝java
我理解的input.txt的內容應該是,文件中只包含ascii碼以及空格,水平製表符,換行符這些字符。因此想直接用String存儲文件內容,用String.length()來獲得字符數。可是這個會有一個問題,就是在windows平臺下,換行符是用"\r\n",String.length()會統計爲兩個字符,用「\n」代替「\r\n」,便可解決問題。git
文件由兩種字符分割符(空格,非字母數字符號)和非分隔符構成,單詞統計與非分割符無關,因此直接爲了接下來split方便,將全部的分割符都替換爲「|」,第二步分割字符串,第三步,使用正則表達式匹配題目所規定的單詞,統計單詞數。github
一開始我想的統計行數是使用readline();去讀出每一行,而後逐行判斷這行有沒有ascii碼值<32的字符存在,由於ascii碼值小於32的是在文本中不顯示的,因此我開始的想法就是這樣的;不過試了一下以後發現對於"\r\n"之類的字符和空白行單靠ascii碼是區分不出來的,而後在網上找了資料以後,發現String.trim().isEmpty()是能夠去除空白行而後統計的,最後使用的是這個方法。正則表達式
由於處理場景爲單機,因此決定選用散列表,順序掃描剛剛的到的單詞數組,當掃描到某個關鍵詞時,就去散列表裏查詢。若是存在,就將對應的次數加一;若是不存在,就將他插入到散列表,並記錄爲1,以此類推,遍歷完後,散列表中就存儲了不重複的單詞以及其出現的次數。
而後就是求Top10,創建一個大小爲10的小頂堆,遍歷散列表,依次取出每一個單詞及對應出現的次數,而後與堆頂的單詞比較。若是出現次數比堆頂單詞的次數多,就刪除堆頂單詞,將這個次數更多的關鍵詞加入到堆中,以此類推,遍歷完散列表,堆中的單詞就是出現次數top10的單詞。而後排序輸出。算法
bean包:Word類編程
unitl包:BasicWordCount類、IOUnitls類windows
BasicWordCount類:數組
統計字符數函數 :網絡
/** * 計算文件字符數 * @param fileName 文件名 * @return long 字符數 */ public long characterCount(String fileName){}
統計行數 :
/** * 計算文件行數 * @param fileName 文件名 * @return long 行數 */ public long lineCount(String fileName){}
統計單詞數 :
/** * 計算文件單詞數 * @param fileName 文件名 * @return long 單詞數 */ public long wordCount(String fileName){}
統計詞頻top10的10個單詞及其詞頻 :
/** * 計算出現次數top10的單詞及其詞頻 * @param fileName 文件名 * @return Word[] 單詞數組 */ public Word[] topTenWord(String fileName) {}
IOUnitls類:
讀文件
/** * 讀入指定文件名的文件數據 * @param fileName 文件名 * @return BufferedReader * @throws IOException */ public static BufferedReader readFile(String fileName){}
寫文件
/** * 將制定字符串輸出到result.txt * @param fileContent 字符串 */ public static void writeFile(String fileContent, String fileName) {}
解題思路
流程圖:
代碼:
//進行統計的散列表 public Word[] topTenWord(String fileName) { ...//省略讀取文件、將文本處理成單詞數組的過程 Map<String, Integer> countMap = new HashMap<>(); for(int i = 0; i < splitStrings.length; i++) { if(Pattern.matches(regex, splitStrings[i])) { Integer outValue = countMap.get(splitStrings[i]); if (null == outValue) { outValue = 0; } outValue++; countMap.put(splitStrings[i], outValue); } } //求top10 PriorityQueue<Word> topN = new PriorityQueue<>(10, comp); Iterator<Map.Entry<String, Integer>> iter = countMap.entrySet().iterator(); Map.Entry<String, Integer> entry; while (iter.hasNext()) { entry = iter.next(); if (topN.size() < 10) { topN.offer(new Word(entry.getKey(), entry.getValue())); } else { // 若是當前數據比小頂堆的隊頭大,則加入,不然丟棄 if (topN.peek().getCountNum() < entry.getValue()) { topN.poll(); topN.offer(new Word(entry.getKey(), entry.getValue())); } } } //結果集 Word[] result = null; int wordCount = countMap.size(); if(wordCount < 10) { result = new Word[(int) wordCount]; }else { result = new Word[10]; } topN.toArray(result); //對top10單詞排序 Arrays.sort(result, comp); return result; }
複雜度分析:遍歷散列表須要 O(n) 的時間複雜度,一次堆化操做須要 O(logK) 的時間複雜度,因此最壞狀況下,n個元素都入堆一次,因此最壞狀況下,求TopK的時間複雜度是O(nlogk)。
從CPU Call Tree圖能夠看出,耗時主要是在對字符串的分割和單詞的正則匹配。
①程序一:爬取CVPR2018論文到本地result.txt的命令行程序
②程序二:支持命令行參數的wordCount命令行程序
(1)使用工具爬取論文信息**輸出到result.txt文件 (2)自定義輸入輸出文件** WordCount.exe -i [file] -o [file] (3)加入權重的詞頻統計** WordCount.exe -w [0|1] (4)新增詞組詞頻統計功能** WordCount.exe -m [number] (5)自定義詞頻統計輸出** WordCount.exe -n [number] (6)多參數的混合使用** WordCount.exe -i input.txt -m 3 -n 3 -w 1 -o output.txt
要從網頁上爬取數據,就得分析網頁HTML源代碼構成,從CVPR2018首頁的HTML代碼能夠看出,在首頁的論文列表中,每一篇的論文標題在HTML代碼上體現爲以下形式:
<dt class="ptitle"><a href="xxx.html">論文標題</a></dt>
其中<a></a>
標籤裏的href屬性爲論文詳情頁的網頁地址。
由此咱們能夠獲取每篇論文詳情頁的url:
第一步就是在網頁的DOM樹中提取全部的<dt class="ptitle">的節點。 第二步從每一個<dt class="ptitle">的節點中提取出<a>標籤的href屬性。
接着分析論文詳情頁的HTML代碼,
咱們能夠發現,在詳情頁源代碼中,論文標題的html代碼長這個樣子:
<div id="papertitle">論文標題</div>
論文摘要的html代碼長這個樣子:
<div id="abstract">這是論文摘要</div>
由此咱們能夠獲取每篇論文的標題和摘要:
第一步從網頁的DOM樹中找到<div id="papertitle">的節點,獲取他的文本即爲paper title。 第二步從網頁的DOM樹中找到<div id="abstract">的節點,獲取他的文本即爲paper abstract。
自定義參數的實現是先設計一個CommandLine類,把做業中的參數做爲類的私有成員,而後從String []args中讀取命令行參數,存進CommandLine類中,再使用getxxxx()公有函數取出相應的參數就可使用自定義參數了。
從爬取的論文數據可知,title獨佔一行,abstract獨佔一行,因此能夠按行處理數據。若是-w 參數爲1,修改基本需求的文件,當一行的開頭是title:,那麼這一行的單詞的權重爲10。其餘的一概權重爲1。
由於詞組不能跨越Title、Abstract,並且需求給定Title和Abstract都是獨佔一行的,因此按行讀取。
而後構造一個隊列,對讀取的一行開始掃描,接下來的判斷邏輯以下圖:
掃描完後,對得到的散列表進行topK操做,便可。
這個很簡單,基本需求規定的是top10,而這裏咱們只要從參數獲取topK的值,傳入參數,遍歷單詞列表,而後維護一個大小爲K的小頂堆便可。
bean包:Word類、CommandLine類
unitl包:AdvancedWordCount類、Command類、IOUnitls類
AdvancedWordCount類:
統計字符數函數 : 同基本需求
統計行數 : 同基本需求
統計單詞數 : 同基本需求
統計單詞詞頻topK的K個單詞及其詞頻 :
/** * 計算單詞的(加權)詞頻和topK * @param fileName 文件名 * @param topK 詞頻前K的單詞 * @param isWeight 是否啓用加權計算 * @return Word[] 有序單詞數組 */ public Word[] topKWordWeighting (String fileName, int topK, boolean isWeight) {}
統計詞組詞頻topK的K個詞組及其詞頻 :
/** * 計算詞組的(加權)詞頻和topK * @param fileName 文件名 * @param groupNum 幾個單詞爲一個組 * @param topK 詞頻前K的詞組 * @param isWeight 是否啓用加權計算 * @return Word[] 有序單詞數組 */ public Word[] topKWordsWeighting (String fileName, int groupNum,int topK, boolean isWeight) {}
Command類:
/** * 解析命令行參數 * @param args 參數 * @return CommandLine對象 */ public CommandLine ParseCommand(String[] args) {}
IOUnitls類(同基本需求)
... //進行統計的散列表 Map<String, Integer> countMap = new HashMap<>(); while((line = bufferedReader.readLine()) != null) { String regexTile = "Title: .*"; int weight = 1; if(Pattern.matches(regexTile, line)) { line = line.replaceAll("Title: ", ""); weight = 10; }else { line = line.replaceAll("Abstract: ", ""); weight = 1; } line = line.toLowerCase(); //按字符分析 int groupnumber = 0;//詞組單詞個數 int wordStratPosition = 0;//單詞開始下標 int wordEndPosition = 0;//單詞開始下標 int flagPosition = 0;//標記詞組第一個單詞的結束位置 Queue<String> wordReadyQueue = new LinkedList<String>(); for(int i = 0; i < line.length()-3;) { boolean flag = false; if(Character.isLetter(line.charAt(i))) { if(Character.isLetter(line.charAt(i+1))) { if(Character.isLetter(line.charAt(i+2))) { if(Character.isLetter(line.charAt(i+3))) { wordStratPosition = i; for(int j=i+4; j < line.length(); ++j) { if(!Character.isLetterOrDigit(line.charAt(j))) { wordEndPosition = j; //獲得一個單詞,加入到隊列 wordReadyQueue.add(line.substring(wordStratPosition, wordEndPosition)); groupnumber++;//詞組單詞數+1 if(groupnumber == 1) { flagPosition = j; } i=j; flag = true; break; }else if((j+1)==line.length()) { wordEndPosition = j+1; //獲得一個單詞,加入到隊列 wordReadyQueue.add(line.substring(wordStratPosition)); groupnumber++;//詞組單詞數+1 if(groupnumber == 1) { flagPosition = j; } i=j; flag = true; break; } } }else { if(!Character.isLetterOrDigit(line.charAt(i))) { wordReadyQueue.add(line.charAt(i)+""); }else { while(wordReadyQueue.poll()!=null) {}//清空隊列 while(Character.isLetterOrDigit(line.charAt(i++))) { if(i >= line.length()){ break; } } groupnumber = 0; flag = true; } } }else { if(!Character.isLetterOrDigit(line.charAt(i))) { wordReadyQueue.add(line.charAt(i)+""); }else { while(wordReadyQueue.poll()!=null) {}//清空隊列 while(Character.isLetterOrDigit(line.charAt(i++))) { if(i >= line.length()){ break; } } groupnumber = 0; flag = true; } } }else { if(!Character.isLetterOrDigit(line.charAt(i))) { wordReadyQueue.add(line.charAt(i)+""); }else { while(wordReadyQueue.poll()!=null) {}//清空隊列 while(Character.isLetterOrDigit(line.charAt(i++))) { if(i >= line.length()){ break; } } groupnumber = 0; flag = true; } } }else { if(!Character.isLetterOrDigit(line.charAt(i))) { if(groupnumber!=0) { wordReadyQueue.add(line.charAt(i)+""); } }else { while(wordReadyQueue.poll()!=null) {}//清空隊列 while(Character.isLetterOrDigit(line.charAt(i++))) { if(i >= line.length()){ break; } } groupnumber = 0; flag = true; } } if(groupnumber == groupNum) {//達到要求個數,加入散列表 String wordGroup = ""; String wordOfQueue = wordReadyQueue.poll(); while(wordOfQueue != null) { wordGroup = wordGroup + wordOfQueue; wordOfQueue = wordReadyQueue.poll(); } groupnumber = 0; //加權 Integer outValue = countMap.get(wordGroup.toString()); if (null == outValue) { outValue = 0; } if(isWeight) { outValue += weight; }else { outValue++; } countMap.put(wordGroup.toString(), outValue); i = flagPosition; } if(!flag) { ++i; } } } ...
由於以前寫安卓應用曾經用戶jsoup抓取教務系統上的成績等數據,此次天然而然使用了jsoup來對論文進行爬取。代碼以下:
/** * 從cvpr網站爬取論文數據,並寫入result.txt * @param URL */ public static void getFile(String URL) { BufferedWriter bufferedWriter = null; try { File outputFile = new File("result.txt"); bufferedWriter = new BufferedWriter(new FileWriter(outputFile)); //get方式獲得HTML數據 //默認設置下,jsoup超時時間爲3秒,鑑於當前網絡環境,修改成10秒 //默認設置下,jsoup最大獲取的長度只有1024K,設置maxBodySize(0),可不限長度 Document doc = Jsoup.connect(URL).timeout(10000).maxBodySize(0).get(); //從HTML中選擇全部class=ptitle的節點 Elements paperList = doc.select("[class=ptitle]"); //從ptitle節點中選擇a標籤的href屬性值 Elements links = paperList.select("a[href]"); int count = 0; //分別當問每篇論文的詳情頁 for (Element link : links) { //論文詳情頁URL String url = link.attr("abs:href"); Document paperDoc = Jsoup.connect(url).timeout(10000).maxBodySize(0).get(); //獲取論文title Elements paperTitle = paperDoc.select("[id=papertitle]"); String title = paperTitle.text(); //獲取論文Abstract Elements paperAbstract = paperDoc.select("[id=abstract]"); String abstracts = paperAbstract.text(); //數據寫入文件 bufferedWriter.write(count++ + "\r\n"); bufferedWriter.write("Title: " + title + "\r\n"); bufferedWriter.write("Abstract: " + abstracts + "\r\n\r\n\r\n"); } } catch (Exception e) { System.out.println("獲取論文數據失敗"); e.printStackTrace(); }finally { try { if(bufferedWriter != null) { bufferedWriter.close(); } }catch (Exception e) { System.out.println("獲取論文數據失敗"); e.printStackTrace(); } } }
考慮到進階的數據量可能比較大,在壓力測試時,咱們用了近60M的txt(近6000萬的字符)文件進行測試。內存佔用在1G左右,因爲複用基本需求的接口,在性能測試時,依然是對字符串的分割(split)耗時最多佔用內存很大,緣由是由於原來的代碼是把這6000萬字符存放到字符數組裏,在進行合法單詞的判斷。
在統計單詞時,直接遍歷一遍文本數據,識別單詞的start和end的下標,直接截取單詞進行識別。代碼以下:
for(int i = 0; i<updateString.length();) { startPosition = i; endPosition = i; while(Character.isLetterOrDigit(updateString.charAt(i++))) { endPosition++; } if(Pattern.matches(regex, updateString.substring(startPosition, endPosition))) { countOfWord++; } }
咱們用的是Eclipse中的JUnit4進行的測試,咱們總共設計了16個測試單元,其中字符計數,詞計數,行計數各三個,進階單詞和詞頻各兩個。測試用例都是根據做業要求設計的各類字符混雜,有空白行,數字字母混雜的形式進行測試,測試結果都顯示咱們的程序知足了題目的要求。
單元測試 | 測試覆蓋 | 測試代碼塊 | 測試個數 |
---|---|---|---|
BasicWordCountTest.testCharacterCount() | 普通字符、空格、各類符號 | BasicWordCountTest.CharacterCount() | 3 |
BasicWordCountTest.testWordCount() | 普通字符、空格、各類符號,字母大小寫,數字與字母各類組合 | BasicWordCountTest.WordCount() | 3 |
BasicWordCountTest.testLineCount() | 空白行、非空白行 | BasicWordCountTest.LineCount() | 3 |
BasicWordCountTest.testTopTenWord() | 普通字符、空格、各類符號,字母大小寫,數字與字母各類組合 | BasicWordCountTest.TopTenWord() | 3 |
AdvancedWord.TesttopWordWeighting() | 混合單詞 | AdvancedWord.topWordWeighting() | 2 |
AdvancedWord.TesttopWordsWeighting() | 混合詞組 | AdvancedWord.topWordsWeighting() | 2 |
部分測試代碼
package Untils; import static org.junit.Assert.*; import org.junit.Test; public class BasicWordCountTest { public static BasicWordCount basic=new BasicWordCount(); static String fileName="testinput2.txt"; String []testTopWord= {"abcd123","here","your","aaaa","abss","bbbb","cccc","ddda","dera","esds"}; int []testTopWordCount= {7,2,2,1,1,1,1,1,1,1}; @Test //測試字符統計 public void testCharacterCount() { System.out.println(basic.characterCount(fileName)); assertEquals(189,basic.characterCount(fileName)); } //測試單詞統計 @Test public void testWordCount() { basic.wordCount(fileName); assertEquals(21,basic.wordCount(fileName)); } @Test //測試top10單詞 public void testTopTenWord() { basic.topTenWord(fileName); for(int i=9;i>0;i--) { System.out.println(testTopWord[9-i]+"=="+i+"=="+basic.topTenWord(fileName)[i].getKey()); assertEquals(testTopWord[9-i],basic.topTenWord(fileName)[i].getKey()); assertEquals(testTopWordCount[9-i],basic.topTenWord(fileName)[i].getCountNum()); } } @Test //測試行數 public void testLineCount() { basic.lineCount(fileName); assertEquals(4,basic.lineCount(fileName)); } }
其中一個測試結果截圖
在JUnit4的測試下,咱們的整體代碼覆蓋率在80%左右,其中Main.java類是用來輸出結果的類,因此沒有加入測試,還有一些緣由是不少的異常處理是沒有辦法觸發的,這些代碼沒有覆蓋住。不過咱們主要代碼的覆蓋率都在90%左右,甚至以上的,因此說咱們的測試結果仍是對程序正確性作了有力驗證的。
碼出高效 :阿里巴巴Java開發手冊終極版v1.3.0
221600327:
行數統計、命令行參數解析、輸入輸出模塊代碼編寫、單元測試、文檔編寫
221600329:
字符統計、單詞統計、詞頻統計模塊代碼編寫、爬蟲設計和實現、性能測試、文檔編寫
原本咱們認爲的詞組詞頻統計會過濾掉單詞間的非法字母數字字符,然後來助教重申了需求,要求輸出這些字符。由於原先的作法是先把非法字母數字字符都替換掉,而後再進行操做。這需求一改,不就全涼了。後面只能一個字符一個字符的遍歷,再截取單詞。可是最後發如今數據量大的時候,不採用split去分割字符串,直接遍歷反而速度更快,並且更省內存。
隊友給夠按時完成分配的任務,有耐心,可能基礎不是特別紮實,可是肯學肯作,這點事值得確定的。
須要改進的地方:編碼能力須要增強。
個人隊友在咱們結對中大部分時間是駕駛員的角色,他對於需求的分析能很快劃分紅若干個字模塊,在編程方面他數據結構和算法方面的基礎知識至關紮實,編碼實現能力也很強。我最欣賞的是個人隊友認真和仔細的態度,他對做業近乎追求完美,在知足正確性以後還儘可能去優化性能;還有他的堅決的信念,就算他在這周身體不舒服,天天狂咳嗽,可是他仍是沒有所以落下過一點進度,這是我很敬佩的。
須要改進的地方:在任務分配方面能夠均衡一些,否則會累着他本身。
此次做業碰上了我重感冒的時候,一直咳嗽不止(不敢熬夜了),再加上本身編程能力不突出,因此時間上安排的很差,對於附加部分的需求,沒有時間去考慮和設計。這兩次都是結對任務,我和隊友之間的協做能力有了進步,可是目前仍是沒有很高的效率,可能還沒度過學習階段,依然處於磨合階段。
這兩週的任務都是結對任務,因此以此爲契機咱們閱讀了《構建之法》第四章兩人合做,瞭解到結對編程是極限編程這一思想的具體體現,結對編程有三種形式:
①鍵盤鼠標式②Ping-pong式③領航員-駕駛員式。
瞭解到結對編程,由於在結對編程過程當中有隨時的複審和交流,能夠減小犯錯,提升解決問題的效率,造成知識傳遞。
瞭解到要作好結對編程,須要遵照相同的代碼規範,在不一樣的階段,不一樣的人之間要有不一樣的方式,並且要養成我的良好的生活習慣。
可是我以爲雖然結對編程有不少好處,可是結對編程的兩個隊友,編程水平不能相差過大,否則可能會形成交流變成了教學,浪費更多的時間,影響效率。