WebMagic之爬取暴走漫画

引言

曾经在知乎上看到过爬取网易云音乐评论的提问,而且在QQ空间和微信朋友圈也曾见过一首歌N评论的长截图,可以说是一首歌牵扯着数人数年的情感。做一个初出茅庐的Java程序猿,对爬虫充满了兴趣。在网上不断的搜索,发现大部分是通过python写的爬虫,也发现了一些Java框架写的爬虫(知乎:GitHub 上有哪些优秀的 Java 爬虫项目?)。

我在上述知乎问答中发现了WebMagic,一个爬虫框架,以下是官方文档中的设计思想。

一个好的框架必然凝聚了领域知识。WebMagic的设计参考了业界最优秀的爬虫Scrapy,而实现则应用了HttpClient、Jsoup等Java世界最成熟的工具,目标就是做一个Java语言Web爬虫的教科书般的实现。

如果你是爬虫开发老手,那么WebMagic会非常容易上手,它几乎使用Java原生的开发方式,只不过提供了一些模块化的约束,封装一些繁琐的操作,并且提供了一些便捷的功能。

如果你是爬虫开发新手,那么使用并了解WebMagic会让你了解爬虫开发的常用模式、工具链、以及一些问题的处理方式。熟练使用之后,相信自己从头开发一个爬虫也不是什么难事。

因为这个目标,WebMagic的核心非常简单——在这里,功能性是要给简单性让步的 ————WebMagic官方文档

HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。HttpClient已经应用在很多的项目中,比如Apache Jakarta上很著名的另外两个开源项目Cactus和HTMLUnit都使用了HttpClient。

Jsoup是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。

想要爬取网易云音乐,可能对于刚开始学习爬虫的人来说直接上有点难度。我在CSDN看到了(使用WebMagic爬虫框架爬取暴走漫画)这篇文章,但是文章介绍存在一些细节问题不符合结果预期。所以我先作为练手爬取了暴走漫画
所需知识:XPath教程CSS教程直通车。

观察网页

我们可以清晰的看到这三个有意义的信息,作者,时间,内容。
这里写图片描述

创建实体

将信息构建为类

public class BaoZouItems {
    // 作者  
    private String author;  
    // 提交时间  
    private String time;  
    // 内容  
    private String content;

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getTime() {
        return time;
    }

    public void setTime(String time) {
        this.time = time;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @Override
    public String toString() {
        return "BaozouNews [author=" + author + ", time=" + time + ", content=" + content + "]";
    }  

}

决定爬取逻辑

第一步:确定要加入待爬取队列的超链接

这里写图片描述
我们可以通过F12或者右键检查 查看页面元素。
这里写图片描述
我们可以清晰的看到class=”pager”的div元素,其中包含了1,2,3,1000和下一页的链接/text?page= * &sv= ***

page.addTargetRequests(page.getHtml().css("div.pager-content").links().all());  

这段代码将获取到div中所有链接,此时可能眼尖的童鞋发现有重复url(第二页和下一页一样),没事,WebMagic去重。当然也可以更精细的实现,因人而异。

第二步:确定待爬取数据

这里写图片描述
如果我按照我所学习的文章来写的话,我将会得到每页中第一条数据。我们分析一下原因(当时打断点分析出来的)。

public class BaoZouProcessor implements PageProcessor {  
    private Site site = Site.me().setRetryTimes(3).setSleepTime(100);  

    @Override  
    public void process(Page page) {  
        //获取链接,这话没错
        page.addTargetRequests(page.getHtml().css("div.pager-content").links().all());  
        //这里逻辑开始有问题,一个页面有好多条笑话,但创建一个实体对象。
        BaozouNews news = new BaozouNews();  
        //下面三句话对对象赋值,经过断点测试,发现page.getHtml().xpath("//a[@class='article-author-name']/text()")可返回好几个值
        //而作者通过toString(看了一下源码,不为空只返回第一个get(0),然后只保存了第一条,下面语句类似
        news.setAuthor(page.getHtml().xpath("//a[@class='article-author-name']/text()").toString());  
        news.setContent(page.getHtml().xpath("//div[@class='article article-text']/@data-text").toString());  
        news.setTime(page.getHtml().xpath("//span[@class='article-date']/text()").toString());  
        //将需要提取的内容放入page对象
        page.putField("news", news);  
    }  

    @Override  
    public Site getSite() {  
        return site;  
    }  

}

最后导致结果集为1000页的第一条数据。

public class BaoZouMainSplider implements PageProcessor {

    private Site site = Site.me().setRetryTimes(3).setSleepTime(100);

    @Override  
    public void process(Page page) {  
        Html html = page.getHtml();
        page.addTargetRequests(html.css("div.pager-content").links().all());  
        //获取所有div块(每个div块有个一想要的信息)
        Selectable xpath = html.xpath("//div[@class='article article-text']");
        List<String> all = xpath.all();
        List<BaoZouItems> baozouNews = new ArrayList<BaoZouItems>();
        //遍历div块
        for (String string : all) {
            BaoZouItems news = new BaoZouItems(); 
            //转换为html
            Html partHtml = new Html(string);
            //解析数据
            String author = partHtml.xpath("//a[@class='article-author-name']/text()").toString();  
            //特殊处理,当用户为匿名时,所在元素为span而不是a
            if(author == null) {
                author = partHtml.xpath("//span[@class='article-author-name']/text()").toString();  
            }
            news.setAuthor(author);  
            Selectable content = partHtml.xpath("//div[@class='article article-text']/@data-text");
            news.setContent(content.toString());  
            news.setTime(html.xpath("//span[@class='article-date']/text()").toString());  
            //当时间为null,代表为垃圾数据
            if(news.getTime() != null) {
                baozouNews.add(news);
            }
        }
        if(baozouNews.size() == 0) {
            page.setSkip(true);
        }else {
            page.putField("news", baozouNews);
        }
    }  

    @Override  
    public Site getSite() {  
        return site;  
    }  
    public static void main(String[] args) {
        Spider.create(new BaoZouMainSplider()).addUrl("http://baozoumanhua.com/text").addPipeline(new UserDefineFilePipeline("E:\\baozou.txt")).thread(5).run();
    }
}

保存数据

在上述main方法中存在一个new UserDefineFilePipeline(“E:\baozou.txt”),意思是输出到E盘baozou.txt中。

public class UserDefineFilePipeline implements Pipeline{
    private Logger logger = LoggerFactory.getLogger(getClass());
    File baozou = null;

    public UserDefineFilePipeline(String path) {
        baozou = new File(path);
    }

    @Override
    public void process(ResultItems resultItems, Task task) {
        //获取每一页中的数据
        List<BaoZouItems> news = resultItems.get("news"); 
        try (FileWriter fw = new FileWriter(baozou,true);BufferedWriter bw = new BufferedWriter(fw)){
            for (BaoZouItems baozouNews : news) {
                bw.write("作者:\t"+baozouNews.getAuthor()+"\r\n");
                bw.write("时间:\t"+baozouNews.getTime()+"\r\n");
                bw.write("内容:\t"+baozouNews.getContent()+"\r\n");
                bw.write("\r\n");
                bw.write("-----------------------------------------------------------"
                        + "----------------------------------------------");
                bw.write("\r\n");
            }
        } catch (IOException e) {
            logger.warn("write file error", e);
        }
    }
}

查看结果

程序运行约3分钟,就将所有页面爬取下来了,数据为8000余条,
这里写图片描述
已经加入写入数据库功能,可GitHub上查看。

结语

这篇文章是通过https://blog.csdn.net/kingsonyoung/article/details/51752146文章学习感悟而来,初学之作,程序定有可改进之处,望各位大佬提出意见和建议。

所有程序在GitHub上,满意的话请添加个star。

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐