結對第二次—文獻摘要熱詞統計及進階需求

課程: 軟件工程1916|W(福州大學)
做業要求: 結對第二次—文獻摘要熱詞統計及進階需求
結對成員:131601207 陳序展221600440 鄭曉彪
本次做業目標: 在實現對文本文件中的單詞的詞頻進行統計的控制檯程序的基礎上,編程實現頂會熱詞統計器
項目:Github地址java

目錄

WordCount

(一) WordCount基本需求

GitHub地址:PairProject1-Javagit

GitHub代碼簽入記錄
github

解題思路正則表達式

  • 實現功能
  1. 統計文件的字符數:利用正則表達式「\p{ASCII}」或「[\x00-\x7F]」匹配ascii碼
  2. 統計文件的單詞總數:一樣利用正則表達式對特殊定義的單詞進行匹配
  3. 統計文件的有效行數:在文件讀入過程當中進行統計
  4. 統計文件中各單詞的出現次數,最終只輸出頻率最高的10個,頻率相同的單詞,優先輸出字典序靠前的單詞:利用鍵值對存儲結構分別存儲單詞和其次數,再進行排序輸出
  5. 按照格式輸出:將結果鏈接成字符串,輸出到文件result.txt(當前目錄,通常爲bin文件夾下)
  • 接口封裝
    能夠將文件處理與字符串處理分開:
  1. 編寫FileUtil工具類,經過Main主類傳進來的第一個參數解析對應路徑文件的文本,並在解析過程當中利用java讀取文本文件的readLine( )方法對文本行數進行統計,並最後解析爲一個字符串傳給統計類進行統計;
  2. 接着編寫Counter類,經過FileUtil解析來的文本構造Counter類,在構造函數中利用replaceAll( )方法將換行‘\r\n’換作‘\n’以達到換行符只統計一次,然後編寫類中的charCnt方法進行字符統計;編寫wordCnt方法進行單詞統計處理,單詞的匹配過程能夠經過正則表達式提升效率

實現過程編程

  • 總體想法流程圖
    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++;
          }
      }
    • map按單詞數降序單詞按字典序排序
    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
  • 測試代碼
    分別對lineCount()、CharCount()、WordCount()三個方法進行測試
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());
        }
    }
}
  • 代碼覆蓋率

性能分析

  • 改進思路:考慮到單詞匹配的方便快捷性,本次學習使用了Java中利用正則表達式匹配的方法,並主要利用jdk提供的工具類編寫代碼
    利用工具JProfiler測試助教其中一個樣例後獲得如下分析結果,可見Counter類中的wordCount方法消耗最大,其中不只使用了map的排序還使用了String對象的split方法方便使用正則匹配

(二)WordCount進階需求

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(); // 關閉文件
    }

WordCount命令行多參數部分

新增功能,並在命令行程序中支持下述命令行參數,且可多參數混合使用

  1. -i 參數設定讀入文件的存儲路徑
  2. -o 參數設定生成文件的存儲路徑
  3. -w 參數設定是否採用不一樣權重計數:加入權重詞頻統計,屬於Title的單詞權重爲10,屬於Abstract 單詞權重爲1
  4. -m 參數設定統計的詞組長度:統計文件夾中指定長度的詞組的詞頻
  5. -n 參數設定輸出的單詞數量:用戶指定輸出前 n 多的單詞(詞組)與其頻數

解題思路

  • 命令行多參數:增長命令行參數的分析類,提取出用戶要求的對應操做信息
  • 不一樣權重:將基礎需求中的文本解析類FileUtil中加入title文本提取方法和abstract文本提取方法,分紅兩部分進入Conuter中統計,以區分權重的異同
  • 詞組:一樣將文本解析爲標題和摘要兩種字符串,對每一個串提取單詞和分隔符,判斷連續m個單詞是否均符合要求,符合則將單詞與分隔符連成詞組,再存儲、統計
  • 單詞、詞組詞頻統計:將單詞或是詞組存爲對應的Map,再根據傳入的m參數以及n參數,進行權重計算和詞頻輸出(這裏還須要merge標題和摘要統計下來的兩個Map)

接口封裝
在基礎部分的設計中,已經將主要操做封裝成了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參數應爲天然數,默認輸出前十位數據");
          }
    }
    • 文件內容轉爲字符串過程在進階需求中分紅兩部分,分別提取Title和Abstract
    @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合併
    // 合併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方法消耗最大

(三)附加題設計與展現

實現功能

  1. 從網站爬取了論文除題目摘要外的其餘信息,如做者、pdf地址等
  2. 分析論文列表中的做者關係,進行可視化處理
  3. 對爬取的摘要數據,生成關鍵詞圖譜
  4. 詞頻分析可視化

設計思路

  1. 從網站爬取論文的其餘信息,如做者、pdf地址等的方法與以前爬取論文題目,摘要的方法相似,主要是經過找到對應結點,獲得對應結點的文本值。
  2. 分析論文列表中各位做者之間的關係,圖形化顯示,主要是先爬取每篇論文的做者信息,因爲爬取的做者信息以 ,分隔,所以能夠直接將其轉化爲csv文件,經過使用Gephi導入csv文件,生成可視化的聯繫
  3. 關鍵詞圖譜部分經過將爬取的摘要內容放在在線生成詞雲的網站生成可視化詞雲
  4. 論文摘要中出現的詞頻統計,主要是先爬取每篇論文的摘要信息,再經過咱們寫的WordCount進階需求程序進行詞頻統計,將出現頻率最高的前十個單詞經過Excel轉化爲柱狀圖顯示

成果展現
result.txt


做者聯繫

關鍵詞圖譜

Top10單詞柱狀圖

結對過程

具體分工

實際過程當中,我與結對夥伴劃分各自的工做,但卻並不是各作各的,在過程當中的"領航者"與「駕駛員」身份時常互換,相互幫助。一開始困惑不少,完成基礎部分的時候,本不打算繼續完善進階甚至作附加任務,由於時間安排不合理,以爲作不來也沒法作好,不過兩人仍是互相攙扶着完成結對任務,我想這也是結對編程帶來的。

  • 鄭曉彪:WordCount編寫,單元測試,編寫文檔
  • 陳序展:爬蟲部分及附加功能編寫,性能測試,代碼質量分析,覆蓋率監測,部分文檔編寫

評價隊友

  • 值得學習的地方:個人隊友認真負責,處理任務目標明確、條理清晰,學習能力強
  • 值得改進的地方:實際工做時效率有待提升

PSP

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

參考資料

  1. JAVA中正則表達式匹配,替換,查找,切割的方法
  2. Java - 正則表達式的運用(Pattern模式和Matcher匹配)
  3. java 對HashMap 進行排序,優先值value排序,若value相同時對鍵KEY按字母表順序排序
  4. eclipse代碼的紅綠黃背景顏色——利用 Coverage 查看代碼的 session覆蓋率 和 決策分支執行覆蓋狀況
  5. jsoup開發指南,jsoup中文使用手冊,jsoup中文文檔
  6. Gephi 中文教程 | Udemy
相關文章
相關標籤/搜索