这个做业属于哪一个课程:软件工程 1916 | Whtml
这个做业要求在哪里:结对第二次——文献摘要热词统计及进阶需求java
结对学号:221600126 刘忠燏, 021600823 余秉鸿git
这个做业的目标:完成做业要求中的基本需求和进阶需求;熟悉 Git 和 GitHub 的使用;学习并掌握单元测试技巧;借助单元测试,适当重构部分代码github
Fork 的 GitHub 项目地址:PairProject2-Javaweb
GitHub 的签入记录apache
本次结对做业中,我和队友之间的分工是这样的:api
因为这种分工,普通需求和进阶需求的仓库其实是分别提交的,虽然我两个仓库都 Fork 了,但实际上只有进阶需求的仓库是有提交记录的。网络
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
· Estimate | · 估计这个任务须要多少时间 | 20 | 20 |
Development | 开发 | 730 | 1120 |
· Analysis | · 需求分析(包括学习新技术) | 90 | 95 |
· Design Spec | · 生成设计文档 | 30 | 35 |
· Design Review | · 设计复审 | - | |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范 | 30 | 30 |
· Design | · 具体设计 | 120 | 240 |
· Coding | · 具体编码 | 400 | 600 |
· Code Review | · 代码复用 | - | |
· Test | · 测试(自我测试、修改代码、提交修改) | 60 | 120 |
Reporting | 报告 | 65 | 65 |
· Test Report | · 测试报告 | 20 | 20 |
· Size Measurement | · 计算工做量 | 10 | 10 |
· Postmortem & Process Improvement | · 过后总结,并提出过程改进计划 | 35 | 35 |
合计 | 815 | 1205 |
在做业要求发布以后,我和个人队友大体商量了一下,最后肯定下来就是他完成基础需求的部分,我使用他的代码完成进阶需求(实际上他不只完成了基础需求,还给进阶需求预留了 API)。因而留给个人任务就只有爬虫以及命令行解析。命令行解析的部分原本想本身写,可是又担忧本身在爬虫上花费太多时间(实际上当我完成爬虫部分的代码后我发现本身的时间可能很少了),因而再一次借助了一个开源的库 Apache Common CLIs 完成了命令行解析的部分。WordCount 核心代码的实现能够在我队友的博客里找到。多线程
在做业截止以前,我本身也想到了一个 WordCound 的实现思路,只不过想到的太迟,来不及写代码了。我就在博客里简单讲一下个人思路吧(可能其余大佬也想到这种方案了):个人思路就是模仿 XML 的 SAX 解析模式,也就是将文档以流的形式读入,一次解析一行,分别在解析新行,解析空白符,解析单词等状况时产生一个对应的事件,并由单独的事件处理程序处理。画成类图的话,WordCount 的结构以下图所示:app
最后,进阶需求的内容我并无所有完成。我没有实现 -m
的功能(即词组解析),短期内我想不出什么方法来完成这个需求(若是最后的词组没有要求带分隔符输出的话,我认为实现起来更容易些)
这部分任务,在整个进阶需求中,算是相对比较简单的。我原本想使用 Python,并使用 Python 的 urllib
库和 BeautifulSoup
库完成。不过在翻阅了相关资料后,我被相关的 API 搞得晕头转向,遂另寻方法。这里的爬取,是从 CVPR 的网页上获取相关信息,因而使用查看论文页的网页结构,发现网页的结构十分清楚,以其中一篇论文为例:
<!-- 省略部分代码…… --> <div id="papertitle">Embodied Question Answering</div> <!-- 省略部分代码…… --> <div id="abstract"> We present a new AI task -- Embodied Question Answering (EmbodiedQA) -- where an agent is spawned at a random location in a 3D environment and asked a question ("What color is the car?"). In order to answer, the agent must first intelligently navigate to explore the environment, gather necessary visual information through first-person (egocentric) vision, and then answer the question ("orange"). EmbodiedQA requires a range of AI skills -- language understanding, visual recognition, active perception, goal-driven navigation, commonsense reasoning, long-term memory, and grounding language into actions. In this work, we develop a dataset of questions and answers in House3D environments, evaluation metrics, and a hierarchical model trained with imitation and reinforcement learning. </div> <!-- 省略部分代码…… -->
上例中,论文的 Title 存放在 id
为 papertitle
的 <div>
标签里,Abstract 存放在 id
为 abstract
的 <div>
标签里,爬取起来可谓是很是容易。因而最后决定使用 Java 实现,爬虫程序使用了一个开源项目 Jsoup(项目地址:GitHub,采用 MIT 协议),这个库提供了对 HTML 文档解析的支持,并提供一系列 API 用于提取数据(就像使用 DOM 同样)。具体思路是先从首页爬取每篇论文的连接,再分别访问每篇论文的网页获取相关信息。
/** * PaperSniffer - Collect paper information from given website. * * @author Liu Zhongyu */ public class PaperSniffer { private List<String> paperUrls = null; private List<PaperContent> paperContents = null; /** * Return a list of paper URLs. */ public List<String> getPaperUrls() { if(paperUrls != null) return paperUrls; paperUrls = new LinkedList<>(); try { // 被注释的代码存在一个隐藏问题,下面未注释的代码为正确代码,详情见后文 // Document document = Jsoup.connect(SnifferConfig.START_URL).get(); Document document = Jsoup.connect(SnifferConfig.START_URL).maxBodySize(0).get(); // document 的 select 方法接受一个字符串参数,字符串内容为 CSS 选择器 // 在 CVPR 2018 的首页上,使用 "dt.ptitle > a" 选择全部包含论文连接的 <a> 标签 Elements linkElements = document.select(SnifferConfig.PAPER_URL_QUERY); for(Element link : linkElements) paperUrls.add(link.attr("href")); } catch (IOException e) { e.printStackTrace(); } return paperUrls; } /** * Return a list of PaperContent. * Each PaperContent object contains the titles and abstract of a paper. */ public List<PaperContent> getPaperContents() { if(paperContents != null) return paperContents; paperUrls = getPaperUrls(); paperContents = new LinkedList<>(); for(String paperUrl : paperUrls) { // visit every paper's web page to grab its title and abstract try { // 对每篇论文的 URL,打开该连接 Document document = Jsoup.connect(SnifferConfig.URL_BASE + paperUrl).get(); // 使用 CSS 选择器语法选择论文的标题节点和摘要节点,其中: // PAPER_TITLE_QUERY ----> "#papertitle" // PAPER_ABSTRACT_QUERY ----> "#abstract" Element titleNode = document.select(SnifferConfig.PAPER_TITLE_QUERY).first(); Element abstractNode = document.select(SnifferConfig.PAPER_ABSTRACT_QUERY).first(); // 提取这两个节点的文本内容,建立一个 PaperContent 对象,加入结果列表 PaperContent paper = new PaperContent(titleNode.text(), abstractNode.text()); paperContents.add(paper); } catch (IOException e) { e.printStackTrace(); } } return paperContents; } }
程序编译完成,也能够正常运行,但须要等很长时间才能出结果。个人网络访问 CVPR 的网站仍是比较慢的,上述代码中,对论文的爬取是单线程的,这影响了整个程序的运行时间。遂尝试加入多线程支持,改动后的代码以下(只展现修改后的方法)
/** * Return a list of PaperContent. * Each PaperContent object contains the titles and abstract of a paper. */ public List<PaperContent> getPaperContents() { if(paperContents != null) return paperContents; paperUrls = getPaperUrls(); paperContents = new LinkedList<>(); for(String paperUrl : paperUrls) { // visit every paper's web page to grab its title and abstract // create a thread for each paperUrl Runnable thread = new Runnable() { @Override public void run() { try { Document document = Jsoup.connect(SnifferConfig.URL_BASE + paperUrl).get(); Element titleNode = document.select(SnifferConfig.PAPER_TITLE_QUERY).first(); Element abstractNode = document.select(SnifferConfig.PAPER_ABSTRACT_QUERY).first(); PaperContent paper = new PaperContent(titleNode.text(), abstractNode.text()); synchronized (LOCK) { paperContents.add(paper); } } catch (IOException e) { e.printStackTrace(); } } }; thread.run(); } return paperContents; }
简单地为采集一篇论文信息的操做建立了线程,固然,在对 paperContents
的写入上须要加锁以免一些问题。我作过简单的性能测试,爬取一样数量的论文,单线程用时 211.73s,多线程用时 130.655s(单次运行时间),可见多线程所带来的提高仍是比较明显的。
可是和其余同窗的爬取结果一比较,才发现我爬取的内容少了一半,经调试,发如今获取论文连接时就只获取了一半的连接,百思不得其解。最后仍是一个使用了相同的库的同窗告诉我 Jsuit.connect()
建立连接时,其默认只获取 1M 左右的信息,而 CVPR 的论文页已经超出了这个大小,解决方案是在建立连接时,使用 Jsuit.connect().maxBodySize(0)
来解除这个内存限制1,修改代码以后,爬取的论文列表才算完整。
这部分我以为作得不是很好。原本个人想法是走测试驱动开发的。但我在和队友讨论思路的时候我忘了提出来了,中间的几天又光顾着注意实现的细节,结果到最后这部分单元测试变成了先写代码后测试,而后顺带帮队友的代码进行 Code Review。具体作的话,就是我看我队友代码里的一个方法,而后根据队友的注释写测试用例,最后运行单元测试。单元测试主要是针对队友的代码中一个实现了相似 C 语言中的 ctype.h
的类中的方法进行测试,好比:
@Test public void testIsNotDigitOrAlpha() { for(int i = 0; i < 26; i++) { assertFalse(String.format("%c: ", 'A' + i), c.isNotDigitOrAlpha((char)('A' + i))); assertFalse(String.format("%c: ", 'a' + i), c.isNotDigitOrAlpha((char)('a' + i))); } for(int i = 0; i < 9; i++) { assertFalse(String.format("%c: ", '0' + i), c.isNotDigitOrAlpha((char)('0' + i))); } assertTrue("â: ", c.isNotDigitOrAlpha('â')); assertTrue("é: ", c.isNotDigitOrAlpha('é')); }
上述方法是判断一个字符是不是非字母数字字符。因而测试用例就覆盖了全部的英文字母和数字,至于最后两个法文字母,是从爬虫的结果里找到了,将其做为一个非英文字母的用例。
@Test public void testIsWord() { assertTrue(c.isWord("apple")); assertFalse(c.isWord("foo")); assertFalse(c.isWord("123foo")); assertFalse(c.isWord("Café")); assertTrue(c.isWord("file123")); assertFalse(c.isWord("Mâché")); }
刚才那个例子,里面的状况是能够列举的,而上面的被测方法是判断一个字符串(仅由小写字母和数字构成)是不是合法的单词,队友说传入这个方法的字符串是已经被分割符分割并所有转成小写后的单词,因此以上的样例大概能覆盖到各类状况。
尽管如此,单元测试仍是在必定程度上发挥了做用,主要是在重构上面,个人队友多是写 C 写习惯了吧,对于字符类型的判断用的是 C 的那一套,代码里 Magic Number 处处乱飞,有了单元测试后我就能够比较放心地去重写这部分代码(测试可以保证重构后的代码功能不变)。
我在写单元测试的时候,也是顺便帮队友的代码进行 Code Review,例如上述测试方法中被测试的一个 isNotDigitOrAlpha
方法,其最先的名称叫 isCharacter
,在写单元测试的时候我发现了这个问题,并将其改为了一个更合理的名字。
本身在此次做业中,有不少事情没有考虑周全,在完成做业的过程当中交流得还不够。我以为若是沟通得再频繁一些,开发过程当中出现的问题会提前暴露出来,至少给解决问题留下余地(好比说将测试驱动开发带到此次做业里来,后期出现严重 bug 的几率会有所下降)
我对我队友的评价仍是很好的。他的代码写得还能够,但有一个问题是他对于一些工具和框架(好比 Git 和 JUnit)不是很熟,还须要学习。他身上有一点值得我去学习的地方是他会尝试着本身造轮子(好比在 WordCount 里本身实现了堆排序),而我则更倾向于使用现有的库
https://jsoup.org/apidocs/org/jsoup/Connection.html#maxBodySize-int-↩