爬虫在工作过程中,会有大量的URL需要存储和分配,如何高效的管理这些URL,是一个爬虫系统的重中之重。
crawler4j默认运行最多每小时解析几千个URL,在修改过后可以达到每小时几十万个(后面的文章中介绍),这么多的URL,应该如何管理呢?
crawler4j使用嵌入式数据库Berkeley DB JE 进行URL的临时存储和分配管理,关于Berkeley DB JE ,我在另一篇文章里做了简单介绍:
海量简单数据不想用SQL?试试高效的嵌入式数据库Berkeley DB JE吧!
WebURL:
还是先从BasicCrawlController的main函数开始,看程序是如何添加入口URL的:
controller.addSeed("http://www.ics.uci.edu/");
controller.addSeed("http://www.ics.uci.edu/~lopes/");
controller.addSeed("http://www.ics.uci.edu/~welling/");
再看CrawlController的addSeed()方法:
public void addSeed(String pageUrl) {
addSeed(pageUrl, -1);
}
public void addSeed(String pageUrl, int docId) {
String canonicalUrl = URLCanonicalizer.getCanonicalURL(pageUrl);
if (canonicalUrl == null) {
logger.error("Invalid seed URL: " + pageUrl);
return;
}
if (docId < 0) {
docId = docIdServer.getDocId(canonicalUrl);
if (docId > 0) {
// This URL is already seen.
return;
}
docId = docIdServer.getNewDocID(canonicalUrl);
} else {
try {
docIdServer.addUrlAndDocId(canonicalUrl, docId);
} catch (Exception e) {
logger.error("Could not add seed: " + e.getMessage());
}
}
WebURL webUrl = new WebURL();
webUrl.setURL(canonicalUrl);
webUrl.setDocid(docId);
webUrl.setDepth((short) 0);
if (!robotstxtServer.allows(webUrl)) {
logger.info("Robots.txt does not allow this seed: " + pageUrl);
} else {
frontier.schedule(webUrl);
}
}
这里定义了一个WebURL作为URL的Model类,存储了一些URL的属性:域、子域、路径、锚、URL地址,这些在调用setURL方法时就会被解析出来,setURL主要是字符串的截取,还用到了TLDList.getInstance().contains(domain),就是从域名列表文件tld-names.txt里查找判断URL里哪部分是域名,因为域名包括的部分可能不太一样,如.cn、.com.cn、.gov、.gov.cn;还有一些爬虫属性:分配的ID、父URLID、父URL、深度、优先级,这些会在爬虫工作时指定,所谓父URL就是在哪个页面发现的该地址,深度是第几级被发现的,如入口URL是0,从入口URL页面发现的地址是1,从1发现的新的是2,依此类推,优先级高的(数字小的)会优先分配爬取。
DocIDServer:
addSeed里面setDocid是给URL分配一个惟一的ID,默认是从1开始自动增长:1 2 3 4 5... 虽然这里可以使用JAVA自带的集合类来管理和存储这些ID,但是为了确保惟一且保证在ID增长到了几十上百万时依然高效,crawler4j使用了前面说的BDB JE来存储,当然还有一个原因是为了可恢复,即系统挂了恢复后爬虫可以继续,但我并不打算讨论这种情况,因为在这种情况下,crawler4j的运行效率相当低!
用docIdServer.getDocId()来检查该URL是否已经存储,如果没有则docId = docIdServer.getNewDocID(canonicalUrl);获取新ID。看下docIdServer是怎么工作的,首先在CrawlController构造函数中初始化并传入Environment(关于Env,请参考文章开头BDB JE链接):
docIdServer = new DocIDServer(env, config);
DocIdServer类只负责管理URL的ID,构造函数:
public DocIDServer(Environment env, CrawlConfig config) throws DatabaseException {
super(config);
DatabaseConfig dbConfig = new DatabaseConfig();
dbConfig.setAllowCreate(true);
dbConfig.setTransactional(config.isResumableCrawling());
dbConfig.setDeferredWrite(!config.isResumableCrawling());
docIDsDB = env.openDatabase(null, "DocIDs", dbConfig);
if (config.isResumableCrawling()) {
int docCount = getDocCount();
if (docCount > 0) {
logger.info("Loaded " + docCount + " URLs that had been detected in previous crawl.");
lastDocID = docCount;
}
} else {
lastDocID = 0;
}
}
这里只是简单的创建了一个名叫DocIDs的DB(有关可恢复不做讨论,这里和下面涉及resumable都是false)。这个DB是以URL为key,以ID为value存储的,因为key的惟一性,可保证URL不重复,且更好的用URL来进行ID查询。
再看getDocId():
public int getDocId(String url) {
synchronized (mutex) {
if (docIDsDB == null) {
return -1;
}
OperationStatus result;
DatabaseEntry value = new DatabaseEntry();
try {
DatabaseEntry key = new D