Veiking百草园


老狗啃爬虫-从抓取到存储之Pipeline

老狗啃骨头   @Veiking   2020-12-16

老狗啃爬虫-从抓取到存储之Pipeline

摘要:

在爬虫框架WebMagic中,用于保存结果的组件叫做Pipeline。在WebMagic已经实现了的Pipeline接口中,如果我们仅仅是想把抓取数据进行控制台输出,我们可以借助它的ConsolePipeline;如果我们想将数据以文件的形式进行存储,即可借助它的FilePipeline。如果我们想实现自己想要的具体功能,我们就得定制我们所需的Pipeline

  经过前面的几篇文章,我们已经可以将页面上的信息成功的抓取到手,接下来就要面临存储问题。
  这里我们准备使用的数据库是Mysql,持久化将借助Mybatis,即springboot集成mybatis的形式来存储我们的数据。

Pipeline

  在爬虫框架WebMagic中,用于保存结果的组件叫做Pipeline,我们需要定制Pipeline来实现我们需要的功能。
  我们可以看到源码,在WebMagic已经实现了的Pipeline接口中,如果仅仅是想把抓取数据进行控制台输出,我们可以借助它的ConsolePipeline;如果只是想将数据以文件的形式进行存储,即可借助它的FilePipeline,如此等等。如果我们想实现自己想要的其他功能,我们就得定制我们自己的Pipeline。

准备工作

  由于我们要用Mysql和Mybatis,所以,在原有的基础上,我们还需要一些支持这些功能的jar包,这时候我们就需要修改下pom.xml文件,dependencies里添加如下代码:

    <!-- mysql-mybatis -->
    <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.21</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.23</version>
    </dependency>

  留意下这里引入的依赖包,其中mybatis-spring-boot-starter的引入,同时会自动引入诸如mybatis、mybatis-spring、mysql-connector-java等在功能支撑上所需的很多依赖包,免去了我们注意配置的很多麻烦,这种方式相对来说就方便得多。
  此外,还要留意下druid-spring-boot-starter,Druid是由阿里提供的一个JDBC组件,借助其可以帮助程序高效的管理数据库连接池,同时还提供数据库访问性能的监控等功能等。这里我们用到的只是它数据库连接池的这块功能,其他的感兴趣可以去深入去了解下。
  改完pom.xml配置文件,更新项目,jar包依赖这块准备工作已经完成。
  接下来再看数据存储,既然我们要用到数据库了,肯定得找地方给项目设置数据库访问的地址、用户密码等参数,springboot有两种参数配置的形式,一种是在程序代码中,一种是配置文件。Springboot启动默认读取的配置文件是需要在src/main/resources文件目录下设置的application文件,文件类型可以是传统的.properties文件格式,也可以是.yml文件格式,我们选用层级结构上可视化更为友好的.yml,当然两种配置文件格式功能上完全相同。
  于是我们创建application.yml文件,并填入以下内容:

spring:
 # 数据库参数
 datasource:
  url: jdbc:mysql://192.168.0.101:3306/v_webmagic?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=UTC
  username: root
  password: ******

  这里,.yml文件格式我们要注意下他的格式缩进形式,我们用的是空格缩进。先配置上数据库所需的这些参数,然后进行下一步的准备。
  此外,这里还要补加一个小功能,就是lombok,由于接下来我们要涉及到java数据对象的操作,lombok即可帮助我们在Java 对象(POJO)的使用上让代码更为简洁,可能一大串的get、set代码,我们借助lombok只需要一个@Data标签即可,总之一句话:代码会漂亮很多。额外提示,eclipse中lombok的使用,除了代码中需要引入依赖,还需要安装一下插件,这个自行安排下,也很简单。
  好了,基本上开发程序的准备工作都已完成,接下来我们进入编程环节。

实现功能

  回顾我们之前的爬虫例子,抓取了一个屈原屈先生的资料页面,这次我们要用数据库存储,就不能只抓这一个页面了,我们就用屈原先生页面的链接,作为种子URL,顺藤摸瓜,爬取10个不同人物的信息,然后存储至数据库。

第一步:数据库建表

  我们再看看我们的抓取目标,人物页面,里面可以提出来的有效信息,我们就取主要的三个,即人物名称、朝代、简介。然后,我们在名为v_webmagic数据库里创建一张,用来承接存储功能的表,创建代码如下:

DROP TABLE IF EXISTS `aigufeng_celebrity`;
CREATE TABLE `aigufeng_celebrity` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `dynasty` varchar(16) DEFAULT NULL,
  `content` varchar(512) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  库表创建完成,我们就要准备java程序部分了。

第二步:Java 对象

  既然要做数据持久化,我们就要抽象出来一个Java 对象(POJO),于是我们根据页面人物信息的人特点,创建一个人物信息类:

package cn.veiking.biz.model;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* @author    :Veiking
* @version    :2020年12月5日
* 说明        :爱古风人物信息
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AigufengCelebrity implements Serializable{
    private static final long serialVersionUID = 1L;
    private Integer id;
    private String name;
    private String dynasty;
    private String content;
}

  这时候,我们留意下这三个标签:@Data、@NoArgsConstructor、@AllArgsConstructor,其中@Data的含义是,lombok已经帮我们处理了getter、setter,@NoArgsConstructor表示提供了无参构造;@AllArgsConstructor表示提供了全参构造。简洁明了的三个标签,省去了很多代码,是不是很方便。

第三步:DAO

  接着我们创建一个dao文件,即AigufengCelebrityDao.java,专门来处理从爱古风的人物信息页面抓取的数据持久化,我们用注解式的编程写法,代码如下:

package cn.veiking.biz.dao;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import cn.veiking.biz.model.AigufengCelebrity;

/**
* @author    :Veiking
* @version    :2020年12月5日
* 说明        :爱古风-人物信息数据持久化接口
*/
@Mapper
public interface AigufengCelebrityDao {
    /************** BASE **************/
    @Insert(value = " INSERT INTO aigufeng_celebrity (`name`, `dynasty`, `content`) VALUES (#{model.name}, #{model.dynasty}, #{model.content}) ")
    public boolean add(@Param("model") AigufengCelebrity model);
}

  有没有发现,相对以前习惯性的配置一个mapper.xml文件,注解式编程的代码是不是看起来更为简洁?其实绝大部分使用场景,注解式是完全可取的,其SQL的使用方式跟xml是一样的,考虑到这种代码形式展示直观上的一些局限性,太过复杂的数据库操作、或者可能频繁变动的功能,还是可以结合配置相对应mapper.xml的方式来实现,两种形式也是可以并存互相配合补充的。

第四步:Pipeline,创建数据处理管道

  这一步,我们要创建处理数据的Pipeline,我们要将一个得到的数据对象,通过DAO,保存至数据库,实现代码如下:

package cn.veiking.spider;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.biz.dao.AigufengCelebrityDao;
import cn.veiking.biz.model.AigufengCelebrity;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;

/**
* @author    :Veiking
* @version    :2020年12月6日6
* 说明        :爱古风-人物信息数据持久化管道
*/
@Service
public class AigufengCelebrityPipeline implements Pipeline{
    SimLogger logger = new SimLogger(this.getClass());

    @Autowired
    private AigufengCelebrityDao aigufengCelebrityDao;

    @Override
    public void process(ResultItems resultItems, Task task) {
        AigufengCelebrity model = resultItems.get("aigufengCelebrity");
        logger.info("AigufengCelebrityPipeline process [model={}] ", model);
        if(model != null) {
            this.aigufengCelebrityDao.add(model);
            logger.info("AigufengCelebrityPipeline insert into DB ... ");
        }
    }
}

  这里注意 @Service标签,我们在程序启动的时候,要将此类以一个服务的形式注册到spring容器中。在这个Pipeline中,如有相应的AigufengCelebrity数据,我们将执行数据入库的操作。

第五步:PageProcessor大升级

  我们前面的例子,写过一个简单的PageProcessor,成功抓取了页面内容,这时候我们就在其基础上稍稍升级一下,以便能突出爬虫的功能特点,以及Pipeline在这里扮演的作用代码如下:

package cn.veiking.spider;

import java.util.List;

import org.springframework.stereotype.Service;

import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.biz.model.AigufengCelebrity;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;

/**
* @author    :Veiking
* @version    :2020年12月6日
* 说明        :爱古风-人物信息数据获取
*/
@Service
public class AigufengCelebrityProcessor implements PageProcessor {
    SimLogger logger = new SimLogger(this.getClass());

    // 抓取目标也
    private static final String TargetUrl = "http://www\\.aigufeng\\.com/celebrity/article-\\d+/page\\.html";
    // 抓取统计,我们只抓取10条记录
    private static int count = 0;

    @Override
    public Site getSite() {
        Site site = Site.me().setRetryTimes(5).setSleepTime(1000).setTimeOut(10000);
        return site;
    }

    //
    @Override
    public void process(Page page) {
        logger.info("AigufengCelebrityProcessor process");
        if(page.getUrl().regex(TargetUrl).match()) {
            // 获取前后翻页页面连接并添加至待抓序列 
            List links= page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/div[2]/div[2]/div[1]").links().regex(TargetUrl).all();
            page.addTargetRequests(links);
            // 获取页面信息
            String name = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/div[2]/div[1]/h1/a/text()").get();
            String dynasty = page.getHtml().xpath("//*[@id=\"content\"]/div/div[1]/div[2]/div[1]/h2/span/a/text()").get().replace("(", "").replace(")", "");
            String content = page.getHtml().xpath("//*[@id=\"icontentContent\"]/text()").get();
            // 页面信息加工备用
            AigufengCelebrity model = new AigufengCelebrity();
            model.setName(name);
            model.setDynasty(dynasty);
            model.setContent(content);
            page.putField("aigufengCelebrity", model);
            count ++;
            // 程序自爆,运行终止
            if(count>10) {
                System.exit(0);
            }
        }
    }
}

  这里设置的参数TargetUrl,是通过正则表达式,用来做URL校验匹配的,如获取到的链接符合此规则,则添加至待抓取序列
  这里又设置了个标记,因为我们是测试,抓取十条满足测试需要即可;实际上如果不做逻辑限制,理论上这个程序会沿着前后翻页的逻辑,顺藤摸瓜穷尽爱古风网站所有人物信息的抓取,这里我们重在测试功能,不做验证。并且WebMagic这里也已经处理了简单的URL重复问题,我们只需要将Processor的重点关注在具体的数据抓取操作上。

第六步:完成测试

  本次测试我们添加了功能组件Pipeline,又增加了数据持久化的接口服务,如想顺利启动并使用,我们须将这些组件注册至spring容器中,这里叫要在测试启动入口类StartTest这里添加几个注解,整个代码升级如下:

package cn.veiking;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

/**
* @author    :Veiking
* @version    :2020年11月30日
* 说明        :测试启动入口
*/
@SpringBootApplication
//开启通用注解扫描

@MapperScan(value = {"cn.veiking.biz.dao"})
@ComponentScan(value = {"cn.veiking.processor"})
@ComponentScan(value = {"cn.veiking.spider.*"})
public class StartTest {
    public static void main(String[] args) {
        SpringApplication.run(StartTest.class, args);
    }
}

  @MapperScan的意思是扫描这个路径下的java类并注入spring容器,对应的接口文件亦须添加与之对应的@Mapper标签;
  @ComponentScan之前我们已经了解过了,即需要添加包路径扫描cn.veiking.spider.*,所有spider下的java类都将在spring容器中注册成相应的服务。
  接下来我们升级测试主程序,代码如下:

package cn.veiking.aigufeng;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import cn.veiking.StartTest;
import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.spider.AigufengCelebrityPipeline;
import cn.veiking.spider.AigufengCelebrityProcessor;
import us.codecraft.webmagic.Spider;

/**
* @author    :Veiking
* @version    :2020年12月8日
* 说明        :aigufengCelebrityPipeline、aigufengCelebrityProcessor 测试
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartTest.class)
public class AigufengCelebrityTest {
    SimLogger logger = new SimLogger(this.getClass());

    @Autowired
    private AigufengCelebrityPipeline aigufengCelebrityPipeline;
    @Autowired
    private AigufengCelebrityProcessor aigufengCelebrityProcessor;

    private static final String StartUrl = "http://www.aigufeng.com/celebrity/article-1000056/page.html";

    @Test
    public void testSpider() {
        long startTime, endTime;
        logger.info("AigufengCelebrityTest testSpider [start={}] ", "开始爬取数据");
        startTime = System.currentTimeMillis();
        Spider.create(aigufengCelebrityProcessor)
        .addUrl(StartUrl)
        .addPipeline(aigufengCelebrityPipeline)
        .thread(3)
        .run();

        endTime = System.currentTimeMillis();
        logger.info("AigufengCelebrityTest testSpider [end={}] ", "爬取结束,耗时约" + ((endTime - startTime) / 1000) + "秒");
    }
}

  这里,留意.addPipeline()方法,已经将Pipeline添加至Spider。
  此外,根据PageProcessor里的升级变动,这里的StartUrl,已经具有种子URL的功能。
  所有程序代码完成准备,运行测试…
  我们看到Console窗口滚动的日志打印,数据已经陆陆续续被成功抓取,添加至数据库:



  然后等程序运行结束,我们去数据库刷新一下,可以看到预期的数据已经在库里了:



  好了,我们针对爬虫框架WebMagic的Pipeline学习测试已经完成。

总结

  本篇我们借助Pipeline实现了抓取数据的入库,通过对Pipeline的学习了解,我们可以发现,在WebMagic处理数据的过程中,PageProcessor和Pipeline在逻辑上是相互独立的:PageProcessor专注于处理数据的解析抓取;Pipeline负责所得数据的处理,Spider像计算机的CPU一样,将他们这些功能组件组合起来,来处理整个程序的创建和运行等。
  接下来之后,我们再看看WebMagic其他组件的使用和爬虫相关的使用技巧。


老狗啃骨头



慷慨发言

(您提供的信息将用于后续必要的反馈联系,本站会恪守隐私)

潜影拾光

羊卓雍错

美丽羊湖,藏南明珠。

扫码转发

二维码
二维码
二维码
二维码
二维码
二维码

博文标签