忠 ju

导航

 

写在前面

本次结对作业中,我和队友之间的分工是这样的:

  • 余秉鸿:完成基础需求
  • 刘忠燏:在队友代码的基础上,完成进阶需求

由于这种分工,普通需求和进阶需求的仓库实际上是分别提交的,虽然我两个仓库都 Fork 了,但实际上只有进阶需求的仓库是有提交记录的。

PSP 表格

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

WordCount 进阶需求——思路

在作业要求发布之后,我和我的队友大致商量了一下,最后确定下来就是他完成基础需求的部分,我使用他的代码完成进阶需求(实际上他不仅完成了基础需求,还给进阶需求预留了 API)。于是留给我的任务就只有爬虫以及命令行解析。命令行解析的部分本来想自己写,但是又担心自己在爬虫上花费太多时间(实际上当我完成爬虫部分的代码后我发现自己的时间可能不多了),于是再一次借助了一个开源的库 Apache Common CLIs 完成了命令行解析的部分。WordCount 核心代码的实现可以在我队友的博客里找到。

在作业截止之前,我自己也想到了一个 WordCound 的实现思路,只不过想到的太迟,来不及写代码了。我就在博客里简单讲一下我的思路吧(可能其他大佬也想到这种方案了):我的思路就是模仿 XML 的 SAX 解析模式,也就是将文档以流的形式读入,一次解析一行,分别在解析新行,解析空白符,解析单词等情况时产生一个对应的事件,并由单独的事件处理程序处理。画成类图的话,WordCount 的结构如下图所示:

最后,进阶需求的内容我并没有全部完成。我没有实现 -m 的功能(即词组解析),短时间内我想不出什么方法来完成这个需求(如果最后的词组没有要求带分隔符输出的话,我认为实现起来更容易些)

WordCount 进阶需求——使用工具爬取论文信息

这部分任务,在整个进阶需求中,算是相对比较简单的。我本来想使用 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 存放在 idpapertitle<div> 标签里,Abstract 存放在 idabstract<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],修改代码之后,爬取的论文列表才算完整。

WordCount 进阶需求——单元测试

这部分我觉得做得不是很好。本来我的想法是走测试驱动开发的。但我在和队友讨论思路的时候我忘了提出来了,中间的几天又光顾着注意实现的细节,结果到最后这部分单元测试变成了先写代码后测试,然后顺带帮队友的代码进行 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 里自己实现了堆排序),而我则更倾向于使用现有的库

参考链接


  1. https://jsoup.org/apidocs/org/jsoup/Connection.html#maxBodySize-int- ↩︎

posted on 2019-03-15 16:41  忠ju  阅读(250)  评论(3编辑  收藏  举报