这节我们来看看Spiderman是如何调度和执行一个爬取任务的。用户可以自行设置爬取任务的调度策略,也可以只执行一次。Spiderman提供了完善的调度策略接口,下面我们先来看看都有哪些和调度相关的字段:
Spiderman:
isShutdownNow:是否立刻停止每个site的爬取线程,true:表示主调度线程(Spiderman所在线程)已经等待完毕设定的scheduleTime,由主线程完成每个site爬取的停止,
false则表明由定时器任务来停止site的爬取
pool:所有site爬取调度线程池,这里的每个task是一个site的爬取主线程
isSchedule:本次爬取是否为调度执行
scheduleTime:每次调度允许所有site爬取的时间,到点后所有site爬取将被停止
scheduleDelay:调度间隔,如果是调度执行,调度时间到达后,间隔这时间再进行下一轮爬取
scheduleTimes:已经执行的调度次数
maxScheduleTimes:用户设定的最大调度次数
Site:
thread:每个site爬取的线程数目
pool:每个site爬取的线程池
waitQueue:每个site内部等待爬取的任务队列(这里的每个任务就是一个经过封装的URL)
下面我们从四个方面来分析spiderman的调度执行过程,总体调度转移流程,总体调度策略,每个site内部的调度策略,site内部具体的一个Task的执行。
总体调度转移流程
当用户通过相关接口设定好调度参数后,由spiderman创建一个外部的线程池pool(线程池的corePoolSize和maxPoolSize是site的个数),然后将每个site封装成一个可运行的线程提交到该线程池,由该线程池负责管理每个site线程的运行,在每个site内部又有一个线程池来存放每个具体的爬取任务,这里的线程池corePoolSize和maxPoolSize为thread变量指定大小。spiderman里的调度在下面函数完成:
private Spiderman _startup(){
for (Site site : sites){
pool.execute(new Spiderman._Executor(site));
listener.onInfo(Thread.currentThread(), null, "spider tasks of site[" + site.getName() + "] start... ");
}
return this;
}
上面的pool创建如下:
private void initPool(){
if (pool == null){
int size = sites.size();
if (size == 0)
throw new RuntimeException("there is no website to fetch...");
pool = new ThreadPoolExecutor(size, size,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
listener.onInfo(Thread.currentThread(), null, "init thread pool size->"+size+" success ");
}
}
从pool的创建代码可以看出corePoolSize和maxPoolSize被设定为site个数,这样设置可以保证每个site的爬取平等执行。
site由于没有继承Thread类也没有实现Runnable接口,因此不能独立执行,其被封装在一个Spiderman的内部类_Executor中,该类实现了Runnable接口。下面来看看具体的调度策略:
总体调度策略
先来看一个直观的调度执行用法:
Spiderman.me()
.listen(listener)//设置监听器
.schedule("10s")//调度,爬虫运行10s
.delay("2s")//每隔 10 + 2 秒后重启爬虫
.times(3)//调度 3 次
.startup()//启动
.blocking();//阻塞直到所有调度完成
上面代码,如果最后不调用blocking,主线程可以继续做其它事情,否则就被block知道所有调度执行完毕.schedule,delay,times都是设定调度参数,具体的调度在startup中完成,下面结合startup代码来分析:
public Spiderman startup() {
if (isSchedule) <span style="color:#3366ff;">{//为调度执行
final Spiderman _this = this;
timer.schedule(new TimerTask() {//<span style="color:#3366ff;">创建TimerTask
public void run() {
//限制schedule的次数
if (_this.maxScheduleTimes > 0 && _this.scheduleTimes >= _this.maxScheduleTimes){
try {
_this.cancel();
} catch (Throwable e){
e.printStackTrace();
_this.listener.onError(Thread.currentThread(), null, e.toString(), e);
}
_this.listener.onInfo(Thread.currentThread(), null, "Spiderman has completed and cancel the schedule.");
try {
_this.listener.onAfterScheduleCancel();
} catch (Throwable e) {
e.printStackTrace();
_this.listener.onError(Thread.currentThread(), null, e.toString(), e);
}
_this.isSchedule = false;
} else {//<span style="color:#3366ff;">未达到最大调度次数</span></strong></span>