webmagic源码浅析
webmagic简介
webmagic可以说是中国传播度最广的Java爬虫框架,https://github.com/code4craft/webmagic,阅读相关源码,获益良多。阅读作者博客【代码工匠】,能够领略到一个IT工作者的工匠精神,希望以后成为他这样的开源贡献者。Webmagic的文档也是写得非常漂亮,这里就不具体讲它的使用方法了,见官方文档
webmagic核心架构
webmagic帮我们做了几个核心的事情:
1.线程池封装,不用手动控制采集线程
2.url调度,实现了生产者消费者模型
3.封装下载器组件(downloader),解析组件,持久化。 见官方文档
4.支持注解
简单案例
借用一段官方案例,快速入门,便于后面的理解。开启一个爬虫,只需要简单几步,编写页面解析器,写具体的解析方法。新建Spider实例,添加至少一个种子URL,设置其他可选属性,最后调用run()方法,或者start(),start()方法内部会为spider单独开启一个线程,使得爬虫与主线程异步。
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.processor.PageProcessor;
public class GithubRepoPageProcessor implements PageProcessor {
private Site site = Site.me().setRetryTimes(3).setSleepTime(100);
@Override
public void process(Page page) {
//将提取的url加入page对象暂存,最终会加入到
page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());
page.putField("author", page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
if (page.getResultItems().get("name")==null){
//skip this page
page.setSkip(true);
}
page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));
}
@Override
public Site getSite() {
return site;
}
public static void main(String[] args) {
//官方链式调用,拆解到下面方便理解
//Spider.create(new GithubRepoPageProcessor()).addUrl("https://github.com/code4craft").thread(5).run();
//创建线程
Spider spider = Spider.create(new GithubRepoPageProcessor());
//添加采集种子URL
spider.addUrl("https://github.com/code4craft");
//设置线程数
spider.thread(5);
//启动爬虫//run()方法既可以看作多线程中的Runnable接口方法,也可以直接运行,是爬虫的核心方法
spider.run();
}
}
Spider类属性
爬虫的核心是us.codecraft.webmagic.Spider类,看看Spider类中都有哪些重要属性
属性列表:
public class Spider implements Runnable, Task {
//下载器对象
protected Downloader downloader;
//持久化统一处理器,可以有多个
protected List<Pipeline> pipelines = new ArrayList<Pipeline>();
//页面解析器
protected PageProcessor pageProcessor;
//种子请求(这个地方看着种子请求也不是很对,因为spider对象在没开始运行时,仍然可以使用addRequest,addUrl添加url )
protected List<Request> startRequests;
//浏览器信息对象
protected Site site;
//爬虫任务标识
protected String uuid;
//任务调度器,默认是JDK中的LinkedBlockingQueue的实现
protected Scheduler scheduler = new QueueScheduler();
protected Logger logger = LoggerFactory.getLogger(getClass());
//线程池(自己封装的一个模型,内部的execute方法实际是executorService的execute实现添加线程的作用)
protected CountableThreadPool threadPool;
//执行管理器对象(和线程池配合使用)
protected ExecutorService executorService;
//线程数,控制采集并发
protected int threadNum = 1;
//爬虫任务运行状态
protected AtomicInteger stat = new AtomicInteger(STAT_INIT);
//是否采集完成退出
protected boolean exitWhenComplete = true;
protected final static int STAT_INIT = 0;
protected final static int STAT_RUNNING = 1;
protected final static int STAT_STOPPED = 2;
//是否回流url,spawn产卵的意思。个人觉得这个参数很多余,不想采集继续下去,可以别把url加入队列
protected boolean spawnUrl = true;
//退出时是否回收处理
protected boolean destroyWhenExit = true;
//控制新生成url锁
private ReentrantLock newUrlLock = new ReentrantLock();
//控制新生成url锁,配合newUrlLock 使用
private Condition newUrlCondition = newUrlLock.newCondition();
//监听器集合,请求爬去成功或者失败时,可以通过注入监听器分别实现onSuccess和onError方法
private List<SpiderListener> spiderListeners;
//采集页面数统计(只代表请求的次数,不代表成功抓取数)
private final AtomicLong pageCount = new AtomicLong(0);
//爬取开始时间
private Date startTime;
//调度器队列中的URL已经被消费光,且采集线程未执行完成,仍然可能生产URL到调度器队列中时,线程最多wait 30秒
private int emptySleepTime = 30000;
threadNum 这里Spider本身实现了Runnable接口,可以作为一个独立的线程开启,当然它的线程控制不仅于此,这里有一个属性threadNum才是控制采集线程数的,后面再细说。
scheduler 对象做为调度器,内部采用队列维护了一个实现生产者消费者模型,爬取的过程中,可以将采集的url提取到scheduler的队列中,线程会持续不断的消费scheduler 的队列中消费。
pageProcessor 用于用户自定义页面解析规则,定义具体的解析逻辑,新建Spider实例的方式仅两种,public static Spider create(PageProcessor pageProcessor)
和构造方法public Spider(PageProcessor pageProcessor)
create方法内部只是调用了一下构造方法。构造一个spider对象都需要一个自定义的解析器,不同页面,解析逻辑不相同,PageProcessor接口中。spider会调用PageProcessor的process方法,这是一个策略设计模式。
uuid 这个名字可能让人误会,和平时uuid不是一个含义,这个属性是一个爬虫进程的唯一标识
其他属性 比较重要的属性还包括threadPool,executorService,控制多线程并发,浏览器对象site,对于有些反爬策略的网站,该对象可以用于模拟浏览器,达到反反爬虫
的作用。
Spider核心方法run()
@Override
public void run() {
checkRunningStat();//检查爬虫运行状态,防止run方法被调用多次
initComponent();//初始化
logger.info("Spider {} started!",getUUID());
while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) {
//循环消费Request,url在放入scheduler时,已经封装为Request对象了
final Request request = scheduler.poll(this);
if (request == null) {
if (threadPool.getThreadAlive() == 0 && exitWhenComplete) {
//threadPool.getThreadAlive()线程池中仍然还有存活线程,那么存活线程可能会生产出新的url来
//exitWhenComplete默认为true,
//exitWhenComplete如果为false,线程等待新URL,
//如果队列(自定义队列)能实现动态添加url,那就可以实现动态添加采集任务的功能
break;
}
// wait until new url added
//等待存活的线程生产新的url
waitNewUrl();
} else {
//将request封装为线程,加入线程队列,线程池会根据设置的并行参数threadNum,并行执行
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
processRequest(request);//执行请求
onSuccess(request);//调用执行成功的方法
} catch (Exception e) {
onError(request);
logger.error("process request " + request + " error", e);
} finally {
pageCount.incrementAndGet();
signalNewUrl();
}
}
});
}
}
stat.set(STAT_STOPPED);
// release some resources
if (destroyWhenExit) {
close();
}
logger.info("Spider {} closed! {} pages downloaded.", getUUID(), pageCount.get());
}
核心方法的流程还是比较简答的,checkRunningStat()
会先检查一下爬虫是否已经启动,这有点儿像多线程中的开启线程的start()方法,两次开启是不允许的。
然后初始化方法initComponent()
各种组件,在initComponent()
方法中,加入startRequests中的Request,实际上在Spider启动之前可以调用addUrl(String... urls)
和addRequest(Request... requests)
方法直接将请求加入到队列中,startRequests和后面那种添加url的方法缺少了一定的一致性。
后面一个循环消费的过程,正如我注释里写的那样,如果队列中url被消费完毕,且没有正在被消费的存活的线程了,且完成采集退出属性exitWhenComplete为true(exitWhenComplete默认为true,设置为false则进程将会一直挂起),就会跳出死循环,采集结束,反之,如果依然有线程存活,或者exitWhenComplete为false,那么线程waitNewUrl()等待,在exitWhenComplete为false的情况,进程就不会自动停止了,除非强杀了,这种设计在分布式的模式下才显得有意义,可以动态添加url到队列中去。
private void waitNewUrl() {
newUrlLock.lock();
try {
// double check
if (threadPool.getThreadAlive() == 0 && exitWhenComplete) {
return;
}
//默认是30秒后自动苏醒,可以通过设置emptySleepTime属性,控制自动苏醒的时间
newUrlCondition.await(emptySleepTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
logger.warn("waitNewUrl - interrupted, error {}", e);
} finally {
newUrlLock.unlock();
}
}
后面使用threadPool执行一个新的子线程。new Runnable构造的匿名内部类会通过threadPool开启一个新的子线程,执行请求processRequest(request)
,执行成功就调用onSuccess(request)
,失败就调用onError(request)
,接着finally代码块中的内容是非常重要的,统计请求的页面次数(无论失败或者成功),signalNewUrl()唤醒等待的线程,这里要和前面waitNewUrl()结合起来看,两者使用同一个锁,waitNewUrl()作为父线程,默认会自动苏醒,但调用signalNewUrl()的用意在于,可能这个子线程已经又生成新的URL放到队列中了,就不用再等30秒了。
private void signalNewUrl() {
try {
newUrlLock.lock();
newUrlCondition.signalAll();
} finally {
newUrlLock.unlock();
}
}
后面的代码则是爬虫结束的操作,这种情况只有前文提到的跳出死循环,采集结束,结束前设置了一下状态,做了一下close()操作
调度器Scheduler
调度器在webmagic中扮演的角色是非常重要的,说来功能也不算太复杂,实现生产者-消费者模式,顺便去重。scheduler默认为QueueScheduler ,在scheduler声明的时候就直接新建了这个实例
public class QueueScheduler extends DuplicateRemovedScheduler implements MonitorableScheduler {
//LinkedBlockingQueue队列存url
private BlockingQueue<Request> queue = new LinkedBlockingQueue<Request>();
@Override 覆盖父类DuplicateRemovedScheduler 的方法
public void pushWhenNoDuplicate(Request request, Task task) {
queue.add(request);
}
@Override 实现DuplicateRemovedScheduler 不完全实现Scheduler的poll方法
public Request poll(Task task) {
return queue.poll();
}
@Override //实现MonitorableScheduler 的方法
public int getLeftRequestsCount(Task task) {
return queue.size();
}
@Override //实现MonitorableScheduler 的方法
public int getTotalRequestsCount(Task task) {
return getDuplicateRemover().getTotalRequestsCount(task);
}
}
以上代码,可以看到QueueScheduler的构成,QueueScheduler继承了抽象类DuplicateRemovedScheduler 实现了接口MonitorableScheduler 接口,DuplicateRemovedScheduler 又实现了Scheduler,DuplicateRemovedScheduler 为抽象类,仅仅实现了push逻辑(生产者),而poll是QueueScheduler自己实现的(消费者)。push()方法使用去重器,判断该请求有没有被采集过。这里要注意,默认Post请求是不去重的,能直接打开的请求都是get的,官方文档也有特别说明
//DuplicateRemovedScheduler 源码
private DuplicateRemover duplicatedRemover = new HashSetDuplicateRemover();
@Override
public void push(Request request, Task task) {
logger.trace("get a candidate url {}", request.getUrl());
//duplicatedRemover.isDuplicate(request, task) 检查是否采集过
if (shouldReserved(request) || noNeedToRemoveDuplicate(request) || !duplicatedRemover.isDuplicate(request, task)) {
logger.debug("push to queue {}", request.getUrl());
pushWhenNoDuplicate(request, task);
}
}
//是否需要去重,POST请求则不需要去重
protected boolean noNeedToRemoveDuplicate(Request request) {
return HttpConstant.Method.POST.equalsIgnoreCase(request.getMethod());
}
public class HashSetDuplicateRemover implements DuplicateRemover {
private Set<String> urls = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
@Override
public boolean isDuplicate(Request request, Task task) {
//add成功,说明没有添加过这条请求,返回true
return !urls.add(getUrl(request));
}
protected String getUrl(Request request) {
return request.getUrl();
}
@Override
public void resetDuplicateCheck(Task task) {
urls.clear();
}
@Override
public int getTotalRequestsCount(Task task) {
return urls.size();
}
}
webmagic源码浅析的更多相关文章
- 【深入浅出jQuery】源码浅析--整体架构
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- 【深入浅出jQuery】源码浅析2--奇技淫巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Struts2源码浅析-ConfigurationProvider
ConfigurationProvider接口 主要完成struts配置文件 加载 注册过程 ConfigurationProvider接口定义 public interface Configurat ...
- (转)【深入浅出jQuery】源码浅析2--奇技淫巧
[深入浅出jQuery]源码浅析2--奇技淫巧 http://www.cnblogs.com/coco1s/p/5303041.html
- HashSet其实就那么一回事儿之源码浅析
上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap, 本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...
- Android 手势识别类 ( 三 ) GestureDetector 源码浅析
前言:上 篇介绍了提供手势绘制的视图平台GestureOverlayView,但是在视图平台上绘制出的手势,是需要存储以及在必要的利用时加载取出手势.所 以,用户绘制出的一个完整的手势是需要一定的代码 ...
- Android开发之Theme、Style探索及源码浅析
1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...
- 【深入浅出jQuery】源码浅析2--使用技巧
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,让你感叹代码之美. 其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐 ...
- Android手势源码浅析-----手势绘制(GestureOverlayView)
Android手势源码浅析-----手势绘制(GestureOverlayView)
随机推荐
- 容器云平台No.5~企业级私有镜像仓库Harbor V2.02
镜像仓库 仓库,顾名思义,就是存放东西的地方,Docker仓库,理所当然,就是存放docker镜像的地方了. Docker仓库分公有仓库和私有仓库.共有仓库有hub.docker.com.gcr.io ...
- python中yield的用法详解——最简单,最清晰的解释(转载)
原文链接 首先我要吐槽一下,看程序的过程中遇见了yield这个关键字,然后百度的时候,发现没有一个能简单的让我懂的,讲起来真TM的都是头头是道,什么参数,什么传递的,还口口声声说自己的教程是最简单的, ...
- Docker镜像发布到阿里云
登录阿里云Docker Registry $ sudo docker login --username=xxx@xxx.com registry.cn-hangzhou.aliyuncs.com 从R ...
- 提效工具-python解析xmind文件及xmind用例统计
现状 每个公司都有一个维护测试case的系统,有自研的也有买的,比如QC, 禅道等等,QA往往习惯使用xmind等思维导图工具来编写测试用例,因为思路清晰,编写方便,那么这就有一个问题,大多公司要求所 ...
- 我要吹爆这份阿里中间件技术内部的RM笔记,简直佩服到五体投地
消息队列 RocketMQ 版是阿里云基于 Apache RocketMQ 构建的低延迟.高并发.高可用.高可靠的分布式消息中间件.该产品最初由阿里巴巴自研并捐赠给 Apache 基金会,服务于阿里集 ...
- Vue.js 学习笔记之二:数据驱动开发
在 Vue.js 框架中,与 HTML 页面元素的交互方式没有像原生 JavaScript 接口那么直接,它是通过先在 HTML 元素标签中嵌入一系列类似于普通标签属性的 Vue 指令属性来绑定数据, ...
- Mysql中 int(3) 类型的含义
注意:这里的(3)代表的并不是存储在数据库中的具体的长度,以前总是会误以为int(3)只能存储3个长度的数字,int(11)就会存储11个长度的数字,这是大错特错的. 其实当我们在选择使用int的类型 ...
- 获取豆瓣读书所有热门标签并保存到mongodb数据库
目标url:https://book.douban.com/tag/?view=type&icn=index-sorttags-all 目的:抓取所有标签名称(tag_name),标签链接(t ...
- 开源后台系统*mee-admin*
mee-admin开源后台系统 Preface 这是一个开放的时代,我们不能总是把东西揣在口袋里面自己乐呵. 也正如名言所说的"如果你有两块面包,你当用其中一块去换一朵水仙花" 所 ...
- C1853 编译器错误:fatal error C1853: 'pjtname.pch' precompiled header file is from a previous
转载:https://www.cnblogs.com/emanlee/archive/2010/10/16/1852998.html 用VC++ 2008 编写C语言程序,编译出现错误: 预编译头文件 ...