最近搞毕业设计,使用到了webmagic,但是才开始学习,对各个组件都还不是很熟悉。相信初学者都会遇到一个问题,那就是:必须要让所有URL都处理完,才能结束整个爬虫过程吗?
当然,动动脑筋就知道当然不用,但是作为新手还是不知道怎么去控制这个爬虫,我一开始也是只会傻傻的设置一个最开始的url,然后写processs方法。但是经过不断的百度,渐渐加深了对webmagic的理解,也开始看起源码来了。
一开始,我用的是非常简单的方法,如下:
int pageCnt = 0;
public static int limit = 10000; //最多爬取1e4个界面,虽然pageCnt不准确
@Override
public void process(Page page) {
if (pageCnt > limit){
page.setSkip(true);
return;
}
pageCnt++;
}
这样看似可以,但是实际上有缺陷,就是pageCnt约束性不强,可能是多线程的原因,即使pageCnt大于limit了,还是会执行,我也不知道为啥,但是, 一般最终处理的页面数是limit的10倍,所以,这个limit还是能限制爬虫提前结束的,就是粗糙了一点,不到万不得已,还是别用这个方法。
(后来看到了可以使用AtomicInteger解决多线程下i++问题,但是我试了一下,还是不行,另外这个pageCnt每次访问一个网页会+10,挺有规律的)
下面是换一种思路的解决方法:
-----------------------------------------------------------------------------------------
这两篇文章算是非常有启发的
https://www.zhihu.com/question/46394779
https://bbs.csdn.net/topics/391952114
经过源码的查看,大致理清楚了spider的执行流程:入口是Spider.run(); 之后spider首先下载(并不是真正的下载),生成一个page对象,里面包含了网页的信息, 这一部分使用的是downloader的组件,然后就是开始process,使用的是 processer组件,而我们编写的process方法就是在这里被调用的,而我们写的process方法里面,最终要的是addTargetRequest这个方法,使用这个可以再将新的爬取网页加入队列,由于有新的request加入队列,process方法会在下一次循环执行,process完之后就是pipeline,进行一些下载或者存储,例如在pipeline里面我们可以将数据存到数据库,之后开始循环,直到队列为空。我们的修改就可以request这里开始。
addTargetRequest是将一个request加入了队列里面,这样下次就能取到这个request,那么我们只需要对request进行判断,只有在一定条件才能push进去就行了,request对象是有一个extra的,它可以设置数据,然后还可以取出来。
这里我们找到源码中的最关键函数,它是将request加入队列的地方(在QueueScheduler.class FileCacheQueueScheduler.class 这样的类里面):
protected void pushWhenNoDuplicate(Request request, Task task) {
if (!this.inited.get()) {
this.init(task);
}
this.queue.add(request);
this.fileUrlWriter.println(request.getUrl());
}
最后贴上我修改的代码
int startDepth = 1; //这是自己新增的变量,表示层数
int levelLimit = 4; //从最开始的网页最大的深度
protected void pushWhenNoDuplicate(Request request, Task task) {
if (!this.inited.get()) { //如果还是一个新的对象
this.init(task);
Map extras = new HashMap();
extras.put("depth",this.startDepth);
request.setExtras(extras);
}
else{ //深度的解释: 1表示是最开始的深度,每往下+1
//这里还有可能为空
if ( request.getExtra("depth") == null ){
Map extras = new HashMap();
extras.put("depth",this.startDepth);
request.setExtras( extras );
}
int currentDepth = (int) request.getExtra("depth"); //获取当前的深度
if ( currentDepth > this.levelLimit ){ //如果超出了深度限制,则不能再继续了
return;
}
}
this.queue.add(request);
this.fileUrlWriter.println(request.getUrl());
}
当然,在process方法里面也要有修改,就是一开始获取request的深度,加1就是子节点的深度
int newDepth = (int)page.getRequest().getExtra("depth")+1; //通过该界面的深度,推算出子界面的深度
Map newMap = new HashMap();
newMap.put("depth",newDepth);
for ( Selectable selectable:aList ){ //对每个链接遍历
String newURL = selectable.links().toString();
if ( judgeURL(newURL) == true ) { //判断URL是否合法
Request newRequest = new Request();
newRequest.setExtras(newMap);
newRequest.setUrl(newURL);
page.addTargetRequest(newRequest); //这里加了一个newRequest,之前写的是加URL
}
}
以上就是控制深度的方法,写的很简略。