欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

WebMagic爬虫Demo

程序员文章站 2022-07-14 11:25:36
...

各位看官可以关注博主个人博客,了解更多信息。
作者:Surpasser
链接地址:https://surpass.org.cn

前言

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爬虫Demo

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>
    

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

解决方案:

  1. 等作者的0.7.4的版本发布

  2. 直接从github上下载最新的代码,安装到本地仓库

这是开发这自己得解决方法: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);
        }
    }
}

启动启动类

输出结果:
WebMagic爬虫Demo

尾言

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