WebMagic是一款简单灵活的爬虫框架。基于它你可以很容易的编写一个爬虫。
简介
WebMagic项目代码分为核心和扩展两部分。核心部分(webmagic-core)是一个精简的、模块化的爬虫实现,而扩展部分则包括一些便利的、实用性的功能。WebMagic的架构设计参照了Scrapy,目标是尽量的模块化,并体现爬虫的功能特点。
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进行分布式管理。
除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。
4.Pipeline
Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。
Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。
主要部分
WebMagic主要包括两个包,这两个包经过广泛实用,已经比较成熟:
webmagic-core
webmagic-core是WebMagic核心部分,只包含爬虫基本模块和基本抽取器。WebMagic-core的目标是成为网页爬虫的一个教科书般的实现。
webmagic-extension
webmagic-extension是WebMagic的主要扩展模块,提供一些更方便的编写爬虫的工具。包括注解格式定义爬虫、JSON、分布式等支持。
Maven依赖
WebMagic基于Maven进行构建,推荐使用Maven来安装WebMagic。在你自己的项目(已有项目或者新建一个)中添加以下依赖即可:
<!--web magic爬虫-->
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.7.3</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.7.3</version>
</dependency>
第一个爬虫项目
这里我以领导留言板的案例举例
网站页面如上,代码如下
Spider
Spider是爬虫启动的入口。在启动爬虫之前,我们需要使用一个PageProcessor创建一个Spider对象,然后使用run()进行启动。同时Spider的其他组件(Downloader、Scheduler、Pipeline)都可以通过set方法来进行设置。
package com.zcf.spider.crawler;
import com.zcf.common.config.HttpConfig;
import com.zcf.spider.webmagic.DataPipeline;
import com.zcf.spider.webmagic.LiuYanBanProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.model.HttpRequestBody;
import us.codecraft.webmagic.utils.HttpConstant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Description TODO
* @Author zcf
* @Date 2020/5/20
**/
@Component
public class LiuYanBanCrawler {
private static final Logger LOG = LoggerFactory.getLogger(LiuYanBanCrawler.class);
@Autowired
private LiuYanBanProcessor liuYanBanProcessor;
@Autowired
private DataPipeline dataPipeline;
/**
* @Scheduled(cron = "0 0 0/2 * * ? ") // 每2个小时执行一次
* @Scheduled(cron = "0 0/1 * * * ?") // 每1分钟执行一次
* @Scheduled(cron = "0/30 * * * * ?") // 每30秒执行一次
*/
@Async("taskExecutor")
@Scheduled(cron = "0 0 0/2 * * ? ")
public void governmentLiuYanBan(){
LOG.info("执行开始时间:{}",new Date());
int fid = 561;
Map<String,Object> map = new HashMap<>();
map.put("fid",fid);
map.put("lastItem",0);
String referer_url = "http://liuyan.people.com.cn/threads/list?fid="+fid;
Request request = getRequest(map, referer_url);
try {
Thread.sleep(1000*10); // 休眠10秒
Spider.create(liuYanBanProcessor)
.addRequest(request)
.thread(10)
.addPipeline(dataPipeline)
.run();
LOG.info("执行结束时间:{}",new Date());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private Request getRequest(Map<String, Object> map, String referer_url){
Request request = new Request();
request.setMethod(HttpConstant.Method.POST);
request.addHeader("Referer",referer_url);
request.addHeader("Origin","http://liuyan.people.com.cn");
request.addHeader("User-Agent",HttpConfig.USER_AGENT);
request.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//构造map用于第一条请求的请求体
request.setRequestBody(HttpRequestBody.form(map,"utf-8"));
request.setUrl("http://liuyan.people.com.cn/threads/queryThreadsList");
return request;
}
}
Site
Site对站点本身的一些配置信息,例如编码、HTTP头、超时时间、重试策略等、代理等,都可以通过设置Site对象来进行配置。
package com.zcf.spider.webmagic;
import com.alibaba.fastjson.JSONObject;
import com.zcf.common.config.HttpConfig;
import com.zcf.utils.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.model.HttpRequestBody;
import us.codecraft.webmagic.processor.PageProcessor;
import java.util.*;
/**
* @Description TODO
* @Author zcf
* @Date 2020/5/19
**/
@Component
public class LiuYanBanProcessor implements PageProcessor {
private static final Logger LOG = LoggerFactory.getLogger(LiuYanBanProcessor.class);
@Override
public void process(Page page) {
List<Map<String,Object>> infoList = new ArrayList<>();
// 获取返回结果
JSONObject result = JSONObject.parseObject(page.getJson().toString());
Boolean flag = (Boolean) result.get("success");
if(flag) {
List<Map<String,Object>> responseData = (List) result.get("responseData");
for (Map<String, Object> map : responseData) {
Map<String, Object> info = new HashMap<>();
Integer threadsCheckTime = (Integer) map.get("threadsCheckTime"); //时间戳
String subject = (String) map.get("subject"); //标题
Integer tid = (Integer) map.get("tid"); //文章ID
String url = "http://liuyan.people.com.cn/threads/content?tid=" + tid; //文章链接
info.put("eventDate",threadsCheckTime);
info.put("subject",subject);
info.put("id",tid);
info.put("url",url);
LOG.info("时间戳的信息:{}",threadsCheckTime);
LOG.info("标题的信息:{}",subject);
LOG.info("文章ID的信息:{}",tid);
LOG.info("文章链接的信息:{}",url);
LOG.info("--------------------------------------------------------");
infoList.add(info);
}
Map<String,Object> map = new HashMap<>();
map.put("fid",responseData.get(responseData.size()-1).get("fid"));
map.put("lastItem",responseData.get(responseData.size()-1).get("tid"));
Integer threadsCheckTime = (Integer) responseData.get(responseData.size() - 1).get("threadsCheckTime");
Date date1 = DateUtil.timestampToDateBySecond(Long.valueOf(threadsCheckTime));
Date date2 = DateUtil.formatDateToDate(new Date());
if(DateUtil.compareToDate(date1, date2)){
Request request = page.getRequest();
request.setRequestBody(HttpRequestBody.form(map,"utf-8"));
page.addTargetRequest(request);
LOG.info("开始下一页");
}
}
page.putField("data",infoList);
}
//配置爬虫信息
private Site site = Site.me()
.setUserAgent(HttpConfig.USER_AGENT)
.setCharset("utf8")
.setTimeOut(10 * 1000)
.setRetryTimes(3)
.setRetrySleepTime(3000);
@Override
public Site getSite() {
return site;
}
}
Pipeline
WebMagic用于保存结果的组件叫做Pipeline。
package com.zcf.spider.webmagic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @Description TODO
* @Author zcf
* @Date 2020/5/19
**/
@Component
public class DataPipeline implements Pipeline {
private static final Logger LOG = LoggerFactory.getLogger(DataPipeline.class);
@Override
public void process(ResultItems resultItems, Task task) {
List<Map<String,Object>> data = resultItems.get("data");
LOG.info("总数据一共有{}条",data.size());
// 后续数据的存储到数据库
for (Map<String, Object> datum : data) {
Set set = datum.keySet();
LOG.info("--------------------------------------------------------");
for (Object o : set){
LOG.info("数据参数:{}的信息:{}",o,datum.get(o));
}
}
}
}
运行一下,查看一下效果
在这里我想获取的数据都已经获取到了,当然每个页面的解析都不一样。
Jsoup是一个简单的HTML解析器,同时它支持使用CSS选择器的方式查找元素。
Xsoup是基于Jsoup开发的一款XPath解析器。
具体我们也可以参考官网。