软件工程实践2020_个人作业 —— 技术博客(webmagic的使用说明)

Part.01 Webmagic介绍

webmagic是一个开源的Java垂直爬虫框架,目标是简化爬虫的开发流程,让开发者专注于逻辑功能的开发
WebMagic项目代码分为核心和扩展两部分

  • 核心部分(webmagic-core)是一个精简的、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。WebMagic的架构设计参照了Scrapy,目标是尽量的模块化,并体现爬虫的功能特点。这部分提供非常简单、灵活的API,在基本不改变开发模式的情况下,编写一个爬虫
  • 扩展部分(webmagic-extension)提供一些便捷的功能,例如注解模式编写爬虫等。同时内置了一些常用的组件,便于爬虫开发

Part.02 Webmagic设计原理

WebMagic的结构分为Downloader、PageProcessor、Scheduler、Pipeline四大组件,并由Spider将它们彼此组织起来。
WebMagic总体架构图如下:

技术

WebMagic的四个组件

  • Downloader:Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。
  • PageProcessor:PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。在这四个组件中,PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。
  • Scheduler:Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。
  • Pipeline:Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。

用于数据流转的对象

  • Request:Request是对URL地址的一层封装,一个Request对应一个URL地址。它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。除了URL本身外,它还包含一个Key-Value结构的字段extra。你可以在extra中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。例如附加上一个页面的一些信息等。
  • Page:Page代表了从Downloader下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容。Page是WebMagic抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。
  • ResultItems:ResultItems相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。

Part.03 Webmagic 实例(爬取 笔趣阁&bilibili的数据)

webmagic使用maven管理依赖,在项目中添加对应的依赖即可使用webmagic

        <!-- web magic -->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.3</version>
        </dependency>

        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.3</version>
        </dependency>

WebMagic 使用slf4j-log4j12作为slf4j的实现.如果你自己定制了slf4j的实现,请在项目中去掉此依赖

<exclusions>
    <exclusion>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
    </exclusion>
</exclusions>

核心代码

  • BilibiliReptile.java
package com.reptile.bilibili;

import com.mysql.dao.BilibiliDao;
import com.mysql.entity.Bilibili;

import com.mysql.pipeline.MysqlPipelineBilibili;
import com.tool.SplitJson;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.pipeline.ConsolePipeline;
import us.codecraft.webmagic.processor.PageProcessor;


import java.sql.SQLException;

import static com.reptile.json.GetHttpInterface.GetHttpInterface;

public class BilibiliReptile implements PageProcessor{

    //设置拼接的url变量
    //爬取av号从1至1000000
    private static int start =1;
    private static int end =1000000;

    //设置网站相关配置
    //重试次数和抓取间隔
    private Site site = Site.me().setRetryTimes(5).setSleepTime(0).setUserAgent("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36");

    public synchronized void process(Page page) {

        //视频标题
        page.putField("title",page.getHtml().xpath("//h1[@class='video-title']/span/text()").get());
        //如果title为空则跳过
        if (page.getResultItems().get("title") == null) {
            page.setSkip(true);
        }

        //标题图
        page.putField("image",page.getHtml().xpath("/html/head/meta[10]").get());

        //up
        page.putField("up",page.getHtml().xpath("//div[@class='name']/a[1]/text()").get());

        //简介
        page.putField("info",page.getHtml().xpath("//div[@class='u-info']/div[2]/text()").get());

        //分p
        page.putField("part",page.getHtml().xpath("//*[@id=\"multi_page\"]/div[1]/div/span/text()").get());

        //时间戳
        page.putField("date",page.getHtml().xpath("//div/time/text()").get());
    }

    public Site getSite() {
        // TODO Auto-generated method stub
        return site;
    }

    public static void main(String[] args) throws SQLException {

        int id = 1;

        BilibiliDao bilidao = new BilibiliDao();
        Bilibili bilibili = new Bilibili();

        SplitJson sj = new SplitJson();

        while (start<end) {

            Spider.create(new BilibiliReptile()).addUrl("https://www.bilibili.com/video/av" + start + "/")
                    //输出到控制台
                    .addPipeline(new ConsolePipeline())
                    //传输到数据库
                   // .addPipeline(new MysqlPipelineBilibili())
                    //开启5个线程抓取
                    .thread(5)
                    //启动爬虫
                    .run();

            String str = GetHttpInterface("https://api.bilibili.com/x/web-interface/archive/stat?aid=" + start);

            if((sj.splitCode(str)).equals("0"))
            {
                bilibili.setId(id);
                bilibili.setPlay(sj.splitView(str));
                bilibili.setBarrage(sj.splitDanmaku(str));
                bilidao.addData(bilibili);
                System.out.println(str);
                System.out.println("view:" + sj.splitView(str));
                System.out.println("danmuke:" + sj.splitView(str));
                id++;
            }

            start++;

        }
    }
}
  • BiQuGeReptile.java
package com.reptile.biquge;

import com.mysql.pipeline.MysqlPipelineBiQuGe;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.pipeline.ConsolePipeline;
import us.codecraft.webmagic.processor.PageProcessor;

import java.util.List;

public class BiQuGeReptile implements PageProcessor {

    //regex of URL:http://www.xbiquge.la/
    public static final String FIRST_URL = "http://www\\.xbiquge\\.la/\\w+";
    public static final String HELP_URL = "/\\d+/\\d+/";
    public static final String TARGET_URL = "/\\d+/\\d+/\\d+\\.html/";

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

    public Site getSite() {
        // TODO Auto-generated method stub
        return site;
    }

    public void process(Page page) {


        if(page.getUrl().regex(FIRST_URL).match()){

            List<String> urls = page.getHtml().links().regex(HELP_URL).all();
            page.addTargetRequests(urls);

            //标题
            page.putField("title",page.getHtml().xpath("//div[@id='info']/h1/text()").get());
            //如果title为空则跳过
            if (page.getResultItems().get("title") == null) {
                page.setSkip(true);
            }

            //作者
            page.putField("author",page.getHtml().xpath("//div[@id='info']/p/text()").get());

            //简介
            page.putField("info",page.getHtml().xpath("//div[@id='intro']/p[2]/text()").get());

            //首图url
            page.putField("image",page.getHtml().xpath("//div[@id='fmimg']/img").get());

            //下一深度的网页爬取章节和内容
            if(page.getUrl().regex(HELP_URL).match()){

                List<String> links = page.getHtml().links().regex(TARGET_URL).all();
                page.addTargetRequests(links);

                //章节
                page.putField("chapter", page.getHtml().xpath("//div[@class='bookname']/h1/text()").get());

                //内容
                page.putField("content", page.getHtml().xpath("//div[@id='content']/text()").get());
        }
        }
    }

    public static void main(String[] args){
        Spider.create(new BiQuGeReptile()).addUrl("http://www.xbiquge.la/xiaoshuodaquan/")
                //输出到控制台
                .addPipeline(new ConsolePipeline())
                //传输到数据库
               // .addPipeline(new MysqlPipelineBiQuGe())
                //开启5个线程抓取
                .thread(5)
                //启动爬虫
                .run();
    }

}

注意事项

在0.7.3版本中,爬取只支持TLS1.2的https站点的时候会报错:

javax.net.ssl.SSLException: Received fatal alert: protocol_version

解决办法:https://github.com/code4craft/webmagic/issues/701


Part.04 Webmagic 拓展

URL 去重

Scheduler是WebMagic中进行 URL 管理的组件。一般来说,Scheduler包括两个作用:

    对待抓取的URL队列进行管理。
    对已抓取的URL进行去重。

Scheduler的内部实现进行了重构,去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个Scheduler选择不同的去重方式,以适应不同的需要,目前提供了三种去重方式。

    HashSet
        使用 java 中 HashSet 不能重复的特点去重。占用内存大,性能低
    Redis 去重
        使用 Redis 的 set 进行去重。优点是速度快,而且不会占用爬虫服务器的资源。可以处理更大数据量的数据爬取;缺点是需要 redis 服务器,增加开发和使用成本
    布隆过滤器(BloomFilter)
        优点是占用内存比 HashSet 小的多,也适合大数据量的去重操作。

布隆过滤器的使用实例:

@Scheduled(initialDelay = 1000, fixedDelay = 60 * 1000 * 60 * 12)
    public void start() {
        Spider.create(new BdProcessor())
                .addUrl(URL)
                .thread(10)
                // 设置布隆过滤器去重操作(默认使用HashSet来进行去重,占用内存较大;使用BloomFilter来进行去重,占用内存较小,但是可能漏抓页面)
                .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)))
                .addPipeline(dbPipeline)
                .run();
    }

网页去重

指纹码对比

最常见的去重方案是生成文档的指纹门。例如对一篇文章进行 MD5 加密生成一个字符串,我们可以认为这是文章的指纹码,再和其他的文章指纹码对比,一致则说明文章重复。

但是这种方式是完全一致则是重复的,如果文章只是多了几个标点符号,那仍旧被认为是重复的,这种方式并不合理。

BloomFilter

这种方式就是我们之前对 url 进行去重的方式,使用在这里的话,也是对文章进行计算得到一个数,再进行对比,缺点和方法 1 是一样的,如果只有一点点不一样,也会认为不重复,这种方式不合理。

KMP 算法

KMP 算法是一种改进的字符串匹配算法。KMP 算法的关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。能够找到两个文章有哪些是一-样的,哪些不一样

这种方式能够解决前面两个方式的“只要一点不一样就是不重复”的问题。但是它的时空复杂度太高了,不适合大数据量的重复比对

SimHash (主要)

Google 的 simhash 算法产生的签名,可以满足上述要求。这个算法并不深奥,比较容易理解。这种算法也是目前 Google 搜索引擎所目前所使用的网页去重算法

    分词,把需要判断文本分词形成这个文章的特征单词。
    hash,通过 hash 算法把每个词变成 hash 值,比如“美国”通过 hash 算法计算为 100101,“51 区”通过 hash 算法计算为 101011。这样我们的字符串就变成了一串串数字。
    加权,通过 2 步骤的 hash 生成结果,需要按照单词的权重形成加权数字串,。“美国”的 hash 值为“100101”,通过加权计算为“4-4-44-44”。“51 区”计算为‘“5-55-555”。
    合并,把上面各个单词算出来的序列值累加,变成只有一一个序列串。。“美国”的“4-4-44-44”,“51 区”的“5-55-555”。

代理的使用

有些网站不允许爬虫进行数据爬取,因为会加大服务器的压力。其中一种最有效的方式是通过 ip+时间进行鉴别,因为正常人不可能短时间开启太多的页面,发起太多的请求。

我们使用的 WebMagic 可以很方便的设置爬取数据的时间(参考第二天的的 3.1. 爬虫的配置、启动和终止)。但是这样会大大降低我们 J 爬取数据的效率,如果不小心 ip 被禁了,会让我们无法爬去数据,那么我们就有必要使用代理服务器来爬取数据。

代理 L(英语:Proxy),也称网络代理,是一-种特殊的网络服务,允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。

提供代理服务的电脑系统或其它类型的网络终端称为代理服务器(英文:Proxy. Server)。一个完整的代理请求过程为:客户端首先与代理服务器创建连接,接着根据代理服务器所使用的代理协议,请求对目标服务器创建连接、或者获得目标服务器的指定资源。

我们就需要知道代理服务器在哪里(ip 和端口号)才可以使用。网上有很多代理服务器的提供商,但是大多是免费的不好用,付费的还行。推荐个免费的服务网站:

配置代理

WebMagic的代理API ProxyProvider。因为相对于 Site 的“配置”,ProxyProvider定位更多是一个“组件”,所以代理不再从Site设置,而是由HttpClientDownloader设置。

  • 设置代理:HttpClientDownloader.setProxyProvider(ProxyProvider proxyProvider)

ProxyProvider有一个默认实现:SimpleProxyProvider。它是一个基于简单Round-Robin的、没有失败检查的ProxyProvider。可以配置任意个候选代理,每次会按顺序挑选一个代理使用。它适合用在自己搭建的比较稳定的代理的场景。

代理示例:

设置单一的普通HTTP代理为101.101.101.101的8888端口,并设置密码为"username","password"

    HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
    httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy("101.101.101.101",8888,"username","password")));
    spider.setDownloader(httpClientDownloader);
    HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
    httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
    new Proxy("101.101.101.101",8888)
    ,new Proxy("102.102.102.102",8888)));

Part.05 总结

  • 关于Webmagic使用说明的总结

    • Webmagic属于可快速上手的简易爬虫框架,在阅读官方文档后可快速上手开发,主要难点在于对于xpath(会正则的同学会很快就上手)的学习以及对于部分网站需要进行的cookie验证、代理以及登陆验证时有一定难度,对于部分动态渲染的前端页面可通过Chrome内核内嵌代码渲染的方式解决

Part.06 参考文献

PS:确实觉得写的很好,转给大家分享,文中提到的一些操作自己打算日后试试


posted @ 2020-06-15 20:43  221701412_theTuring  阅读(371)  评论(2编辑  收藏  举报