班級:軟件工程1916|W
做業:結對第二次—文獻摘要熱詞統計及進階需求
結對學號:221600418 黃少勇、 221600420 黃種鑫
課程目標:學會使用Git、提升團隊協做能力
Github地址:基礎需求、進階需求
分工:javascript
- 黃少勇--詞頻統計代碼實現,性能優化
- 黃種鑫--爬蟲代碼實現,可視化實現,單元測試
基本需求:
進階需求:
html
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 10 | 10 |
• Estimate | • 估計這個任務須要多少時間 | 10 | 10 |
Development | 開發 | 750 | 980 |
• Analysis | • 需求分析 (包括學習新技術) | 120 | 150 |
• Design Spec | • 生成設計文檔 | 20 | 20 |
• Design Review | • 設計複審 | 10 | 10 |
• Coding Standard | • 代碼規範 (爲目前的開發制定合適的規範) | 10 | 10 |
• Design | • 具體設計 | 30 | 40 |
• Coding | • 具體編碼 | 400 | 550 |
• Code Review | • 代碼複審 | 100 | 120 |
• Test | • 測試(自我測試,修改代碼,提交修改) | 60 | 80 |
Reporting | 報告 | 50 | 50 |
• Test Report | • 測試報告 | 20 | 20 |
• Size Measurement | • 計算工做量 | 10 | 10 |
• Postmortem & Process Improvement Plan | • 過後總結, 並提出過程改進計劃 | 20 | 20 |
合計 | 810 | 1040 |
在拿到這個題目後,咱們倆經過分析,肯定基本需求即爲進階需求的特例(進階需求中, -m
參數的值取爲 1
, -n
參數的值取爲 10
, -w
參數的值取爲 0
即爲基本需求的要求),因此咱們一開始就肯定直接開發進階需求,經過給定參數默認值的方式,完成基本需求的任務。java
本次需求包含三部分:字符統計、單詞(詞組)統計、行數統計,而這三個能夠當作一個連續的過程,因此咱們的思路一開始也是想把三部分整合在一塊兒完成,即:打開一次文件,按行讀取(實現了行數統計),將讀取出來的行,進行字符統計,而且使用 String.split()
方法將整行切割爲單詞,實現單詞的統計,進而按照給定的 -m
參數,將分隔完的單詞進行拼裝,統計出長度爲m的詞組。可是後來詳細分析需求後,發現題目要求將三個功能獨立出來,因此最終決定把三個功能分開實現,即:實現三個方法,分別實現打開文件並統計字符、單詞(詞組)、行數。node
另外,爲了使處理命令行參數和詞頻統計等功能分隔開,而且實現相對獨立,咱們決定實現一個 Signal
類。該類主要完成對命令行的分析,爲詞頻統計提供參數。jquery
由以上思路能夠獲得,咱們須要兩個類—— WordCount
類及 Signal
類。WordCount
類中至少實現三個方法:字符統計、單詞(詞組)統計、行數統計, Signal
類至少實現一個方法:命令行分析。類圖設計以下:
詞頻統計流程圖:
行數、字符數統計流程圖:
git
如下測試均使用爬取獲得的2018年CVPR論文數據,使用參數 -m 2
,使用工具JProfiler獲得。
概覽:
優化前各方法耗時:
經過分析各個方法的耗時狀況,咱們獲得,程序的瓶頸主要在 WordCount.setWordNumber()
方法上,所以咱們着重對該方法優化。github
private void setWordNumber() { String line; try (BufferedReader br = new BufferedReader(new FileReader(inFile))) { while ((line = br.readLine()) != null) { line = line.toLowerCase(); // 按行讀取,並判斷是否爲Title行 if (line.indexOf("title:") == 0) { weight = signal.getwValue(); line = line.replaceFirst("title:", ""); } if (line.indexOf("abstract:") == 0) { weight = 1; line = line.replaceFirst("abstract:", ""); } splitLine(line); // 將每一行進行拆分處理 } } catch (IOException e) { e.printStackTrace(); } } // 判斷一個單詞是不是題目要求的合法單詞 private boolean isWord(String s) { return s.length() >= 4 && Character.isLetter(s.charAt(0)) && Character.isLetter(s.charAt(1)) && Character.isLetter(s.charAt(2)) && Character.isLetter(s.charAt(3)); } private void splitLine(String line) { if(line.isEmpty()){ return; } if(Character.isLetterOrDigit(line.charAt(0))) { deviation = -1; } else { deviation = 0; } String[] str = line.split("[^a-z0-9]"); // 切割獲得每一個單詞 for (String aStr : str) { if (!"".equals(aStr)) { // 去掉切割產生的空白後,將單詞加入arrayList待後續組詞使用 arrayList.add(aStr); if (isWord(aStr)) { // 判斷單詞單詞是否合法 wordNumber += 1; } } } str = line.split("[a-z0-9]"); // 切割獲得單詞間的分隔符 for(String aStr : str) { if(!"".equals(aStr)) { separator.add(aStr); } } setMap(); // 拼接單詞,組成長度爲m的詞組 arrayList.clear(); separator.clear(); word.clear(); }
經過觀察JProfiler的數據,咱們發現, WordCount.setWordNumber()
中,耗時最多的爲 WordCount.splitLine()
中的 String.split()
耗時最多。正則表達式
查閱資料後,網上對於 split()
方法的優化,主要是使用 String.indexOf()
方法或 StringTokenizer()
方法實現,可是這兩個方法一次僅能查找一個分隔符,而本次做業中分隔符種類較多,自行實現起來可能較爲麻煩,並且咱們查閱了JAVA中的 split()
源碼,發現其也是採用的 indexOf()
實現,故咱們放棄了手動使用 indexOf()
實現分割。數組
最後,咱們嘗試將 split()
的調用,改成使用正則實現:性能優化
private void splitLine(String line) { // ... Pattern r = Pattern.compile("[a-z0-9]+"); Matcher m = r.matcher(line); while (m.find()){ arrayList.add(m.group(0)); if (isWord(m.group(0))) { wordNumber += 1; } Pattern r2 = Pattern.compile("[^a-z0-9]+"); Matcher m2 = r2.matcher(line); while (m2.find()){ separator.add(m2.group(0)); } // ... }
改用正則優化第一次後各方法耗時:
使用正則後,時間少了 500ms 左右,算是有些許優化,可是效果並不明顯,因此咱們針對耗時第二多的 isWord()
方法進行改進。 isWord()
雖然簡單,可是因爲調用次數過多(每一個單詞都得調用兩次),因此總耗時過大。咱們經過加入一個輔助數組,用來記錄每一個單詞是否合法,這樣子能夠把 isWord()
的調用次數變爲原來的一半。修改以下:
private void splitLine(String line) { // ... while (m.find()){ arrayList.add(m.group(0)); if (isWord(m.group(0))) { wordNumber += 1; isLegalWord.add(true); // 若是是合法單詞,就把對應位置爲true } else{ isLegalWord.add(false); } } // ... setMap(); // ... } private void setMap() { combineWord(); // ... } // 將單詞拼接爲長度爲m的詞組 private void combineWord() { for (int i = 0; i < arrayList.size() - signal.getmValue() + 1; i++) { boolean flag = true; for (int j = 0; j < signal.getmValue() && flag; j++) { if (!isLegalWord.get(i+j)) { flag = false; } } // ... } }
優化第二次後各方法耗時:
加入輔助數組後,時間又減小了 500ms 左右。
至此,在兩次優化後,運行時間可以減小 1s 左右。
// 提供一個獲取行數的接口,供外部調用 public int getLineNumber() { return lineNumber; } // 生成行數,爲私有方法,供構造函數調用 private void setLineNumber() { String line; try (BufferedReader br = new BufferedReader(new FileReader(inFile))) { while ((line = br.readLine()) != null) { // 按行讀取文件 if(!line.trim().isEmpty()){ // 去掉行首行尾的空白後,判斷是否爲空行 lineNumber++; } } } catch (IOException e) { e.printStackTrace(); } }
// 提供一個獲取字符數的接口,供外部調用 public int getCharacterNumber() { return characterNumber; } // 生成字符數,爲私有方法,供構造函數調用 private void setCharacterNumber() { int ch; try (FileReader fr = new FileReader(inFile)) { while ((ch = fr.read()) != -1) { // 逐字節讀取文本 if(ch != 13){ // 若是讀取到的字符不爲 \r 時字符數加1 // 由於題目要求的換行符爲\r\n(爲一個總體), // 若是不跳過其中一個,會致使最終字符數會比實際字符數多出一倍行數, // 因此選擇其中的一個便可,咱們選擇了 \r。 characterNumber++; } } } catch (IOException e) { e.printStackTrace(); } }
// 提供一個獲取單詞數的接口,供外部調用 public int getWordNumber() { return wordNumber; } // 生成單詞數,爲私有方法,供構造函數調用 private void setWordNumber() { String line; try (BufferedReader br = new BufferedReader(new FileReader(inFile))) { while ((line = br.readLine()) != null) { // 逐行讀取,並將每行的內容轉爲小寫,便於後續處理 line = line.toLowerCase(); // 判斷是否爲Title行或者Abstract行,調整權重,用於統計詞頻(若是爲基礎需求,默認的權重爲1) if (line.indexOf("title:") == 0) { weight = signal.getwValue(); line = line.replaceFirst("title:", ""); } if (line.indexOf("abstract:") == 0) { weight = 1; line = line.replaceFirst("abstract:", ""); } splitLine(line); // 將每一行進行拆分處理 } } catch (IOException e) { e.printStackTrace(); } } // 判斷一個單詞是不是題目要求的合法單詞 private boolean isWord(String s) { return s.length() >= 4 && Character.isLetter(s.charAt(0)) && Character.isLetter(s.charAt(1)) && Character.isLetter(s.charAt(2)) && Character.isLetter(s.charAt(3)); } private void splitLine(String line) { if(line.isEmpty()){ return; } // 判斷首字母是否爲字母,用於後續拼接詞組時,將單詞與分隔符正確拼接 if(Character.isLetterOrDigit(line.charAt(0))) { deviation = -1; } else { deviation = 0; } // 正則匹配單詞 Pattern r = Pattern.compile("[a-z0-9]+"); Matcher m = r.matcher(line); while (m.find()){ arrayList.add(m.group(0)); // 將單詞加入arrayList待後續組詞使用 if (isWord(m.group(0))) { wordNumber += 1; isLegalWord.add(true); // 若是是合法單詞,就把對應位置爲true } else{ isLegalWord.add(false); } } // 正則匹配分隔符 Pattern r2 = Pattern.compile("[^a-z0-9]+"); Matcher m2 = r2.matcher(line); while (m2.find()){ separator.add(m2.group(0)); } setMap(); // 拼接單詞,組成長度爲m的詞組 // 清空本行的內容,等待處理下一行 arrayList.clear(); separator.clear(); word.clear(); isLegalWord.clear(); } // 將單詞拼接爲長度爲m的詞組,並統計詞組詞頻 private void setMap() { combineWord(); for (String aWord : word) { if (map.containsKey(aWord)) { map.put(aWord, map.get(aWord) + weight); } else { map.put(aWord, weight); } } } // 將單詞拼接爲長度爲m的詞組 private void combineWord() { for (int i = 0; i < arrayList.size() - signal.getmValue() + 1; i++) { boolean flag = true; // 判斷從i開始的m個單詞是否均爲合法單詞,如果,則可組成一個長度爲m的詞組 for (int j = 0; j < signal.getmValue() && flag; j++) { if (!isLegalWord.get(i+j)) { flag = false; } } if (flag) { // 若是可拼接成長度爲m的詞組,則將這m個單詞與其中的分隔符進行拼接 StringBuilder s = new StringBuilder(arrayList.get(i)); for(int j = 1; j < signal.getmValue(); j++) { s.append(separator.get(i + j + deviation)).append(arrayList.get(i + j)); } word.add(s.toString()); } } }
爬蟲使用的是JAVA實現,因爲對於一些爬蟲框架並不熟悉,因此使用的是JAVA原生的URL類請求網頁內容,使用正則進行數據匹配。代碼以下:
// 封裝的請求函數,用於發起請求並返回頁面HTML內容 private static String getHtmlContent(String uri) { StringBuilder result = new StringBuilder(); try { String baseURL = "http://openaccess.thecvf.com"; URL url = new URL(baseURL + "/" + uri); URLConnection connection = url.openConnection(); connection.connect(); BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result.append(line); } } catch (IOException e) { e.printStackTrace(); } return String.valueOf(result); } public static void main(String[] args) { // 加載論文類型的 CSV 表格 // 因爲未在官網上找到論文類型,所以使用在Github(https://github.com/amusi/daily-paper-computer-vision/blob/master/2018/cvpr2018-paper-list.csv)上其餘人蒐集好的數據 Map<String, String> map = new HashMap<>(); try (BufferedReader bufferedReader = new BufferedReader(new FileReader(new File("cvpr/cvpr2018-paper-list.csv")))) { String line; // 逐行讀取CSV表格,並切割獲得其論文類型及論文名,並以論文名爲鍵,論文類型爲值,生成一個HashMap while ((line = bufferedReader.readLine()) != null) { String[] list = line.split(","); map.put(list[2].toLowerCase(), list[1]); } } catch (IOException e) { e.printStackTrace(); } // 獲取CVPR官網首頁內容 String result = getHtmlContent("CVPR2018.py"); // 獲取到首頁後,使用正則匹配,獲得論文詳細內容的連接,並逐個發起請求,繼續使用正則匹配詳情頁,獲得論文題目、摘要、做者信息等內容 /* 首頁HTML樣式 <dt class="ptitle"><br><a href="content_cvpr_2018/html/Das_Embodied_Question_Answering_CVPR_2018_paper.html">Embodied Question Answering</a></dt> */ /* 詳情頁HTML樣式 <div id="papertitle">title</div><div id="authors"><br><b><i>authors</i></b>; where</div><font size="5"><br><b>Abstract</b></font><br><br><div id="abstract" >abstract</div><font size="5"><br><b>Related Material</b></font><br><br>[<a href="url">pdf</a>] */ String pattern = "<dt class=\"ptitle\"><br><a href=\"(.*?)\">(.*?)</a></dt>"; String pattern2 = "<div id=\"papertitle\">(.*?)</div><div id=\"authors\"><br><b><i>(.*?)</i></b>; (.*?)</div><font size=\"5\"><br><b>Abstract</b></font><br><br><div id=\"abstract\" >(.*?)</div><font size=\"5\"><br><b>Related Material</b></font><br><br>\\[<a href=\"(.*?)\">pdf</a>]"; Pattern r = Pattern.compile(pattern); Pattern r2 = Pattern.compile(pattern2); Matcher m = r.matcher(result); StringBuilder res = new StringBuilder(); int i = 0; while (m.find()) { String html = getHtmlContent(m.group(1)); System.out.println(i); System.out.println("URL: " + m.group(1)); Matcher m2 = r2.matcher(html); if (m2.find()) { // 輸出詳細內容 System.out.println("Title: " + m2.group(1)); System.out.println("authors: " + m2.group(2)); System.out.println("Type: " + map.get(m2.group(1).split(",")[0].toLowerCase())); System.out.println("Where: " + m2.group(3)); System.out.println("Abstract: " + m2.group(4)); System.out.println("PDF: " + m2.group(5).replace("../../", "http://openaccess.thecvf.com/")); res.append(i).append("\r\n"); // res.append("Type: ").append(map.get(m2.group(1).split(",")[0].toLowerCase())).append("\r\n"); res.append("Title: ").append(m2.group(1)).append("\r\n"); // res.append("Authors: ").append(m2.group(2)).append("\r\n"); res.append("Abstract: ").append(m2.group(4)).append("\r\n"); // res.append("PDF: ").append(m2.group(5).replace("../../", "http://openaccess.thecvf.com/")).append("\r\n\r\n\r\n"); } System.out.println(); System.out.println(); i++; } // 將內容寫入文件 try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(new File("cvpr/result.txt")))) { bufferedWriter.write(String.valueOf(res)); } catch (IOException e) { e.printStackTrace(); } }
爬蟲結果展現:
使用 JUnit4
進行單元測試,測試了詞頻統計部分主要的四個函數,即:
WordCount.getCharacterNumber()
:字符數統計WordCount.getLineNumber()
:行數統計WordCount.getWordNumber()
:單詞數統計WordCount.getList()
:詞頻統計測試數據集爲十個文本,分別爲:空文本、全爲空行的文本、字母數字分隔符隨機交替出現的文本、混有空行的文本、混有非單詞的文本、正常的單詞文本等。
單元測試結果及代碼覆蓋率以下圖:
覆蓋率中,WordCount
類因爲存在比較多的異常處理分支,故覆蓋率只有86%,而命令行處理類 Signal
類因爲在單元測試時未指定全部參數,故覆蓋率也較低。
測試數據: file123&&&file123 123file aaa AAA aaaa AAAA AaAa 測試結果: charactors: 49 words: 5 lines: 7 <file123&&&file123>: 1
測試數據: as!@#$!@$rwehhhkk***---===++==++ \t\n \r\n mmm&^&&&^^^ 測試結果: charactors: 38 words: 3 lines: 3 <aaaa>: 1 <fefeffffff>: 1 <file1daa>: 1
測試數據: abcdefghijklmnopqrstuvwxyz 1234567890 ,./;'[]\<>?:"{}|`-=~!@#$%^&*()_+ 測試結果: charactors: 76 words: 1 lines: 3 <abcdefghijklmnopqrstuvwxyz>: 1
查閱網上博客及相關教程
正則相關已解決,爬蟲框架因爲時間問題沒有深刻了解
對於正則表達式,有了進一步的認識,同時對於爬蟲相關的東西也有了初步的印象,後續有時間可多多接觸下。
個人隊友很nice,思路清晰,代碼能力也不錯
從網站綜合爬取論文的除題目、摘要外其餘信息。
因爲未在官網上找到論文類型,所以使用在 Github 上其餘人蒐集好的數據。最終結果以下:
實現了論文做者的關係圖。關係圖使用 ECharts
框架實現可視化效果。數據來自爬蟲獲得的論文數據集,使用Java將原始數據生成ECharts所須要的XML數據格式。HTML代碼以下:
<html> <head> <meta charset="utf-8"> <script src="echarts.js"></script> <script src="dataTool.min.js"></script> <script src="jquery-3.3.1.js"></script> </head> <body> <div id="main" style="width: 100%;height:110%;"></div> <script type="text/javascript"> var myChart = echarts.init(document.getElementById('main')); myChart.showLoading(); $.get('cvpr_authors.gexf', function (xml) { myChart.hideLoading(); var graph = echarts.dataTool.gexf.parse(xml); var categories = []; for (var i = 0; i < 30; i++) { categories[i] = { name: '' + i }; } graph.nodes.forEach(function (node) { node.itemStyle = null; node.value = node.symbolSize; node.symbolSize *= 1; node.label = { normal: { show: node.symbolSize > 30 } }; node.category = node.attributes.modularity_class; }); option = { title: { text: '做者關係圖', subtext: 'Default layout', top: 'bottom', left: 'right' }, tooltip: {}, legend: [{ data: categories.map(function (a) { return a.name; }) }], animationDuration: 1500, animationEasingUpdate: 'quinticInOut', series : [ { name: '論文數', type: 'graph', layout: 'circular', circular: { rotateLabel: true }, data: graph.nodes, links: graph.links, categories: categories, roam: true, focusNodeAdjacency: true, itemStyle: { normal: { borderColor: '#fff', borderWidth: 1, shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.3)' } }, label: { position: 'right', formatter: '{b}' }, lineStyle: { color: 'source', curveness: 0.3 }, emphasis: { lineStyle: { width: 5 } } } ] }; myChart.setOption(option); }, 'xml'); </script> </body> </html>
效果圖以下:
因爲做者間關係過於複雜,全部數據生成的圖會過密。
篩選部分可得下圖:
鼠標移動到某個點上可查看這個做者的論文數及與其餘做者的關係: