crawler4j是google的一款纯java的轻量级爬取框架,主要有如下几方面的优点:
1.轻量级,效率上有保证,基本上没有采用多么复杂的算法,也没有定制DNS和HTTP管理,这样虽然会对性能上有影响,但使用和扩展上都容易了很多;另一方面,
也没有采用较复杂的数据结构,作为Frontiner,Fetcher和Parser几个爬取模块之间衔接的URL队列以及一些辅助功能的实现主要使用了BDB-JE和一些简单的Java内置数据类型,如HashMap,TreeSet等,而其对htttp连接的管理也是直接采用了HTTPComponent包自带的PoolingClientConnectionManager来实现。所以说,这款爬取框架的设计理念之一就是让用户尽可能快,尽可能多的把注意力几种在定制自己的页面解析类实现上,从而提升框架集成的效率。
2.支持高并发,支持多线程,所有线程共用唯一的Frontiner,fetcher,robotsserver。对数据的访问如URL,DocID等的访问都通过BDB来实现
3.可恢复的爬取,即:当前爬取如果意外终止,那么下次重启时,可从上次中断的URL继续进行。
4.良好的集成接口,用户如果需要基于此框架实现自己的爬取,则只需要设计一个自己的爬取类并继承自现有的WebCrawler类,重写其中的visit和shouldVisit方法即可。
--目前只想到这么多,如果还有,后面补上。
接下来几篇文章,本人将根据自己的学习心得从如下几个方面来讲讲这个框架的主要组成部分及其设计思想。
接下来将分别从如下几个部分来介绍:
CrawlController和WebCrawler,Frontier, Fetcher, Parser,支撑类(URL规范化,存储支持,Robots协议支持)
本节作为本系列博客的第一篇,首先讲讲控制相关的几个类,这部分的主要作用就是控制爬取过程,然后提供一个接口实现用户自己定义的访问控制和信息抽取逻辑,主要有如下几个类:
CrawlConfig,Configurable,CrawlController,WebCrawler
CrawlConfig----------该类用来存放所有的和爬取相关的配置,主要有两类,爬虫属性和爬取控制,前者如:crawlStorageFolder和userAgentString,后者如:maxDepthOfCrawling,maxPagesToFetch等等。这个类中存放的配置被frontiner,fetcher和parser共用。
Configurable----------该类为一个抽象类,是CrawlController,frontiner,fetcher和parser的父类,起包含了一个被传进来的CrawlConfig供后面爬取使用。
CrawlController-------该类为爬取控制类,主要有三个作用:
(1)初始化爬取环境,根据传入的配置创建相关对象。
File folder = new File(config.getCrawlStorageFolder());
if (!folder.exists()) {
if (!folder.mkdirs()) {
throw new Exception("Couldn't create this folder: " + folder.getAbsolutePath());
}
}
boolean resumable = config.isResumableCrawling();
EnvironmentConfig envConfig = new EnvironmentConfig();
envConfig.setAllowCreate(true);
envConfig.setTransactional(resumable);
envConfig.setLocking(resumable);
File envHome = new File(config.getCrawlStorageFolder() + "/frontier");
if (!envHome.exists()) {
if (!envHome.mkdir()) {
throw new Exception("Couldn't create this folder: " + envHome.getAbsolutePath());
}
}
if (!resumable) {
IO.deleteFolderContents(envHome);
}
Environment env = new Environment(envHome, envConfig);
docIdServer = new DocIDServer(env, config);
frontier = new Frontier(env, config, docIdServer);
这部分又包含两部分工作:一:初始化BDB运行所需环境(红色代码);二:创建DocIDServer和Frontier(蓝色代码)
(2)根据传入爬取线程个数及传入的爬取类,创建,初始化并启动爬取线程,并启动监控线程(实时对所有爬取线程进行监控,在合适的时机结束爬取过程)
(3)保存各个爬取线程的本地数据(例如用户可以设计自己的爬取类,将爬取过程中收集到的任何信息存入到CrawlController,然后在爬取完成后从该类提取。
WebCrawler----该类实现了一个完整的爬取流程,继承自Runnable,因此可以作为线程独立运行,除此之外,提供了一些接口供CrawlController在某些状况发生时调用,用户可以重写这些接口实现自己的定义,另外还有两个很重要的接口:shouldVisit和visit,第一个用于对当前发现的新的链接进行过滤,第二个实现了用户自己的从网页中抽取所需信息的逻辑。
下面分别来分析一下CrawlController和WebCrawler的运行过程
首先来看CrawlController的运行:
CrawlController的运行通过protected
从函数定义可以看出,第一个参数接受所有WebCrawler类型的类,第二个为启动的crawler的个数(也就是WebCrawler线程的个数),第三个确定是否为阻塞式运行。
所谓阻塞式运行,即所有爬取线程以及监控线程都已经启动后,主线程是否等待他们执行完毕,如果等待则为阻塞式,否则为非阻塞。
第一步:创建,初始化并启动爬取线程,
final List threads = new ArrayList();
final List crawlers = new ArrayList();
for (int i = 1; i <= numberOfCrawlers; i++) {
T crawler = _c.newInstance();
Thread thread = new Thread(crawler, "Crawler " + i);
crawler.setThread(thread);
crawler.init(i, this);
thread.start();
crawlers.add(crawler);
threads.add(thread);
logger.info("Crawler " + i + " started.");
}
public void init(int id, CrawlController crawlController) {
this.myId = id;
this.pageFetcher = crawlController.getPageFetcher();
this.robotstxtServer = crawlController.getRobotstxtServer();
this.docIdServer = crawlController.getDocIdServer();
this.frontier = crawlController.getFrontier();
this.parser = new Parser(crawlController.getConfig());
this.myController = crawlController;
this.isWaitingForNewURLs = false;
}
从上述代码可以看出,初始化的过程中,除了parser是每个crawler创建一个新的之外,其它都是从CrawlController继承来的,可以看出这部分工作逻辑很清晰,也很简单,没有什么好讲的,下面我们重点分析一下监控进程的运行
第二步:创建并运行crawler的监控进程:
final CrawlController controller = this;
Thread monitorThread = new Thread(new Runnable() {
@Override
public void run() {
try {
synchronized (waitingLock) {
//首先获取阻塞锁,如果是阻塞模式,则主线程会在启动该监控线程后,获取该锁来判断当前爬取是否结束,所以必须加锁
while (true) {
sleep(10);
boolean someoneIsWorking = false;
for (int i = 0; i < threads.size(); i++) {
Thread thread = threads.get(i);
if (!thread.isAlive()) {
if (!shuttingDown) {
logger.info("Thread " + i + " was dead, I'll recreate it.");
T crawler = _c.newInstance();
thread = new Thread(crawler, "Crawler " + (i + 1));
threads.remove(i);
threads.add(i, thread);
crawler.setThread(thread);
crawler.init(i + 1, controller);
thread.start();
crawlers.remove(i);
crawlers.add(i, crawler);
}
//循环判断每个线程,如果已经死亡,则重启该线程
} else if (crawlers.get(i).isNotWaitingForNewURLs()) {
someoneIsWorking = true;
//如果有线程活着,且没有处于等待URL状态,则表明当前还有正在运行的线程,置标志位
}
}
if (!someoneIsWorking) {
//在当前的遍历中,没有线程处于正常运行状态(要么死亡,要么处于等待URL状态)
// Make sure again that none of the threads
// are
// alive.
logger.info("It looks like no thread is working, waiting for 10 seconds to make sure...");
sleep(10);
//再确认一遍,因为如果上次的遍历中如果有死亡的线程,则会重新创建并运行,因此需要再次确认,是否所有线程都处于活动状态,并且在等待新的URL
someoneIsWorking = false;
for (int i = 0; i < threads.size(); i++) {
Thread thread = threads.get(i);
if (thread.isAlive() && crawlers.get(i).isNotWaitingForNewURLs()) {
someoneIsWorking = true;
}
}
if (!someoneIsWorking) {
if (!shuttingDown) {//所有线程处于等待新的URL状态
long queueLength = frontier.getQueueLength();
if (queueLength > 0) {
continue;
}
logger.info("No thread is working and no more URLs are in queue waiting for another 10 seconds to make sure...");
sleep(10);
queueLength = frontier.getQueueLength();
if (queueLength > 0) {
continue;
}
//如果URL队列上有URL,则继续下一轮检查
}
//URL队列也已经空,停止frontiner,并保存每个crawler的本地数据
logger.info("All of the crawlers are stopped. Finishing the process...");
// At this step, frontier notifies the
// threads that were
waiting for new URLs and they should
// stop
frontier.finish();
for (T crawler : crawlers) {
crawler.onBeforeExit();
crawlersLocalData.add(crawler.getMyLocalData());
}
logger.info("Waiting for 10 seconds before final clean up...");
sleep(10);
//关闭
frontier.close();
docIdServer.close();
pageFetcher.shutDown();
finished = true;
//释放阻塞锁,恢复主线程
waitingLock.notifyAll();
return;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
monitorThread.start();
if (isBlocking) {
waitUntilFinish();
}
接下来看看WebCrawler:
WebCrawler的运行全部体现在run函数:
public void run() {
onStart();
while (true) {
List assignedURLs = new ArrayList(50);
isWaitingForNewURLs = true;
frontier.getNextURLs(50, assignedURLs);
isWaitingForNewURLs = false;
if (assignedURLs.size() == 0) {
if (frontier.isFinished()) {
return;
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
for (WebURL curURL : assignedURLs) {
if (curURL != null) {
processPage(curURL);
frontier.setProcessed(curURL);
}
if (myController.isShuttingDown()) {
logger.info("Exiting because of controller shutdown.");
return;
}
}
}
}
}
这其中需要注意的时,由于可能存在多个线程同时访问frontiner,因此,存在加锁的问题,所以需要在获取URL前和获取后分别设置isWaitingForNewURLs标志位来告诉监控线程当前crawler线程的状态。上述代码中紧紧在每处理一个URL后去判断controller是否已经停止(shutdown),本人认为至少可以在每次循环开始或者结束的地方再加以次这样的判断,这样时效性或许更强些。
再来看看对每个URL的处理:
URL的处理主要包含如下几个步骤:
第一步:下载当前URL页面,将HTTP响应消息保存在PageFetchResult中,然后对两类特殊情况进行处理:
第一类就是页面的重定向,对于重定向的页面主要就是判断是否符合访问规则,如果符合则将其放入URL队列。
第二类就是对于成功访问的页面对其响应中的URL再次进行判断,检查其是否被访问过,因为有些页面可能存在好几个不同的URL,这样可以提高去重的准确度。
第二步:对当前的页面的http响应解析,主要是对响应消息的不同类型进行解析并保存到Page对象中,主要分二进制,文本和html三种,对于Html则抽取出其中的链接,对于符合过滤规则的URL,放入URL队列。
第三步:调用visit函数实现用户信息的抽取。
本部分的分析就到此,下节来看看frontiner的运行