**課程: ** 軟體工程1916|W(福州大學)
**作業要求: ** 結對第二次—文獻摘要熱詞統計及進階需求
結對成員:131601207 陳序展、221600440 鄭曉彪
**本次作業目标: **在實作對文本檔案中的單詞的詞頻進行統計的控制台程式的基礎上,程式設計實作頂會熱詞統計器
項目:Github位址
目錄
- (一)WordCount基本需求
- (二)WordCount進階需求
- 爬蟲部分
- WordCount指令行多參數部分
- (三)附加題設計與展示
- 結對過程
- PSP
- 參考資料
WordCount
(一) WordCount基本需求
GitHub位址:PairProject1-Java
GitHub代碼簽入記錄
解題思路
- 實作功能
- 統計檔案的字元數:利用正規表達式“\p{ASCII}”或“[\x00-\x7F]”比對ascii碼
- 統計檔案的單詞總數:同樣利用正規表達式對特殊定義的單詞進行比對
- 統計檔案的有效行數:在檔案讀入過程中進行統計
- 統計檔案中各單詞的出現次數,最終隻輸出頻率最高的10個,頻率相同的單詞,優先輸出字典序靠前的單詞:利用鍵值對存儲結構分别存儲單詞和其次數,再進行排序輸出
- 按照格式輸出:将結果連接配接成字元串,輸出到檔案result.txt(目前目錄,一般為bin檔案夾下)
-
接口封裝
可以将檔案處理與字元串處理分開:
- 編寫FileUtil工具類,通過Main主類傳進來的第一個參數解析對應路徑檔案的文本,并在解析過程中利用java讀取文本檔案的readLine( )方法對文本行數進行統計,并最後解析為一個字元串傳給統計類進行統計;
- 接着編寫Counter類,通過FileUtil解析來的文本構造Counter類,在構造函數中利用replaceAll( )方法将換行‘\r\n’換做‘\n’以達到換行符隻統計一次,而後編寫類中的charCnt方法進行字元統計;編寫wordCnt方法進行單詞統計處理,單詞的比對過程可以通過正規表達式提高效率
實作過程
- 整體想法流程圖
- 具體類圖
- 代碼說明
- 檔案内容轉字元串
@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;
}
- 單元測試
利用包括助教所給的兩個用例(input1.txt、input2.txt)以及一個空檔案(input3.txt)等近10個測試檔案對代碼進行了簡單的測試
測試資料主要測試特殊定義單詞格式(單詞定義:至少以4個英文字母開頭,跟上字母數字元号,單詞以分隔符分割,不區分大小寫)是否能正确比對并統計以及測試處理一些空白符、換行符、空行等,以下給出部分測試資料:
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
爬蟲工具使用了jsoup,jsoup 是一款Java 的HTML解析器,可直接解析某個URL位址、HTML文本内容。觀察CVPR2018官網的頁面元素,發現論文的連結都在ptitle類下
通過選擇器得到對應的ptitle類的Elements清單,在進一步通過選擇器得到具有href屬性的a标簽Elements清單,再連接配接清單中每個a标簽對應的href對應的url位址,通過選擇器分别選擇論文的Title和Abstract
最後按格式輸出到result.txt檔案中
1、在屬性名前加 abs: 字首。這樣就可以傳回包含根路徑的URL位址attr("abs:href")
2、在剛開始爬取的時候,一直不能爬取全部的論文清單,後來通過向同學請教得知,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(); // 關閉檔案
}
新增功能,并在指令行程式中支援下述指令行參數,且可多參數混合使用
- -i 參數設定讀入檔案的存儲路徑
- -o 參數設定生成檔案的存儲路徑
- -w 參數設定是否采用不同權重計數:加入權重詞頻統計,屬于Title的單詞權重為10,屬于Abstract 單詞權重為1
- -m 參數設定統計的詞組長度:統計檔案夾中指定長度的詞組的詞頻
- -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方法消耗最大
(三)附加題設計與展示
- 從網站爬取了論文除題目摘要外的其他資訊,如作者、pdf位址等
- 分析論文清單中的作者關系,進行可視化處理
- 對爬取的摘要資料,生成關鍵詞圖譜
- 詞頻分析可視化
設計思路
- 從網站爬取論文的其他資訊,如作者、pdf位址等的方法與之前爬取論文題目,摘要的方法類似,主要是通過找到對應結點,得到對應結點的文本值。
- 分析論文清單中各位作者之間的關系,圖形化顯示,主要是先爬取每篇論文的作者資訊,由于爬取的作者資訊以 ,分隔,是以可以直接将其轉化為csv檔案,通過使用Gephi導入csv檔案,生成可視化的聯系
- 關鍵詞圖譜部分通過将爬取的摘要内容放在線上生成詞雲的網站生成可視化詞雲
- 論文摘要中出現的詞頻統計,主要是先爬取每篇論文的摘要資訊,再通過我們寫的WordCount進階需求程式進行詞頻統計,将出現頻率最高的前十個單詞通過Excel轉化為柱狀圖顯示
成果展示
result.txt
作者聯系
關鍵詞圖譜
Top10單詞柱狀圖
具體分工
實際過程中,我與結對夥伴劃分各自的工作,但卻并非各做各的,在過程中的"領航者"與“駕駛員”身份時常互換,互相幫助。一開始困惑很多,完成基礎部分的時候,本不打算繼續完善進階甚至做附加任務,因為時間安排不合理,覺得做不來也無法做好,不過兩人還是互相攙扶着完成結對任務,我想這也是結對程式設計帶來的。
- 鄭曉彪:WordCount編寫,單元測試,編寫文檔
- 陳序展:爬蟲部分及附加功能編寫,性能測試,代碼品質分析,覆寫率監測,部分文檔編寫
評價隊友
- 值得學習的地方:我的隊友認真負責,處理任務目标明确、條理清晰,學習能力強
- 值得改進的地方:實際工作時效率有待提高
PSP是卡耐基梅隆大學(CMU)的專家們針對軟體工程師所提出的一套模型:Personal Software Process (PSP, 個人開發流程,或稱個體軟體過程)。
PSP2.1 | Personal Software Process Stages | 預估耗時(分鐘) | 實際耗時(分鐘) |
---|---|---|---|
Planning | 計劃 | 30 | 20 |
• Estimate | • 估計這個任務需要多少時間 | ||
Development | 開發 | 1160 | 1470 |
• Analysis | • 需求分析 (包括學習新技術) | 90 | 150 |
• Design Spec | • 生成設計文檔 | 40 | |
• Design Review | • 設計複審 | ||
• Coding Standard | • 代碼規範 (為目前的開發制定合适的規範) | ||
• Design | • 具體設計 | 60 | |
• Coding | • 具體編碼 | 720 | 900 |
• Code Review | • 代碼複審 | 120 | 180 |
• Test | • 測試(自我測試,修改代碼,送出修改) | ||
Reporting | 報告 | 70 | |
• Test Report | • 測試報告 | ||
• Size Measurement | • 計算工作量 | ||
• Postmortem & Process Improvement Plan | • 事後總結, 并提出過程改進計劃 | ||
合計 | 1260 | 1560 |
- JAVA中正規表達式比對,替換,查找,切割的方法
- Java - 正規表達式的運用(Pattern模式和Matcher比對)
- java 對HashMap 進行排序,優先值value排序,若value相同時對鍵KEY按字母表順序排序
- eclipse代碼的紅綠黃背景顔色——利用 Coverage 檢視代碼的 session覆寫率 和 決策分支執行覆寫情況
- jsoup開發指南,jsoup中文使用手冊,jsoup中文文檔
- Gephi 中文教程 | Udemy