软工实践第五次作业-结对作业(2)
一、结对信息
- 结对成员
031602209 陈志炜
031602206 陈文垚 - 本次作业博客链接
- Github项目地址
- 作业代码的GitHub地址
- 具体分工
陈志炜 爬取论文信息、完成编码要求2、3、4、6的代码编写、完成附加题
陈文垚 完成编码要求5的代码编写、单元测试、性能分析
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 940 | 1420 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 180 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 | 60 | 180 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 240 | 240 |
· Coding | · 具体编码 | 360 | 540 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 180 |
Reporting | 报告 | 100 | 160 |
· Test Repor | · 测试报告 | 60 | 120 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1040 | 1580 |
三、解题思路及设计说明
爬虫(自己完成)
运行环境:Python 3.6.6
使用的库:requests, lxml, bs4
通过分析页面源码可以看出每篇论文的主页都是由
<dt class="ptitle"><br><a href="content_cvpr_2018/html/Das_Embodied_Question_Answering_CVPR_2018_paper.html">Embodied Question Answering</a></dt>
@href 属性 和 http://openaccess.thecvf.com/ 拼接而成的。
所以就先获取所有论文的主页,然后再依次爬取所有的论文的信息,利用 lxml, bs4, 正则 提取出
相应位置的信息。 一开始是做单进程的,完整爬一遍要9分钟多,就改了一个多进程的版本。
全部爬完之后再输出到文件。
def scrape(lock, data, url):
try:
text = get_page(url)
pdf_links, abstracts, authors, titles, booktitles, months, years = data
# print(text)
html = etree.HTML(text)
pdf_link = html.xpath('//a[contains(text(), "pdf")]/@href')[0]
pdf_link = 'http://openaccess.thecvf.com/' + pdf_link[6:]
abstract = html.xpath('//div[@id="abstract"]/text()')[0]
abstract = abstract[1:]
soup = BeautifulSoup(text, 'lxml')
detail = soup.find(class_="bibref")
regex = '(\w+) = {([\S\xa0 ]+)}'
results = re.findall(regex, detail.getText())
author = results[0][1]
title = results[1][1]
booktitle = results[2][1]
month = results[3][1]
year = results[4][1]
lock.acquire()
pdf_links.append(pdf_link)
abstracts.append(abstract)
authors.append(author)
titles.append(title)
booktitles.append(booktitle)
months.append(month)
years.append(year)
lock.release()
except ConnectionError:
print('Error Occured ', url)
finally:
print('URL ', url, ' Scraped')
代码组织与内部实现设计(类图)
根据需求实现了单词/词组的词频统计、加入权重的词频统计、行数统计、单词/词组数统计、字符数
统计、自定义输入输出文件等功能
说明算法的关键与关键实现部分流程图
主要就是提取词组这部分, 要求有不能跨title和abstract,
两个部分的统计方式是相同的,所以就先将title和abstract分别存进List,然后再相同方式处理。
词组处理,就使用分隔符将title或者abstract进行分割,因为词组统计的时候要保留分割符,所以
将所有的分隔符匹配出来备用,词组统计的时候需要分隔符的时候再连接上去。当词组数为m时,需要
连续m个为单词才满足条件。
四、附加题设计与展示
附加题及爬虫python代码实现和爬虫数据戳这里
爬虫
爬虫爬取完毕后会将结果输出到两个文件
一个是只包含Title, abstract
另一个是包含所有能爬取到的信息
with open('result.txt', 'w', encoding='utf-8') as file:
for i in range(len(titles)):
file.write(str(i)+'\n')
file.write('Title: ' + titles[i] + '\n')
file.write('Abstract: ' + abstracts[i] + '\n')
file.write('\n\n')
with open('all_data.txt', 'w', encoding='utf-8') as file:
for i in range(len(titles)):
file.write(str(i)+'\n')
file.write('Title: ' + titles[i] + '\n')
file.write('Authors: ' + authors[i] + '\n')
file.write('Abstract: ' + abstracts[i] + '\n')
file.write('Booktitle: ' + booktitles[i] + '\n')
file.write('PDF Link: ' + pdf_links[i] + '\n')
file.write('Time: ' + years[i] + ' ' + months[i])
file.write('\n\n')
数据分析
render.html下载后可直接打开
以下截图是在Jypyter Notebook中截取
通过python的pyecharts库调用echarts,实现关系图,......,数据太多了...,所以
放大看看..., 论文数发表越多的人的点越大
移动鼠标到要了解的人上面, 与他共同发表过论文的人会亮起
五、关键代码解释
主要部分就是实现词组统计这一块
private HashMap<String, Integer> countContent(List<String> contents, int m) {
HashMap<String, Integer> map = new HashMap<>();
String splitRegex = "[\\s+\\p{Punct}]+";
String splitStartRegex = "^[\\s+\\p{Punct}]+";
String wordRegex = "^[a-zA-Z]{4,}.*";
Pattern pattern = Pattern.compile(splitRegex);
for (String content : contents) {
String[] temp = content.split(splitRegex);
List<String> splits = new ArrayList<>();
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
splits.add(matcher.group());
}
boolean isSplitStart = content.matches(splitStartRegex);
for (int i = 0; i < temp.length - m + 1; i++) {
StringBuilder stringBuilder = new StringBuilder();
if (temp[i].matches(wordRegex)) {
stringBuilder.append(temp[i]);
} else {
continue;
}
boolean isContinue = true;
for (int j = 1; j < m; j++) {
if (!temp[i + j].matches(wordRegex)) {
isContinue = false;
break;
} else {
if (isSplitStart) {
stringBuilder.append(splits.get(i + j));
} else {
stringBuilder.append(splits.get(i + j - 1));
}
stringBuilder.append(temp[i + j]);
}
}
if (isContinue) {
String words = stringBuilder.toString().toLowerCase();
if (!map.containsKey(words)) {
map.put(words, 1);
} else {
int num = map.get(words);
map.put(words, num + 1);
}
}
}
}
return map;
}
一开始有想过通过写正则,直接匹配出符合条件的,emmmm....还是有点难处理。
所以先将句子通过分割符分割,分割出每个词。匹配出所有的分割符(词组统计时候,要求带分隔符连接起来)。
然后直接暴力求解...,判断出符合条件的词组。将结果存入map中,并记录出现的次数。
六、性能分析与改进
测试时使用爬取CVPR的979篇论文作为输入,命令行参数为-i test.txt -n 15 -m 3 -o output.txt,
使用权重统计词组词频,每三个单词为一个词组,输出词频前15的词组存放到output.txt
循环运行100次,性能分析结果如下
代码覆盖率如下
使用VisualVM进行性能分析发现,Main中统计输入文件词组词频的WordsCount.countContent和统
计输入文件单词总数的WordsCount.getWordsSum占用时间最多,占用了90%左右的时间
七、单元测试
在这列出三个单元测试并给出中文注释,所有的单元测试代码可以在github中的test项目中看到。
- 1.对CalMost的单元测试
测试统计前十个单词及频数的函数和统计前n个单词及频数的函数
import org.junit.Test;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
public class CalMostTest {
String path = "input.txt";
HandleContent handleContent = new HandleContent(path);
// -m 词组单词数设为1
// -w 权重设为0
WordsCount wordsCount = new WordsCount(handleContent, 1, 0);
HashMap<String, Integer> map = wordsCount.getMap();
CalMost calMost = new CalMost();
@Test
//无 -n 参数输入时
//单词数超过十个则输出前十个单词及词频
//不足则输出所有单词及词频
public void mostWords() {
List<Map.Entry<String, Integer>> list = calMost.mostWords(map);
list.forEach(System.out::println);
}
@Test
//有 -n 参数输入时
//单词数超过n个则输出前n个单词及词频
//不足则输出所有单词及词频
public void mostWords1() {
List<Map.Entry<String, Integer>> list = calMost.mostWords(map,9);
list.forEach(System.out::println);
}
}
- 2.对WordsCount的单元测试
测试获取单词数和单词、词频的函数
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.Assert.*;
public class WordsCountTest {
String path = "input.txt";
HandleContent handleContent = new HandleContent(path);
// -m 词组单词数设为1
// -w 权重设为0
WordsCount wordsCount = new WordsCount(handleContent, 1, 0);
@Test
//获取单词总数
public void getSum() {
System.out.println(wordsCount.getSum());
}
@Test
//获取map内容,单词及其词频数
public void getMap() {
System.out.println(wordsCount.getMap().toString());
}
}
- 3.对HandleContent的单元测试
测试获取论文title、论文abstract和所有论文内容的函数
import org.junit.Test;
public class HandleContentTest {
String path = "test.txt";
//声明一个handleContent对输入内容进行分类
//将所有的title和所有的abstract各自分到一起
HandleContent handleContent = new HandleContent(path);
@Test
//输出应为所有的title内容
public void getTitles() {
System.out.println(handleContent.getTitles().toString());
}
@Test
//输出应为所有的abstract内容
public void getAbstracts() {
System.out.println(handleContent.getAbstracts().toString());
}
@Test
//输出应为title + abstract 内容
public void getHandledContent() {
System.out.println(handleContent.getHandledContent());
}
}
八、GitHub代码签入记录
九、遇到的困难及解决方法
对于题意的理解存在问题,题目要求标点符号也要算成分隔符,例如question(“orange就可以
分割成question和orange两个单词。但是我们对连字符"-"产生了误解,认为像Super-Resolution
这样的从语义上来看,只能算一个单词,所以产生了错误。经过和助教还有其他同学的沟通后,才明
白了自己的错误。- 粗心导致的问题,在测试时发现统计单词数时经常会漏掉许多单词,我们一开始认为是正则出现了
问题,但是实际上导致这个bug的是循环语句的错误,在循环体中本应该使用continue结束本次循环
但是错误地使用了break结束了整个循环,所以导致结果的错误。
for (int j = 1; j < m; j++) {
if (!temp[i + j].matches(wordRegex)) {
isContinue = false;
break;
} else {
if (isSplitStart) {
stringBuilder.append(splits.get(i + j));
} else {
stringBuilder.append(splits.get(i + j - 1));
}
stringBuilder.append(temp[i + j]);
}
}
- 玄学bug, 出现了一次玄学bug,在测试时,读取文件中的序号后,正则匹配不出来,按行读取查
看输出时没问题,单独测试正则也是没问题的,???,就完全找不到问题,删除了这个txt文件,重
新建了一个txt文件,内容完全一样,再次测试就没问题了,???。
十、评价队友
志炜大佬精通Java、Python,项目经验丰富,号称数计最强的男人,本次的结对作业让我实实在在地体会到了这位巨佬的惊人实力。这次的结对作业对于他来说并没有什么难度,但是像我这种刚学Java的人就不一样了。但是每次在我遇到问题时,他都能够一眼看出我的问题出在哪里,告诉我遇到这种问题该如何解决并提出一些新的思路。而且志炜大佬总是会非常严格认真地纠正我的代码习惯(我之前对这个并不在意)。在他的帮助下,我对Java的学习和理解又得到了更进一步的加深。和志炜大佬一起合作完成本次作业,让我的Java学习之旅轻松又愉快。为了感谢志炜大佬一段时间的帮助,我决定送一套自己的亲笔签名照给他,希望他能够喜欢(必须喜欢)~~
十一、学习进度条
第N周 | 新增代码行 | 累计代码行 | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 200 | 200 | 15 | 15 | 学习Java以及IDEA的使用 |
2 | 10 | 25 | 阅读构建之法,了解了NABCD模型,学会了原型工具的使用 | ||
3 | 600 | 800 | 20 | 45 | 阅读《第一行代码》学习Android开发,Java进一步学习 |