老狗啃爬虫-去重自定义之Scheduler
摘要:
经过对WebMagic的源码进行了走读,知道了Spider默认设置了QueueScheduler,用以处理链接重复的问题。本次学习我们再次对WebMagic的源码进行了走读,并实现了一个判定重复的简单逻辑。至此,WebMagic框架的Scheduler组件是如何实现对URL的判定重复,实际应用中,会有更加具体、更加复杂的业务需要,我们在此思路下进行实现即可
接上回,我们读了读爬虫框架WebMagic的源码,我们已经知道了是Spider帮我们默认设置了QueueScheduler,用以协助处理链接重复的问题。
然而实际的使用中,有时我们抓取的目标网站连接规则上不是那么规范,亦或根据不同的参数组合,不同的URL很有可能指向的是相同内容的页面,这时候在逻辑上可预先判断的情况下,去重就不是简单的URL字符串对比了,我们就需要一些额外的规则考量,来设计实现满足我们具体需求的Scheduler。
源码走读
我们来仔细看看源代码,WebMagic框架的Scheduler组件,在URL去重复的过程中,是如何工作的。
我们从中枢类Spider进入QueueScheduler类,可以看到,很明显此类继承的父类DuplicateRemovedScheduler是用来处理重复问题的,那我们进去看看:

从代码可以发现,在push方法里,实现了URL是否重的判定业务,其中,shouldReserved()、noNeedToRemoveDuplicate ()分别表示保留和不删除的特定设置验证,这个我们暂且不究,不难看出,具体判定重复的业务逻辑,在HashSetDuplicateRemover类的isDuplicate()方法中,于是我们跟过去看看:

可以看到,isDuplicate()方法中利用一个巧妙的逻辑,即Set在添加相同元素时会返回false,完成了URL是否重复的对比判断。
好了,源代码已走读完毕,实现功能的逻辑也非常清晰了,我们就在此代码的基础上,来实现去重复的自定义。
功能实现
经过代码走读,东西已经很清晰了,我们直接比葫芦画瓢,照着HashSetDuplicateRemover的模样,创建我们的DuplicateRemover:
package cn.veiking.scheduler.component;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.scheduler.component.DuplicateRemover;
/**
* @author :Veiking
* @version :2020年12月12日
* 说明 :处理URL重复问题
*/
public class VDuplicateRemover implements DuplicateRemover {
private Set urls = Collections.newSetFromMap(new ConcurrentHashMap());
@Override
public boolean isDuplicate(Request request, Task task) {
String url = getUrl(request);
// 设置规则,我们假设这个编码的人物信息页面已经获取,故在抓取的时候,当作重复
// 1000058-刘昶
String celebrityCode = "1000058";
if(url.contains(celebrityCode)) {
return true;
}
// 如原Set没有则非重复
if(urls.add(url)) {
return false;
}
return true;
}
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();
}
}
这里我们设置一个变量参照,即假定编码为1000058,名为刘昶的人物信息页面,我们已经抓取过了,根据本次抓取网站URL的组成特点,URL字符串中包含此编码,即认为重复。
由于我们是将编码1000056-屈原的页面作为种子URL,顺着前后翻页的链接顺藤摸瓜进行数据抓取的,这是一个前继后续的线性爬取链,如若此处去重逻辑设置能满足预期,爬取过程必定会在编码为1000058的URL处向后隔断,只能爬取此编码页面之前的数据,我们接下来赶紧完成代码,验证下。
写好了VduplicateRemover类之后,我们继续,参照QueueScheduler,实现我们要用的Scheduler:
package cn.veiking.scheduler;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import cn.veiking.base.common.logs.SimLogger;
import cn.veiking.scheduler.component.VDuplicateRemover;
import us.codecraft.webmagic.Request;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.scheduler.MonitorableScheduler;
import us.codecraft.webmagic.scheduler.component.DuplicateRemover;
import us.codecraft.webmagic.utils.HttpConstant;
/**
* @author :Veiking
* @version :2020年12月12日
* 说明 :处理待抓取URL,管理URL队列
*/
@Service
public class VQueueScheduler implements MonitorableScheduler{
SimLogger logger = new SimLogger(this.getClass());
private DuplicateRemover duplicatedRemover = new VDuplicateRemover(); // 链接重复规则自定义设置
private BlockingQueue queue = new LinkedBlockingQueue();
@Override
public void push(Request request, Task task) {
String url = request.getUrl();
logger.info("VQueueScheduler get [url:{}] ", url);
boolean isDuplicate = duplicatedRemover.isDuplicate(request, task); // 重复 //
boolean shouldReserved = this.shouldReserved(request); // 应保留的
boolean noNeedToRemoveDuplicate = this.noNeedToRemoveDuplicate(request); // 不需要删除的
if (shouldReserved || noNeedToRemoveDuplicate || !isDuplicate) {
logger.info("VQueueScheduler push to queue [url:{}]", url);
queue.add(request);
}
}
@Override
public Request poll(Task task) {
return queue.poll();
}
@Override
public int getLeftRequestsCount(Task task) {
return queue.size();
}
@Override
public int getTotalRequestsCount(Task task) {
return getDuplicateRemover().getTotalRequestsCount(task);
}
public DuplicateRemover getDuplicateRemover() {
return duplicatedRemover;
}
public VQueueScheduler setDuplicateRemover(DuplicateRemover duplicatedRemover) {
this.duplicatedRemover = duplicatedRemover;
return this;
}
protected boolean shouldReserved(Request request) {
return request.getExtra(Request.CYCLE_TRIED_TIMES) != null;
}
protected boolean noNeedToRemoveDuplicate(Request request) {
return HttpConstant.Method.POST.equalsIgnoreCase(request.getMethod());
}
}
此处我们为了强调我们这次编程学习的重点,减少了源码中的一层继承关系,即QueueScheduler、DuplicateRemovedScheduler的功能都在我们的VQueueScheduler中实现了。
我们可以看到,在我们写的VqueueScheduler中,VduplicateRemover也已替换了原来的HashSetDuplicateRemover,接下来我们就测试一下,看看预想的效果能否如愿实现。
程序测试
完成了上面的代码,我们就可以进行测试了。
测试入口在原来的基础上,稍作变动:
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.scheduler.VQueueScheduler;
import cn.veiking.spider.AigufengCelebrityImgPipeline;
import cn.veiking.spider.AigufengCelebrityPipeline;
import cn.veiking.spider.AigufengCelebrityProcessor;
import us.codecraft.webmagic.Spider;
/**
* @author :Veiking
* @version :2020年12月8日
* 说明 :aigufengCelebrityPipeline、aigufengCelebrityImgPipeline、aigufengCelebrityProcessor 测试
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = StartTest.class)
public class AigufengCelebrityTest {
SimLogger logger = new SimLogger(this.getClass());
@Autowired
private AigufengCelebrityPipeline aigufengCelebrityPipeline;
@Autowired
private AigufengCelebrityImgPipeline aigufengCelebrityImgPipeline;
@Autowired
private VQueueScheduler vQueueScheduler;
@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)
.addPipeline(aigufengCelebrityImgPipeline) // 添加图片下载管道
.setScheduler(vQueueScheduler) // 设置自定义Scheduler
.thread(3)
.run();
endTime = System.currentTimeMillis();
logger.info("AigufengCelebrityTest testSpider [end={}] ", "爬取结束,耗时约" + ((endTime - startTime) / 1000) + "秒");
}
}
变化就是增添这一行代码:.setScheduler(vQueueScheduler),即给Spider设置成我们要用的VqueueScheduler。
一切OK,运行程序,进行测试验证…

通过窗口日志我们可以看到,没有找到包含编码1000058 URL 的push to 操作,也没有看到编码序列比1000058大的get或push操作,即预期设想已实现。
结语
本次学习我们再次对WebMagic的源码进行了走读,并实现了一个判定重复的简单逻辑。至此,WebMagic框架的Scheduler组件是如何实现对URL的重复剔除,已然十分清楚了,当然实际应用中,会有更加具体、更加复杂的业务需要,在此思路下进行补充实现即可。
在我们剖析URL去重的过程中,对Scheduler的工作机制已经有了比较完整的理解,去重是没问题了,但爬虫应用在实际的使用场景中,很容易发生程序中断这种情况,如何将抓取工作过程的数据持久化也是个很迫切的需求。
抓取工作过程的数据,即待抓取URL队列、已抓取URL队列。接下来,我们将试着实现如何保存和使用这些数据,实现增量爬取的功能。