老狗啃爬虫-从抓取到存储之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其他组件的使用和爬虫相关的使用技巧。