課程: 軟件工程1916|W(福州大學)
做業要求: 結對第二次—文獻摘要熱詞統計及進階需求
結對成員:131601207 陳序展、221600440 鄭曉彪
本次做業目標: 在實現對文本文件中的單詞的詞頻進行統計的控制檯程序的基礎上,編程實現頂會熱詞統計器
項目:Github地址java
GitHub地址:PairProject1-Javagit
GitHub代碼簽入記錄
github
解題思路正則表達式
實現過程編程
總體想法流程圖
windows
具體類圖
session
@SuppressWarnings("resource") public String FiletoText() throws IOException { InputStream is = new FileInputStream(filePath); int char_type; // 用來保存每行讀取的內容 BufferedReader reader = new BufferedReader(new InputStreamReader(is)); while ((char_type = reader.read()) != -1) { // 若是 line 爲空說明讀完了 sb.append((char) char_type); // 將讀到的內容添加到 buffer 中 } return sb.toString(); }
@SuppressWarnings("resource") public void lineCount() throws IOException { BufferedReader br = new BufferedReader(new FileReader(filePath)); String readline; while ((readline = br.readLine()) != null) { readline = readline.trim();// 去除空白行 if (readline.length() != 0) lineCnt++; } }
public void charCount() { String charRegex = "[\\x00-\\x7F]";// [\p{ASCII}] Pattern p = Pattern.compile(charRegex); Matcher m = p.matcher(text); while (m.find()) { charCnt++; } }
public static <K extends Comparable<? super K>, V extends Comparable<? super V>> Map<K, V> sortMap(Map<K, V> map) { List<Map.Entry<K, V>> list = new LinkedList<Map.Entry<K, V>>(map.entrySet()); Collections.sort(list, new Comparator<Map.Entry<K, V>>() { public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) { int re = o2.getValue().compareTo(o1.getValue()); if (re != 0) return re; else return o1.getKey().compareTo(o2.getKey()); } }); Map<K, V> result = new LinkedHashMap<K, V>(); for (Map.Entry<K, V> entry : list) { result.put(entry.getKey(), entry.getValue()); } return result; }
Map<String, Integer> wordCount() { String lowerText = text.toLowerCase(); String splitRegex = "[^a-z0-9]";// 分隔符 lowerText = lowerText.replaceAll(splitRegex, " ");// 將非字母數字替換爲空格 String words[] = lowerText.split("\\s+");// 利用空白分割全部單詞 String wordRegex = "[a-z]{4,}[a-z0-9]*";// 單詞匹配正則表達式 for (int i = 0; i < words.length; i++) { Pattern p = Pattern.compile(wordRegex); Matcher m = p.matcher(words[i]); if (m.find()) {// 符合單詞定義 wordCnt++; Integer num = map.get(words[i]); if (num == null || num == 0) { map.put(words[i], 1); // map中無該單詞,數量置1 } else if (num > 0) { map.put(words[i], num + 1); // map中有該單詞,數量加1 } } } map = sortMap(map); return map; }
單元測試app
利用包括助教所給的兩個用例(input1.txt、input2.txt)以及一個空文件(input3.txt)等近10個測試文件對代碼進行了簡單的測試
測試數據主要測試特殊定義單詞格式(單詞定義:至少以4個英文字母開頭,跟上字母數字符號,單詞以分隔符分割,不區分大小寫)是否能正確匹配並統計以及測試處理一些空白符、換行符、空行等,如下給出部分測試數據:eclipse
a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 aaa0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 0aa0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 00a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0 aaaaa0a0a0a0a0a0a0a0a0a0a0a0a0a0 aaaa00a0a0a0a0a0a0a0a0a0a0a0a0a0
NOT_EMPTY_LINE
利用JUnit將測試數據一塊兒進行字符統計、行數統計、單詞數統計單元測試後結果以下
另,對單詞字典序輸出過程未進行單元測試,如下給出字典序測試數據及運行結果函數
windows2000 windows8 windows7 win7 windows2000 windows2000 windows2000 windows2000 windows2000 windows2000 windows95 windows98 windows98 windows95 windows98 windows98 windows95 windows95 windows9 windows9 windows9 windows9
characters: 224 words: 21 lines: 22 <windows2000>: 7 <windows9>: 4 <windows95>: 4 <windows98>: 4 <windows7>: 1 <windows8>: 1
import static org.junit.Assert.*; import java.io.IOException; import org.junit.AfterClass; import org.junit.BeforeClass; public class Test { String files[]= {"soursefile\\input1.txt","soursefile\\input2.txt","soursefile\\input3.txt", "soursefile\\input5.txt","soursefile\\input6.txt","soursefile\\input7.txt", "soursefile\\input8.txt","soursefile\\input9.txt","soursefile\\input10.txt"}; int lines[]= {2,3,0,6,1,1,7,22,20}; int chars[]= {102,76,0,197,40,36,358,224,99}; int words[]= {2,1,0,2,2,0,14,21,20}; @BeforeClass public static void setUpBeforeClass() { System.out.println("開始測試..."); } @AfterClass public static void tearDownAfterClass() { System.out.println("測試結束..."); } @org.junit.Test public void lineCountTest() throws IOException { for(int i=0;i<files.length;i++) { FileUtil fileutil=new FileUtil(files[i]); fileutil.lineCount(); assertEquals(lines[i],fileutil.getLineCnt()); } } @org.junit.Test public void TestCharCount() throws IOException { for(int i=0;i<files.length;i++) { FileUtil fileutil=new FileUtil(files[i]); Counter c=new Counter(fileutil.FiletoText()); c.charCount(); assertEquals(chars[i],c.getCharCnt()); } } @org.junit.Test public void TestWordCount() throws IOException { for(int i=0;i<files.length;i++) { FileUtil fileutil=new FileUtil(files[i]); Counter c=new Counter(fileutil.FiletoText()); c.wordCount(); assertEquals(words[i],c.getWordCnt()); } } }
性能分析
GitHub地址:PairProject2-Java
GitHub代碼簽入記錄
解題思路
爬蟲工具使用了jsoup,jsoup 是一款Java 的HTML解析器,可直接解析某個URL地址、HTML文本內容。觀察CVPR2018官網的頁面元素,發現論文的連接都在ptitle類下
經過選擇器獲得對應的ptitle類的Elements列表,在進一步經過選擇器獲得具備href屬性的a標籤Elements列表,再鏈接列表中每一個a標籤對應的href對應的url地址,經過選擇器分別選擇論文的Title和Abstract
最後按格式輸出到result.txt文件中
一、在屬性名前加 abs: 前綴。這樣就能夠返回包含根路徑的URL地址attr("abs:href")
二、在剛開始爬取的時候,一直不能爬取所有的論文列表,後來經過向同窗請教得知,jsoup最大獲取的響應長度正好是1M。只要設置 connection.maxBodySize(0),設置爲0,就能夠獲得不限響應長度的數據了。
代碼說明
public static void main(String[] args) throws IOException { int cnt = 0; String fileName = "result.txt"; String url = "http://openaccess.thecvf.com/CVPR2018.py"; File resultFile = new File(fileName); resultFile.createNewFile(); BufferedWriter out = new BufferedWriter(new FileWriter(resultFile)); Connection connection = Jsoup.connect(url).ignoreContentType(true); connection.timeout(2000000); connection.maxBodySize(0); // jsoup最大獲取的響應長度正好是1M。只要設置 connection.maxBodySize(0),設置爲0,就能夠獲得不限響應長度的數據了。 Document document = connection.get(); Elements ptitle = document.select(".ptitle"); // 經過選擇器獲得類ptitle的Elements列表 Elements links = ptitle.select("a[href]"); // 經過選擇器進一步獲得具備href屬性的a標籤Elements列表 for (Element link : links) { out.write(cnt + "\r\n"); cnt++; String eachUrl = link.attr("abs:href"); // 在屬性名前加 abs: 前綴。這樣就能夠返回包含根路徑的URL地址attr("abs:href") Connection eachConnection = Jsoup.connect(eachUrl).ignoreContentType(true); eachConnection.timeout(2000000); eachConnection.maxBodySize(0); // jsoup最大獲取的響應長度正好是1M。只要設置 connection.maxBodySize(0),設置爲0,就能夠獲得不限響應長度的數據了。 Document eachDocument = eachConnection.get(); Elements eachTitle = eachDocument.select("#papertitle"); // 在文章中經過選擇器找到Title String paperTitle = eachTitle.text(); out.write("Title: " + paperTitle + "\r\n"); Elements eachAbstract = eachDocument.select("#abstract"); // 在文章中經過選擇器找到Abstract String paperAbstract = eachAbstract.text(); out.write("Abstract: " + paperAbstract + "\r\n"); out.write("\r\n\r\n"); out.flush(); } out.close(); // 關閉文件 }
新增功能,並在命令行程序中支持下述命令行參數,且可多參數混合使用
解題思路
接口封裝
在基礎部分的設計中,已經將主要操做封裝成了FileUtil類和Counter類,在進階提出多參數的要求,咱們認爲多封裝一個對參數的解析類,對於文本過濾和統計的修改實際上是很少的
實現過程
總體想法流程圖
在基礎部分的總體流程中多添加了一個用於解析命令行參數的過程
具體類圖
public void analyse() { for (int i = 0; i < args.length; i++) { if (args[i].equals("-i")) inputFilePath = args[i + 1]; else if (args[i].equals("-o")) outputFilePath = args[i + 1]; else if (args[i].equals("-w")) weight = Integer.parseInt(args[i + 1]); else if (args[i].equals("-m")) { if(Integer.parseInt(args[i+1])>=0) phraseSize = Integer.parseInt(args[i + 1]); else System.out.println("-m參數應爲天然數,默認進行單詞統計"); } else if (args[i].equals("-n")) if(Integer.parseInt(args[i+1])>=0) resultCnt = Integer.parseInt(args[i + 1]); else System.out.println("-n參數應爲天然數,默認輸出前十位數據"); } }
@SuppressWarnings("resource") public String getTitleText() throws IOException { StringBuffer sb = new StringBuffer(); BufferedReader br = new BufferedReader(new FileReader(filePath)); String readtext; while ((readtext = br.readLine()) != null) { if (readtext.contains("Title: ")) {//提取Title行 lineCnt++; readtext = readtext.substring(7);//剔除"Title: " sb.append(readtext + "\r\n");//補上readLine缺乏的換行 } } return sb.toString(); } @SuppressWarnings("resource") public String getAbstractText() throws IOException { StringBuffer sb = new StringBuffer(); BufferedReader br = new BufferedReader(new FileReader(filePath)); String readtext; while ((readtext = br.readLine()) != null) { if (readtext.contains("Abstract: ")) {//提取Abstract行 lineCnt++; readtext = readtext.substring(10);//剔除"Abstract: " sb.append(readtext+ "\r\n");//補上readLine缺乏的換行 } } return sb.toString(); }
public void phraseCount(int size) { String splittext = text.replaceAll("[a-z0-9]", "0");// 將字母數字替換爲0 String splits[] = splittext.split("[0]+");// 剔除0,獲得單詞跟着的分隔符 String splitRegex = "[^a-z0-9]";// 分隔符 String lowerText = text.replaceAll(splitRegex, " ");// 將非字母數字替換爲空格 String words[] = lowerText.split("\\s+");// 利用空白分割全部單詞 String wordRegex = "[a-z]{4,}[a-z0-9]*";// 單詞匹配正則表達式 for (int i = 0; i < words.length; i++) { boolean canPhrase = true; if (i + size <= words.length) {//當前單詞的第後size個單詞不超過單詞總數 for (int j = i; j < i + size; j++) { if (!Pattern.matches(wordRegex, words[j])) {//單詞的後size個單詞均要符合單詞定義 canPhrase = false; break; } } for (int k = i + 1; k < i + size; k++) { if (Pattern.matches("\n", splits[k])) {//不一樣篇論文的title與abstract不能組成詞組,用回車符區分 canPhrase = false; } } } else canPhrase = false; if (canPhrase) { String phrase = new String(); for (int m = 0; m < size; m++) { int pos = i + m; if (m == size - 1) phrase += words[pos]; else phrase += (words[pos] + splits[pos + 1]); } Integer num = phraseMap.get(phrase); if (num == null || num == 0) { phraseMap.put(phrase, 1); } else if (num > 0) { phraseMap.put(phrase, num + 1); } } } }
// 合併map,value值疊加 public void mergeMap(Map<String, Integer> map) { Set<String> set = map.keySet(); for (String key : set) { if (weightMap.containsKey(key)) { weightMap.put(key, weightMap.get(key) + map.get(key)); } else { weightMap.put(key, map.get(key)); } } weightMap = sortMap(weightMap); }
public void weightCount(int weight, String type, int Size) { if (Size == 1) {// 單詞詞頻計算 if (weight == 1) { if (type.equals("title")) { for (Map.Entry<String, Integer> word : cntMap.entrySet()) { weightMap.put(word.getKey(), word.getValue() * 10); } } if (type.equals("abstract")) { for (Map.Entry<String, Integer> word : cntMap.entrySet()) { weightMap.put(word.getKey(), word.getValue()); } } } else if (weight == 0) { for (Map.Entry<String, Integer> word : cntMap.entrySet()) { weightMap.put(word.getKey(), word.getValue()); } } else { System.out.println("w參數只能與數字 0|1 搭配使用"); } } else {// 詞組詞頻計算 if (weight == 1) { if (type.equals("title")) { for (Map.Entry<String, Integer> phrase : phraseMap.entrySet()) { weightMap.put(phrase.getKey(), phrase.getValue() * 10); } } if (type.equals("abstract")) { for (Map.Entry<String, Integer> phrase : phraseMap.entrySet()) { weightMap.put(phrase.getKey(), phrase.getValue()); } } } else if (weight == 0) { for (Map.Entry<String, Integer> phrase : phraseMap.entrySet()) { weightMap.put(phrase.getKey(), phrase.getValue()); } } else { System.out.println("w參數只能與數字 0|1 搭配使用"); } } }
測試部分
對進階部分的測試未能完善,字符單詞統計處理要求過於精細,時間緣由還未細測,詞組詞頻測試對爬蟲爬取的978篇結果(d:\result.txt)進行處理,數據量較多,不知結果正確與否,未進行JUnit白盒測試,只貼出統計結果(測試文件與輸出文件均存放於d:盤中)
在命令行窗口中輸入:java Main -i d:\result -o d:\output.txt -w 1 -m 3
d:盤下output.txt中詞組統計及詞頻輸出結果以下:
性能測試
如下測試結果使用工具JProfiler爬取獲得的2018年CVPR論文數據,加入了參數-w 1
-m 3
獲得,可見在Counter中的map排序sortMap還有mergeMap方法消耗最大
實現功能
設計思路
成果展現
result.txt
做者聯繫
關鍵詞圖譜
Top10單詞柱狀圖
具體分工
實際過程當中,我與結對夥伴劃分各自的工做,但卻並不是各作各的,在過程當中的"領航者"與「駕駛員」身份時常互換,相互幫助。一開始困惑不少,完成基礎部分的時候,本不打算繼續完善進階甚至作附加任務,由於時間安排不合理,以爲作不來也沒法作好,不過兩人仍是互相攙扶着完成結對任務,我想這也是結對編程帶來的。
評價隊友
PSP是卡耐基梅隆大學(CMU)的專家們針對軟件工程師所提出的一套模型:Personal Software Process (PSP, 我的開發流程,或稱個體軟件過程)。
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 30 | 20 |
• Estimate | • 估計這個任務須要多少時間 | 30 | 20 |
Development | 開發 | 1160 | 1470 |
• Analysis | • 需求分析 (包括學習新技術) | 90 | 150 |
• Design Spec | • 生成設計文檔 | 40 | 20 |
• Design Review | • 設計複審 | 40 | 20 |
• Coding Standard | • 代碼規範 (爲目前的開發制定合適的規範) | 30 | 20 |
• Design | • 具體設計 | 60 | 90 |
• Coding | • 具體編碼 | 720 | 900 |
• Code Review | • 代碼複審 | 120 | 180 |
• Test | • 測試(自我測試,修改代碼,提交修改) | 60 | 90 |
Reporting | 報告 | 70 | 70 |
• Test Report | • 測試報告 | 30 | 30 |
• Size Measurement | • 計算工做量 | 20 | 20 |
• Postmortem & Process Improvement Plan | • 過後總結, 並提出過程改進計劃 | 20 | 20 |
合計 | 1260 | 1560 |