前言
关于本文的一些背景知识,请移步该系列的前序文章。
设计说明
从这篇文章开始讨论爬虫crawler的具体实现,首先讨论爬虫内部维护的数据结构。
本系列的第一篇文章已经包含了Crawler控制过程的示例代码,通过下面的代码片段,可以先简单回顾一下处理逻辑。其中使用的TargetLinks和FetchedLinks就是这篇文章要讨论的内容。
Link target = null;
// 如果需要爬取的链接集合还有剩余
while (null != (target = targetLinks.next())) {
try {
fetchAndProcess(target);
//如果已采集的链接数量不超过采集范围约束的最大值
if (this.fetchedLinks.total() >= crawlingScope.maxToCrawl()) {
break;
}
TimeUnit.MILLISECONDS.sleep(this.crawlDelay);
} catch (Exception e) {
e.printStackTrace();
}
TargetLinks保存需要采集的目标链接,FetchedLinks保存已经采集过的链接。每当crawler成功爬取完一个链接,就要从TargetLinks中删除这个链接,并保存到FetchedLinks中。这两个集合就是crawler的内部状态。
代码示例
接口设计
interface TargetLinks { //待爬取网址集合
void addAll(Collection<Link> links);
//增加一个链接
void add(Link link);
//带爬取链接总数
long total();
//返回下一个要爬取的链接
Link next();
//清空,相当于重置爬取进度
void clear();
}
interface FetchedLinks { //已爬取网址集合
//某个链接是否已经爬取过
boolean contains(Link link);
//增加一个链接
void add(Link link);
//总链接数
long total();
//清空,相当于重置爬取进度
void clear();
}
具体实现
具体实现的时候,这两个集合的持久化方式有很多选择。
(1)可以保存在内存中,这种方式实现最简单,但程序重启后,采集进度就丢失了,只能从头再来,测试环境可用。
(2)也可以保存在文件中,实现上复杂一些,要自己实现缓存,好处是进度不丢失。
(3)还可以保存到数据库中,常用的包括MySQL,Redis等,由于这两个集合变化速度很快,用Redis更适合,但可能需要单独安装一个数据库。
下面的代码示例是基于Redis实现的TargetLinks,只能用在实验环境,如果在生产环境使用,还需要继续完善。
需要说明的是,这个简单爬虫在整体设计上,让每个crawler实例执行一个采集任务,并且拥有独立的TargetLinks集合对象以及FetchedLinks集合对象。TargetLinks和FetchedLinks集合的名字包含了采集任务ID(taskId),以便和其他采集任务状态隔离。如果要同时执行多个采集任务,要使用多线程方式,让每个crawler实例在独立线程中运行。
细心的读者会发现,代码中使用了Redis集合(Set)作为持久化方案,这样就不能控制网站的遍历顺序了,的确如此,如果对这方面有要求,请自行切换为Redis列表。
public class TargetLinksRedisImpl implements TargetLinks {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String setName;
private String taskId; // 每个任务对应一个集合,任务之间隔离
private Gson gson = new GsonBuilder().create()
public void setTaskId(String taskId) {
this.taskId = taskId;
setName = taskId + "-" + "targets";
}
@Override
public void addAll(Collection<Link> links) {
SetOperations<String, String> setOps = redisTemplate.opsForSet();
for (Link each : links) {
setOps.add(SET_NAME, gson.toJson(each));
}
}
@Override
public void add(Link link) {
SetOperations<String, String> setOps = redisTemplate.opsForSet();
setOps.add(setName, gson.toJson(link));
}
@Override
public Link next() {
SetOperations<String, String> setOps = redisTemplate.opsForSet();
String linkJson = setOps.pop(setName);
Link link = gson.fromJson(linkJson, Link.class);
return link;
}
@Override
public long total() {
SetOperations<String, String> setOperations = redisTemplate.opsForSet();
long size = setOperations.size(SET_NAME);
return size;
}
}
小结
这篇文章讨论了爬虫内部状态的管理,包括待爬取网址集合TargetLinks和已爬取网址集合FetchedLinks,并分别展示了这两个集合的接口设计和具体实现。
通过这两个基础概念的引入,不仅可以让爬取逻辑更加清晰,还可以方便地观察爬虫在爬取网页过程中状态的变化,并随时展示当前的爬取进度。