WebMagic

介绍

WebMagic项目代码分为核心和扩展两部分。核心部分(webmagic-core)是一个精简的、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。

WebMagic的设计目标是尽量的模块化,并体现爬虫的功能特点。这部分提供非常简单、灵活的API,在基本不改变开发模式的情况下,编写一个爬虫。

扩展部分(webmagic-extension)提供一些便捷的功能,例如注解模式编写爬虫等。同时内置了一些常用的组件,便于爬虫开发。

架构

WebMagic的结构分为Downloader(下载,向Scheduler要下载的地址)、PageProcessor(页面解析)、Scheduler(存放url下载队列)、Pipeline(输出到mysql,文件等)四大组件,并由Spider将它们彼此组织起来。这四大组件对应爬虫生命周期中的下载、处理、管理和持久化等功能。WebMagic的设计参考了Scapy,但是实现方式更Java化一些。

而Spider则将这几个组件组织起来,让它们可以互相交互,流程化的执行,可以认为Spider是一个大的容器,它也是WebMagic逻辑的核心

WebMagic的四个组件

1.Downloader
Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。

2.PageProcessor
PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。

在这四个组件中,PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。

3.Scheduler
Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。

4.Pipeline
Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。

Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。

架构图

WebMagic_第1张图片

  1. Downloader从互联网下载一般用的是http请求,下载之后拿到的是一个html页面,把下载的内容封装为一个page对象
  2. 1)PageProcessor对page对象进行解析,把需要的数据封装到ResultItems中,传递给Pipeline
    2)Scheduler 通过request(对url地址的封装)从PageProcesser中拿到url,Scheduler再通过request分发给downloader
  3. Pipeline拿到ResultItems(相当于一个map),做对应的持久化

PageProcessor组件及入门案例

PageProcessor组件

PageProcessor组件是实现核心业务逻辑的组件,在使用WebMagic的使用必须要自定义PageProcessor组件。需要自定一个类实现PageProcessor接口。此接口中有两个方法需要实现一个是getSite方法,此方法需要返回一个Site对象。一个是一个是process方法,此方法没有返回值,方法有个参数是Page对象。

入门案例

依赖


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.itheimagroupId>
    <artifactId>crawler_day02_1artifactId>
    <version>1.0-SNAPSHOTversion>

    <dependencies>
        
        <dependency>
            <groupId>us.codecraftgroupId>
            <artifactId>webmagic-coreartifactId>
            <version>0.7.3version>
        dependency>
        <dependency>
            <groupId>us.codecraftgroupId>
            <artifactId>webmagic-extensionartifactId>
            <version>0.7.3version>
        dependency>
        <dependency>
            <groupId>com.google.guavagroupId>
            <artifactId>guavaartifactId>
            <version>16.0version>
        dependency>

    dependencies>

project>

代码实现

package com.itheima.webmagic;

import org.apache.commons.io.FileUtils;
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.pipeline.FilePipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.scheduler.BloomFilterDuplicateRemover;
import us.codecraft.webmagic.scheduler.QueueScheduler;

import java.util.BitSet;
import java.util.List;

public class MyPageProcessor implements PageProcessor {
    public void process(Page page) {

	//把数据交给Pipeline进行输出
    page.putField("content",page.getHtml().css("div#news_div ul li a",
    "text").all());

	//可以对爬虫进行一些配置
    private Site site = Site.me();

	public Site getSite() {
        return site;
    }
    
	//WebMagic使用的默认下载器是HttpClient
    public static void main(String[] args) {

        //提供自己定义的PageProcessor
        Spider.create(new MyPageProcessor())
                //设置初始下载url地址
                .addUrl("https://www.jd.com/moreSubject.aspx")
                .run();

Site对象

Site对象,可以使用Site.me()创建。
在此对象中可以对爬虫进行一些配置配置,包括编码、抓取间隔、超时时间、重试次数等。

//可以对爬虫进行一些配置
    private Site site = Site.me()
    		// 单位是秒
            .setCharset("UTF-8")//编码
            .setSleepTime(1)//抓取间隔时间,可以解决一些反爬限制
            .setTimeOut(1000 * 10)//超时时间
            .setRetrySleepTime(3000)//重试时间
            .setRetryTimes(3);//重试次数

Site对象中提供的配置方法列表:

方法 说明 示例
setCharset(String) 设置编码 site.setCharset(“utf-8”)
setUserAgent(String) 设置UserAgent site.setUserAgent(“Spider”)
setTimeOut(int) 设置超时时间,单位是毫秒 site.setTimeOut(3000)
setRetryTimes(int) 设置重试次数 site.setRetryTimes(3)
setCycleRetryTimes(int) 设置循环重试次数 site.setCycleRetryTimes(3)
addCookie(String,String) 添加一条cookie site.addCookie(“dotcomt_user”,“code4craft”)
setDomain(String) 设置域名,需设置域名后,addCookie才可生效 site.setDomain(“github.com”)
addHeader(String,String) 添加一条addHeader site.addHeader(“Referer”,“https://github.com”)

page对象

Page对象是PageProcess组件中的核心对象,此对象中包含三个核心操作:

  1. 获取Downloader对象下载结果。
    当我们需要从page对象中获得下载结果时,可以使用page对象的getHtml()方法。
    此方法的返回结果就是一个Html对象,也可以看做把这个html页面解析之后映射成一个Html对象,Html对象实现了Selectable接口,是可以直接使用Selectable接口中提供的选择器。

  2. Scheduler对象中添加Request对象也就是待访问的url
    使用addTargetRequest或者addTargetRequests方法可以将解析出来的链接添加到url访问队列,系统会把url封装成Request对象供Scheduler对象使用。

3)向Pipeline对象中设置输出结果
使用putField方法可以将解析的结果添加到ResultItems对象中,将来在Pipeline对象中可以取到这个数据。

Selectable对象

抽取元素

Selectable相关的抽取元素链式API是WebMagic的一个核心功能。使用Selectable接口,可以直接完成页面元素的链式抽取,也无需去关心抽取的细节。

  1. XPath
    以下是获取属性class=mt的div标签,里面的h2标签的内容
    page.getHtml().xpath("//div[@class=mt]/h2/text()")

  2. CSS选择器
    CSS选择器是与XPath类似的语言。它比XPath写起来要简单一些,但是如果写复杂一点的抽取规则,就相对要麻烦一点。
    div.mt>h1表示class为mt的div标签下的直接子元素h2标签
    page.getHtml().css(“div.mt>h2”).toString()。
    具体规则见css选择器

  3. 正则表达式
    正则表达式则是一种通用的文本抽取语言。在这里一般用于获取url地址。正则表达式学习难度要大一些
    建议不是专门用的话,不需要去专门的学。
    但是如果是专业爬虫的话,很多语言是都支持正则的,并且在代码量上来说更简洁。

	//links:获取所有连接  regex:使用正则  
	// addTargetRequests添加多个url到url任务队列中
     page.addTargetRequests(page.getHtml()
    .css("#news_diva").links()
    .regex("https://www.jd.com/news.html.*3$")
    .all());

获取结果

方法 说明 示例
get() 返回一条String类型的结果 String link= html.links().get()
toString() 同get(),返回一条String类型的结果 String link= html.links().toString()
all() 返回所有抽取结果 List links= html.links().all()

使用Pipeline保存结果

在WebMagic中,Pileline是抽取结束后,进行数据处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。
在这里我们可以指定输出的位置,可以是控制台也可以是文件,当然也可以用户自定义Pipeline实现数据导入到数据库中。

现有的Pipeline

说明 备注
ConsolePipeline 输出结果到控制台 抽取结果需要实现toString方法
FilePipeline 保存结果到文件 抽取结果需要实现toString方法
JsonFilePipeline JSON格式保存结果到文件
ConsolePageModelPipeline (注解模式)输出结果到控制台
FilePageModelPipeline (注解模式)保存结果到文件
JsonFilePageModelPipeline (注解模式)JSON格式保存结果到文件 想持久化的字段需要有getter方法

代码实现添加FilePipeline

//提供自己定义的PageProcessor
Spider.create(new MyPageProcessor())
//设置初始下载url地址
      .addUrl("https://www.jd.com/moreSubject.aspx")
//添加文件输出的Pipeline
	  .addPipeline(new FilePipeline("D:\\crawler"))

Scheduler组件

WebMagic提供了Scheduler可以帮助我们解决下载目标url管理的问题。

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

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

WebMagic内置了几个常用的Scheduler。如果你只是在本地执行规模比较小的爬虫,那么基本无需定制Scheduler,但是了解一下已经提供的几个Scheduler还是有意义的。

WebMagic_第2张图片
去重部分被单独抽象成了一个接口:DuplicateRemover,从而可以为同一个Scheduler选择不同的去重方式,以适应不同的需要,目前提供了两种去重方式。
WebMagic_第3张图片
RedisScheduler是使用Redis的set进行去重,其他的Scheduler默认都使用HashSetDuplicateRemover来进行去重。

三种去重方式

  1. HashSet(小型爬虫)
    使用java中的HashSet不能重复的特点去重。优点是容易理解。使用方便。
    缺点:占用内存大,性能较低。

  2. Redis去重(超大型爬虫,可以搭集群)
    使用Redis的set进行去重。优点是速度快(Redis本身速度就很快),而且去重不会占用爬虫服务器的资源,可以处理更大数据量的数据爬取。
    缺点:需要准备Redis服务器,增加开发和使用成本。

  3. 布隆过滤器(BloomFilter)(大型爬虫)
    使用布隆过滤器也可以实现去重。优点是占用的内存要比使用HashSet要小的多,也适合大量数据的去重操作。
    缺点:有误判的可能。没有重复可能会判定重复,但是重复数据一定会判定重复。

布隆过滤器 (Bloom Filter)是由Burton Howard Bloom于1970年提出,它是一种space efficient的概率型数据结构,用于判断一个元素是否在集合中。在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。
哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,误报率越大,但是漏报是不可能的(重复的一定找得到,但是有可能漏抓)。原理见算法

Spider

Spider是爬虫启动的入口。在启动爬虫之前,我们需要使用一个PageProcessor创建一个Spider对象,然后使用run()进行启动。

同时Spider的其他组件(Downloader、Scheduler、Pipeline)都可以通过set方法来进行设置。

WebMagic_第4张图片

案例:爬取51job上的招聘信息

依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
    </parent>
    <groupId>com.ithiema</groupId>
    <artifactId>crawler_day02_51job</artifactId>
    <version>1.0-SNAPSHOT</version>


    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--SpringMVC-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--测试组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--SpringData Jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!--MySQL连接包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--WebMagic核心包-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--WebMagic扩展-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.3</version>
        </dependency>
        <!--WebMagic对布隆过滤器的支持-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0</version>
        </dependency>

    </dependencies>
</project>

JobPageProcessor

package com.itheima.wuyijob.crawler;

import com.itheima.wuyijob.pojo.JobInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Html;

import java.util.List;

@Component
public class JobPageProcessor implements PageProcessor {

    @Autowired
    private JpaPipeline jpaPipeline;

    // 测试代码
//    String url = "https://www.jd.com/news.html?id=38673";
    String url = "https://search.51job.com/list/010000,000000,0000,32%252C38,9,99,java,2,1.html?" +
        "lang=c&stype=&postchannel=0000&workyear=99&cotype=99°reefrom=99&jobterm=99&companysize=99" +
        "&providesalary=99&lonlat=0%2C0&radius=-1&ord_field=0&confirmdate=9&fromType=&dibiaoid=0&address=&line=&specialarea=00&from=&welfare=";

    @Override
    public void process(Page page) {
        // 测试代码
//        page.putField("content",page.getHtml().css("div.mt h1","text").all());

        // 获取列表页的职位详情url
        List<String> urlList = page.getHtml().css("div#resultList div.el p.t1").links().all();
//        urlList.forEach(e -> System.out.println(e));

        // urlList没有值,页面是职位详情页,如果有值,是职位列表页
        if (urlList.size()>0){
            // 把职位详情url放到url管理列表中
            page.addTargetRequests(urlList);
            // 获取下一页的地址,到这就一直不会停,会一直下一页,具体原因参考csdn架构图
            page.addTargetRequests(page.getHtml().css("li.bk").links().all());
        }else {
            // 解析页面并存放结果到ResultItems里
            parseJobInfo(page);
        }
    }

    private void parseJobInfo(Page page) {
        // 创建职位详情对象,用来存放解析的数据
        JobInfo jobInfo = new JobInfo();
        // 解析页面获取数据
        Html html = page.getHtml();
        jobInfo.setJobName(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > h1","text").get());
        jobInfo.setSalary(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > strong","text").get());
        jobInfo.setCompanyName(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > p.cname > a.catn","text").get());
        jobInfo.setJobAddr(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tHeader.tHjob > div > div.cn > p.msg.ltype","text").get());
        jobInfo.setJobInfo(html.css("body > div.tCompanyPage > div.tCompany_center.clearfix > div.tCompany_main > div:nth-child(1) > div","text").get());
        jobInfo.setUrl(page.getUrl().toString());
        // 封装好的职位详情数据存放到resultItems中
        page.putField("jobInfo",jobInfo);
    }

    // 添加定时任务配置
    // initialDelay,项目启动成功后,多久执行任务,单位毫秒
    // fixedDelay,任务执行完成后,间隔多久下一次任务执行,单位毫秒
    @Scheduled(initialDelay = 1000, fixedDelay = 10000)
    public void run(){
        Spider.create(new JobPageProcessor())
        // 使用自定义的PipeLine保存数据
        .addPipeline(jpaPipeline)
        .addUrl(url)
        .thread(20)
        .run();
    }

    private Site site = Site.me()
            .setTimeOut(10*  1000); // 超时10s

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

JpaPipeline

package com.itheima.wuyijob.crawler;

import com.itheima.wuyijob.pojo.JobInfo;
import com.itheima.wuyijob.service.JobInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

/**
 * 实现PipeLine和使用定时器
 */
@Component
public class JpaPipeline implements Pipeline {

    @Autowired
    private JobInfoService jobInfoService;

    @Override
    public void process(ResultItems resultItems, Task task) {
        // 获取职位数据
        JobInfo jobInfo = resultItems.get("jobInfo");


        if (jobInfo!=null){
                jobInfoService.save(jobInfo);
        }
    }
}

代理的使用

有些网站不允许爬虫进行数据爬取,因为会加大服务器的压力。其中一种最有效的方式是通过ip+时间进行鉴别,因为正常人不可能短时间开启太多的页面,发起太多的请求。
WebMagic_第5张图片
提供两个免费代理ip的服务商网站:
米扑代理
https://proxy.mimvp.com/free.php
西刺免费代理IP
http://www.xicidaili.com/

代码实现

package com.itheima.day03.job;

import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.downloader.HttpClientDownloader;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.proxy.Proxy;
import us.codecraft.webmagic.proxy.SimpleProxyProvider;


public class ProxyTest implements PageProcessor {
    @Override
    public void process(Page page) {
        System.out.println("获取到的自己的ip地址是:");
        System.out.println(page.getHtml().css("center", "text").get());
    }

    private Site site = Site.me();

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

    public static void main(String[] args) {
        //创建下载器
        HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
        //设置代理服务器
        httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
                new Proxy("27.203.165.139",8060 )
        ));
        Spider.create(new ProxyTest())
                .addUrl("http://2019.ip138.com/ic.asp")
                //把设置好代理服务器的下载器进行使用
                .setDownloader(httpClientDownloader)
                .run();
    }
}

Selenium+headless浏览器实现动态爬虫

我们可以使用HttpClient模拟浏览器抓取静态html,但是对js的解析部分还是很薄弱。虽然我们可以读取js的运作机制并且找到相关数据,但是这样会耗费大量时间。为了解决这个问题我们可以使用工具来模拟浏览器的运行,直接获取解析结果。这就是使用Selenium+headless浏览器来实现动态爬虫。

例如京东商品页:先加载的一个html没有价格,加载完之后会执行js,js会发起ajax或者类似的远程调用获取价格,然后再写入html页面中的价格去。所以此时页面的价格是一般爬虫无法爬取的,它的价格是在另一个请求中。

Selenium
Selenium是一个用于Web应用程序测试的工具。Selenium可以使用代码控制浏览器,就像真正的用户在操作一样。而对于爬虫来说,使用Selenium操控浏览器来爬取网上的数据那么肯定是爬虫中的杀手武器。Selenium支持多种浏览器可以是chrome、Firefox、PhantomJS等

使用WebDriver在Chrome浏览器上进行测试时或者做页面抓取,需要从http://chromedriver.storage.googleapis.com/index.html网址中下载与本机chrome浏览器对应的驱动程序,驱动程序名为chromedriver。chromedriver的版本需要和本机的chrome浏览器对应,才能正常使用,一般情况下下载最新版就可以了。

headless浏览器(PhantomJS(这个和headless应该是等价的,但是这个已经被弃用了))
一个基于webkit内核的无头浏览器,即没有UI界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现

使用动态爬虫爬取京东商城的完整数据

京东现在貌似增加了反爬策略,在之后爬取的过程中报sesssion错误的问题,不过不是很确定

需求分析

要爬取京东商城的完整商品数据,需要使用无头浏览器来进行数据抓取,这样就可以取到搜索结果页面的后半部分数据。 (这个案例)只能爬取30个详情,具体为什么不太明白。

WebMagic框架默认使用的是HttpClient下载页面,所以我们需要把HttpClient换成无头浏览器,那么就需要定制Downloader组件。

依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
    </parent>

    <groupId>com.itheima</groupId>
    <artifactId>crawler_day03_jd</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--SpringData Jpa-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--MySQL连接包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!--WebMagic核心包-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-core</artifactId>
            <version>0.7.3</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--WebMagic扩展-->
        <dependency>
            <groupId>us.codecraft</groupId>
            <artifactId>webmagic-extension</artifactId>
            <version>0.7.3</version>
        </dependency>

        <!--selenium依赖-->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>3.13.0</version>
        </dependency>
    </dependencies>


</project>

JdChromeDownloader

package com.itheima.cralwer.crawler;

import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.downloader.Downloader;
import us.codecraft.webmagic.selector.PlainText;

@Component
public class JdChromeDownloader implements Downloader {

    //声明驱动
    private RemoteWebDriver driver;

    public JdChromeDownloader() {
        //第一个参数是使用哪种浏览器驱动
        //第二个参数是浏览器驱动的地址
        System.setProperty("webdriver.chrome.driver","C:\\Users\\Administrator\\AppData\\Local\\Google\\Chrome\\Application\\chromedriver\\chromedriver.exe");

        //创建浏览器参数对象
        ChromeOptions chromeOptions = new ChromeOptions();

        // 设置为 headless 模式,上课演示,或者学习不要打开
        // chromeOptions.addArguments("--headless");
        // 设置浏览器窗口打开大小
        chromeOptions.addArguments("--window-size=1280,700");

        //创建驱动
        this.driver = new ChromeDriver(chromeOptions);
    }

    @Override
    public Page download(Request request, Task task) {
        try {
            driver.get(request.getUrl());
            Thread.sleep(2000);

            //无论是搜索页还是详情页,都滚动到页面底部,所有该加载的资源都加载
            //需要滚动到页面的底部,获取完整的商品数据
            driver.executeScript("window.scrollTo(0, document.body.scrollHeight - 1000)");
            Thread.sleep(2000l);

            //获取页面对象
            Page page = createPage(request.getUrl(), driver.getPageSource());

            //判断是否是搜索页
            if (request.getUrl().contains("search")) {
                //如果请求url包含search,说明是搜索结果页
                //在搜索结果页,需要获取下一页的链接地址
                //点击下一页按钮,在下一页中获取当前页的url(就是下一页的url),放到任务队列中
                WebElement next = driver.findElement(By.cssSelector("a.pn-next"));
                //点击
                next.click();

                //获取当前页面(其实就是下一页)的url地址
                String nextUrl = driver.getCurrentUrl();

                //使用page对象,把下一页url放到任务列表中
                page.addTargetRequest(nextUrl);
            }

            //关闭浏览器
            //driver.close();

            return page;

        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        return null;
    }

    @Override
    public void setThread(int threadNum) {

    }

    //构建page返回对象
    private Page createPage(String url, String content) {
        Page page = new Page();
        page.setRawText(content);
        page.setUrl(new PlainText(url));
        page.setRequest(new Request(url));
        page.setDownloadSuccess(true);

        return page;
    }

}

JdPageProcessor

package com.itheima.cralwer.crawler;

import com.itheima.cralwer.pojo.Item;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Selectable;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class JdPageProcessor implements PageProcessor {
    @Override
    public void process(Page page) {
        //System.out.println(page.getHtml().css("div.mt h1", "text"));
        //获取页面中的商品列表数据,只有搜索结果页才有商品列表
        List<Selectable> nodes = page.getHtml().css("#J_goodsList li.gl-item").nodes();

        //判断nodes是否有值
        if (nodes != null && nodes.size() > 0) {
            //如果有值表示是搜索结果页

            //声明存放商品的集合
            List<Item> itemList = new ArrayList<>();

            //遍历商品项
            for (Selectable node : nodes) {
                //获取商品spu
                String spu = node.css("li", "data-spu").get();

                //获取商品的sku,一个spu有可能有多个sku
                List<String> skuList = node.css("li.ps-item img", "data-sku").all();


                //遍历sku
                for (String sku : skuList) {
                    //创建对象
                    Item item = new Item();

                    //设置数据
                    item.setSpu(Long.parseLong(spu));
                    item.setSku(Long.parseLong(sku));
                    item.setCreated(new Date());
                    item.setUpdated(item.getCreated());

                    //放到集合中
                    itemList.add(item);

                    //把商品详情页的url放到url任务队列中
                    page.addTargetRequest("https://item.jd.com/" + sku + ".html");
                }

            }

            //把需要持久化的数据放到ResultItems中
            page.putField("itemList", itemList);


        } else {
            //如果没有值表示是商品详情页
            //创建商品对象
            Item item = new Item();
            String sku = page.getHtml().css("div.left-btns a.J-follow", "data-id").get();

            item.setSku(Long.parseLong(sku));
            item.setTitle(page.getHtml().css("div.sku-name", "text").get());
            item.setPrice(page.getHtml().css("span.p-price span.price", "text").get());
            item.setUrl(page.getUrl().toString());

            //保存到ResultItems中
            page.putField("item", item);
        }


    }

    private Site site = Site.me().setTimeOut(2000);

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

JpaPipeline

package com.itheima.cralwer.crawler;

import com.itheima.cralwer.pojo.Item;
import com.itheima.cralwer.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

import java.util.List;

@Component
public class JpaPipeline implements Pipeline {

    @Autowired
    private ItemService itemService;

    @Override
    public void process(ResultItems resultItems, Task task) {
        //获取商品列表页数据
        List<Item> itemList = resultItems.get("itemList");

        if (itemList != null && itemList.size() > 0) {
            itemService.saveItemList(itemList);
        }


        //获取商品详情页数据
        Item item = resultItems.get("item");
        if (item != null) {
            itemService.saveItem(item);
        }

    }
}

StartCrawler

package com.itheima.cralwer.crawler;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Spider;

@Component
public class StartCrawler {

    @Autowired
    private JdChromeDownloader downloader;
    @Autowired
    private JpaPipeline jpaPipeline;

    //声明搜索页的初始地址
    String url = "https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&enc=utf-8" +
            "&suggest=1.his.0.0&wq=&pvid=72c93b8e6951419f83e22a7daee906d0";

    @Scheduled(cron = "0/5 * * * * *")
    public void run() {
        Spider.create(new JdPageProcessor())
                //.addUrl("https://www.jd.com/news.html?id=38673")
                .addUrl(url)
                //设置下载器
                .setDownloader(downloader)
                //设置使用jpa的输出
                .addPipeline(jpaPipeline)
                .run();
    }
}

你可能感兴趣的:(爬虫,webmejic)