美文网首页小菜鸟
WebMagic爬虫Demo

WebMagic爬虫Demo

作者: 我是一颗小虎牙_ | 来源:发表于2020-05-14 13:55 被阅读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!

    相关文章

      网友评论

        本文标题:WebMagic爬虫Demo

        本文链接:https://www.haomeiwen.com/subject/fadtohtx.html