小菜鸟

WebMagic爬虫Demo

2020-05-14  本文已影响0人  我是一颗小虎牙_

前言

WebMagic介绍

Java的可伸缩Web搜寻器框架。官方网站: http://webmagic.io/

一款爬虫框架是WebMagic,其底层使用的HttpClient和Jsoup。

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

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

结构介绍

WebMagic的结构分为Downloader、PageProcessor、Scheduler、Pipeline四大组件,并由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。

用于数据流转的对象

  1. Request

    Request是对URL地址的一层封装,一个Request对应一个URL地址。

    它是PageProcessor与Downloader交互的载体,也是PageProcessor控制Downloader唯一方式。

    除了URL本身外,它还包含一个Key-Value结构的字段extra。你可以在extra中保存一些特殊的属性,然后在其他地方读取,以完成不同的功能。例如附加上一个页面的一些信息等。

  2. Page

    Page代表了从Downloader下载到的一个页面——可能是HTML,也可能是JSON或者其他文本格式的内容。

    Page是WebMagic抽取过程的核心对象,它提供一些方法可供抽取、结果保存等。

  3. ResultItems

    ResultItems相当于一个Map,它保存PageProcessor处理的结果,供Pipeline使用。它的API与Map很类似,值得注意的是它有一个字段skip,若设置为true,则不应被Pipeline处理。

正文

添加依赖

  1. GitHub拉取WebMagic文件,地址https://github.com/code4craft/webmagic

  2. 找到webmagic-core,利用mvn安装到本地仓库。

  3. 添加常规maven坐标。

    <dependencies>
            <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency><!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
                <version>2.1.6.RELEASE</version>
            </dependency><!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.34</version>
            </dependency><!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-core -->
            <dependency>
                <groupId>us.codecraft</groupId>
                <artifactId>webmagic-core</artifactId>
                <version>0.6.1</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/us.codecraft/webmagic-extension -->
            <dependency>
                <groupId>us.codecraft</groupId>
                <artifactId>webmagic-extension</artifactId>
                <version>0.6.1</version>
            </dependency>
            <!--WebMagic对布隆过滤器的支持-->
            <dependency>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
                <version>16.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.4</version>
            </dependency>
        </dependencies>
    

<font color=red>注:博主这里使用得是spring boot+springdatajpa,为什么要在GitHub拉去核心代码,因为现版本对SSL处理不完全,对在爬取只支持SSL v1.2的网站会有SSL的异常抛出。</font>

<font color=red>解决方案:</font>

  1. <font color=red>等作者的0.7.4的版本发布</font>

  2. <font color=red>直接从github上下载最新的代码,安装到本地仓库</font>

<font color=red>这是开发这自己得解决方法</font>:https://github.com/code4craft/webmagic/issues/701

配置文件

WebMagic使用slf4j-log4j12作为slf4j的实现。

添加log4j.properties配置文件

log4j.rootLogger=INFO,A1 
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n

添加application.properties配置文件

#DB Configuration:
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/crawler
spring.datasource.username=root
spring.datasource.password=root

#JPA Configuration:
spring.jpa.database=MySQL
spring.jpa.show-sql=true

数据库表

CREATE TABLE `job_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `company_name` varchar(100) DEFAULT NULL COMMENT '公司名称',
  `company_addr` varchar(200) DEFAULT NULL COMMENT '公司联系方式',
  `company_info` text COMMENT '公司信息',
  `job_name` varchar(100) DEFAULT NULL COMMENT '职位名称',
  `job_addr` varchar(50) DEFAULT NULL COMMENT '工作地点',
  `job_info` text COMMENT '职位信息',
  `salary_min` int(10) DEFAULT NULL COMMENT '薪资范围,最小',
  `salary_max` int(10) DEFAULT NULL COMMENT '薪资范围,最大',
  `url` varchar(150) DEFAULT NULL COMMENT '招聘信息详情页',
  `time` varchar(10) DEFAULT NULL COMMENT '职位最近发布时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='招聘信息';

实体类

@Entity
public class JobInfo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String companyName;
    private String companyAddr;
    private String companyInfo;
    private String jobName;
    private String jobAddr;
    private String jobInfo;
    private Integer salaryMin;
    private Integer salaryMax;
    private String url;
    private String time;
    get/set
}

Dao

public interface JobInfoDao extends JpaRepository<JobInfo, Long>{
}

Service

public interface JobInfoService {

    /**
     * 保存数据
     *
     * @param jobInfo
     */
    public void save(JobInfo jobInfo);

    /**
     * 根据条件查询数据
     *
     * @param jobInfo
     * @return
     */
    public List<JobInfo> findJobInfo(JobInfo jobInfo);
}

ServiceImpl

@Service
public class JobInfoServiceImpl implements JobInfoService {

    @Autowired
    private JobInfoDao jobInfoDao;

    @Override
    @Transactional
    public void save(JobInfo jobInfo) {
        //先从数据库查询数据,根据发布日期查询和url查询
        JobInfo param = new JobInfo();
        param.setUrl(jobInfo.getUrl());
        param.setTime(jobInfo.getTime());
        List<JobInfo> list = this.findJobInfo(param);
        if (list.size() == 0) {
            //没有查询到数据则新增或者修改数据
            this.jobInfoDao.saveAndFlush(jobInfo); 
        }
    }

    @Override
    public List<JobInfo> findJobInfo(JobInfo jobInfo) {
        Example example = Example.of(jobInfo);
        List<JobInfo> list = this.jobInfoDao.findAll(example);
        return list;
    }
}

启动类

@SpringBootApplication
@EnableScheduling//开启定时任务
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

功能实现

@Component
public class JobProcessor implements PageProcessor {

    @Autowired
    private DataPipline dataPipline;

    private String url = "https://search.51job.com/list/010000,000000,0000,00,9,99,python,2,1.html?lang=c&stype=&postchannel=0000&workyear=99&cotype=99&degreefrom=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) {
        List<Selectable> list = page.getHtml().css("div#resultList div.el").nodes();
        if (list.size()==0){
            //详情页
            this.saveJobInfo(page);
        }else {
            //列表页
            for (Selectable selectable : list) {
                //获取url地址
                String jobInfoUrl = selectable.links().toString();
                //把url放入任务队列中
                page.addTargetRequest(jobInfoUrl);
            }
            //获取下一页url
            String s = page.getHtml().css("div.p_in li.bk").nodes().get(1).links().toString();
            //下一页放入任务队列
            page.addTargetRequest(s);
        }
    }

    //解析招聘详情信息,对数据进行封装
    public void saveJobInfo(Page page){
        Html html = page.getHtml();
        if (html==null){
            return;
        }

        JobInfo jobInfo = new JobInfo();

        String companyName = html.css("div.cn p.cname a", "text").toString();
        jobInfo.setCompanyName(companyName);
        String companyInfo = Jsoup.parse(html.css("div.tmsg").toString()).text();
        jobInfo.setCompanyInfo(companyInfo);
        String jobName = Jsoup.parse(html.css("div.cn h1").toString()).text();
        jobInfo.setJobName(jobName);
        String text = Jsoup.parse(html.css("p.ltype").toString()).text();
        String address = text.substring(0, 2);
        //String jobAddr = Jsoup.parse(html.css("div.bmsg p.fp").nodes().get(1).toString()).text();
        jobInfo.setJobAddr(address);
        String jobInfos = Jsoup.parse(html.css("div.job_msg").toString()).text();
        jobInfo.setJobInfo(jobInfos);
        String salary = html.css("div.cn strong", "text").toString();
        if (StringUtils.isNotBlank(salary)){
            Integer[] texts = MathSalary.getSalary(salary);
            jobInfo.setSalaryMin(texts[0]);
            jobInfo.setSalaryMax(texts[1]);
        }
        String url = page.getUrl().toString();
        jobInfo.setUrl(url);
        String times = Jsoup.parse(html.css("p.ltype").toString()).text();
        String[] split = times.split("-");
        String time = split[0].substring(split[0].length() - 2) +"-"+ split[1].substring(0, 2);
        jobInfo.setTime(time);

        //保存(内存)
        page.putField("jobInfo",jobInfo);


    }

    private Site site = Site.me()
            .setCharset("gbk")  //编码
            .setTimeOut(20*1000)  //超时
            .setRetryTimes(5)  //重试次数
            .setRetrySleepTime(3000);   //重试时间间隔

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

    //等待一秒后执行  一百秒执行一次
    @Scheduled(initialDelay = 1000,fixedDelay = 100000)
    public void process(){
        Spider.create(new JobProcessor())
                .addUrl(url)
                .setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(100000)))    //布隆过滤器
                .thread(10)    //10线程爬取
                .addPipeline(this.dataPipline)  //添加Pipeline配置
                .run();
    }
}

使用和定制Pipline输出

在WebMagic中,Pileline是抽取结束后,进行处理的部分,它主要用于抽取结果的保存,也可以定制Pileline可以实现一些通用的功能。在这里我们会定制Pipeline实现数据导入到数据库中。

public interface Pipeline {
    // ResultItems保存了抽取结果,它是一个Map结构,
    // 在page.putField(key,value)中保存的数据,
    //可以通过ResultItems.get(key)获取
    public void process(ResultItems resultItems, Task task);
}

在WebMagic里,一个Spider可以有多个Pipeline,使用Spider.addPipeline()即可增加一个Pipeline。这些Pipeline都会得到处理,例如可以使用:

spider.addPipeline(new ConsolePipeline())
    .addPipeline(new FilePipeline())

实现输出结果到控制台,并且保存到文件的目标。

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

自定义Pipeline导入保存数据

@Component
public class SpringDataPipeline implements Pipeline {

    @Autowired
    private JobInfoService jobInfoService;

    @Override
    public void process(ResultItems resultItems, Task task) {
        //获取需要保存到MySQL的数据
        JobInfo jobInfo = resultItems.get("jobInfo");

        //判断获取到的数据不为空
        if(jobInfo!=null) {
            //如果有值则进行保存
            this.jobInfoService.save(jobInfo);
        }
    }
}

启动启动类

输出结果:


尾言

这里记录了博主学习得一个爬取例子,如果可以,以后直接参考这篇文章即可完成对简单页面得爬取。ok!

上一篇下一篇

猜你喜欢

热点阅读