10.3.2 定制Queue-assignment-policy两个问题
首先提出两个问题:
l 什么是Queue-assignment-policy
l 为什么要改变Queue-assignment-policy
在10.2节中,向读者介绍过了 Heritrix的架构。其中,讲解了Heritrix使用了Berkeley DB来构建链接队列。这些队列被置放于BdbMultipleWorkQueues中时,总是先给予一个Key,然后将那些Key值相同的链接放在一起, 成为一个队列,也就是一个Queue。
这里就出现了一个问题,这个Key值到底该如何计算呢?事实上,这里也说的Key值,应该是做为一种标识符的形式存在。也就是说,它要与URL之间有一种内在的联系。
在Heritrix中,为每个队列赋上Key值的策略,也就是它的queue-assignment-policy。这就解答了第一个问题。
在默认的情况下,Heritrix使用 HostnameQueueAssignmentPolicy来解决Key值生成的问题。仔细看一下这个策略的名称就知道,这种策略其实是以链接的 Host名称为Key值来解决这个问题的。换句话也就是说,相同Host名称的所有URL都会被置放于同一个队列中间。
这种方式在很大程度上可以解决广域网中信息抓取 时队列的键值问题。但是,它对于某个单独网站的网页抓取,就出现了很大的问题。以Sohu的新闻网页为例,其中大部分的URL都来自于sohu网站的内 部,因此,如果使用了HostnameQueueAssignmentPolicy,则会造成有一个队列的长度非常长的情况。
在Heritrix中,一个线程从一个队列中取URL链接时,总是会先从队列的头部取出第一个链接,在这之后,这个被取出链接的队列会进入阻塞状态,直到待该链接处理完,它才会从阻塞状态中恢复。
假如使用 HostnameQueueAssignmentPolicy策略来应对抓取一个网站中内容的情况,很有可能造成仅有一个线程在工作,而其他所有线程都在 等待。这是因为那个装有绝大多数URL链接的队列几乎会永远处于阻塞状态,因此,别的线程根本获取不到其中的URI,在这种情况下,抓取工作会进入一种类 似于休眠的状态。因此,需要改变queue-assignment-policy来避免发生这种情况,这也就回答了第二个问题。
10.3.3 定制Queue-assignment-policy继承QueueAssignmentPolicy类
那么,被改变的Key值的生成方式,应该具有什么样的要求呢?从上面的叙述中可以知道,这个Key值最重要的一点就是应该能够有效的将所有的URL散列到不同的队列中,最终能使所有的队列的长度的方差较小,在这种情况下,才能保证工作线程的最大效率。
任何扩展queue-assignment- policy的默认实现的类,均继承自QueueAssignmentPolicy并覆写了其getClassKey()方法,getClassKey方 法的参数为一个链接对象,而我们的散列算法,正是要根据这个链接对象来返回一个值。
具体的算法就不说了,有许多种方法可以实现的。
heritrix多线程的方法,我在网上找了很多文章,试着配置,但是基本上都不合适,唯独这个合适了。
1.在org.archive.crawler.frontier下新建一个ELFHashQueueAssignmentPo
licy 类,这个类要注意继承自 QueueAssignmentPolicy。
2.在该类下编写代码如下:
package org.archive.crawler.frontier;import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.httpclient.URIException;
import org.archive.crawler.datamodel.CandidateURI;
import org.archive.crawler.framework.CrawlController;
import org.archive.net.UURI;
import org.archive.net.UURIFactory;
public class ELFHashQueueAssignmentPolicy extends QueueAssignmentPolicy{
private static final Logger logger = Logger
.getLogger(ELFHashQueueAssignmentPolicy.class.getName());
/**
* When neat host-based class-key fails us
*/
private static String DEFAULT_CLASS_KEY = "default...";
private static final String DNS = "dns";
public String getClassKey(CrawlController controller, CandidateURI cauri) {
String scheme = cauri.getUURI().getScheme();
String candidate = null;
try {
if (scheme.equals(DNS)){
if (cauri.getVia() != null) {
// Special handling for DNS: treat as being
// of the same class as the triggering URI.
// When a URI includes a port, this ensures
// the DNS lookup goes atop the host:port
// queue that triggered it, rather than
// some other host queue
UURI viaUuri = UURIFactory.getInstance(cauri.flattenVia());
candidate = viaUuri.getAuthorityMinusUserinfo();
// adopt scheme of triggering URI
scheme = viaUuri.getScheme();
} else {
candidate= cauri.getUURI().getReferencedHost();
}
} else {
String uri = cauri.getUURI().toString();
long hash = ELFHash(uri);
// candidate = cauri.getUURI().getAuthorityMinusUserinfo();
candidate = Long.toString(hash % 100);
}
if(candidate == null || candidate.length() == 0) {
candidate = DEFAULT_CLASS_KEY;
}
} catch (URIException e) {
logger.log(Level.INFO,
"unable to extract class key; using default", e);
candidate = DEFAULT_CLASS_KEY;
}
if (scheme != null && scheme.equals(UURIFactory.HTTPS)) {
// If https and no port specified, add default https port to
// distinguish https from http server without a port.
if (!candidate.matches(".+:[0-9]+")) {
candidate += UURIFactory.HTTPS_PORT;
}
}
// Ensure classKeys are safe as filenames on NTFS
return candidate.replace(':','#');
}
//hash散列
public static long ELFHash(String str){
long hash = 0;
long x = 0;
for (int i = 0; i < str.length(); i++) {
hash = (hash << 4) + str.charAt(i);
if ((x = hash & 0xF000000L) != 0) {
hash ^=(x >> 24);
hash &= ~x;
}
}
return (hash & 0X7FFFFFF);
}
}
3. 修改AbstractFrontier 类的AbstractFrontier方法 :
关键代码段是:
StringqueueStr = System.getProperty(AbstractFrontier.class.getName()+
"."+ ATTR_QUEUE_ASSIGNMENT_POLICY,
ELFHashQueueAssignmentPolicy.class.getName()+ " " +
// HostnameQueueAssignmentPolicy.class.getName() + " "+
IPQueueAssignmentPolicy.class.getName()+ " " +
BucketQueueAssignmentPolicy.class.getName()+ " " +
SurtAuthorityQueueAssignmentPolicy.class.getName());
Patternp = Pattern.compile("\s*,\s*|\s+");
String[] queues = p.split(queueStr);
其中红色部分是新加的代码。
4. 修改heritrix.properties 中的配置
#############################################################################
# F R O N T I ER
#############################################################################
# List here all queue assignment policies you'd have show asa
# queue-assignment-policy choice in AbstractFrontier derivedFrontiers
# (e.g. BdbFrontier).
org.archive.crawler.frontier.AbstractFrontier.queue-assignment-policy=
org.archive.crawler.frontier.ELFHashQueueAssignmentPolicy
// org.archive.crawler.frontier.HostnameQueueAssignmentPolicy
org.archive.crawler.frontier.IPQueueAssignmentPolicy
org.archive.crawler.frontier.BucketQueueAssignmentPolicy
org.archive.crawler.frontier.SurtAuthorityQueueAssignmentPolicy
org.archive.crawler.frontier.TopmostAssignedSurtQueueAssignmentPolicy
org.archive.crawler.frontier.BdbFrontier.level = INFO
红色部分为新加部分。
10.3.5 在Prefetcher中取消robots.txt的限制
Robots.txt是一种专门用于搜索引擎网 络爬虫的文件,当构造一个网站时,如果作者希望该网站的内容被搜索引擎收录,就可以在网站中创建一个纯文本文件robots.txt,在这个文件中,声明 该网站不想被robot访问的部分。这样,该网站的部分或全部内容就可以不被搜索引擎收录了,或者指定搜索引擎只收录指定的内容。
Heritrix在其说明文档中,表明它是一个 完全遵守robots.txt协议的网络爬虫。这一点固然在宣传上起到了一定的作用。但是,在实际的网页采集过程中,这并不是一种最好的作法。因为大部分 的网站并不会放置一个robots.txt文件以供搜索引擎读取,在互联网信息以几何级数增长的今天,网站总是在希望自己的内容不被人所利用的同时,又希 望自己能够被更多的用户从搜索引擎上检索到。
不过幸好,robots.txt协议本身只是一种附加的协议,网站本身并不能了解究竟哪些Socket联接属于爬虫哪些属于正常的浏览器连接。所以,不遵守robos.txt协议成为了更多搜索引擎的首选。
使用过Heritrix的朋友就会发现这样一个 问题,如果当一个网站没有放置robots.txt文件时,Heritrix总是要花上大量的时间试图去访问这样一个文件,甚至可能retry很多次。这 无疑很大的降低了抓取效率。因此,为了提高抓取的效率,可以试着将对robots.txt的访问部分去除。
在Heritrix中,对robots.txt 文件的处理是处于PreconditionEnforcer这个Processor中的。PreconditionEnforcer是一个 Prefetcher,当处理时,总是需要考虑一下当前这个链接是否有什么先决条件要先被满足的,而对robots.txt的访问则正好是其中之一。在 PreconditionEnforcer中,有一个private类型的方法,它的方法签名为:
private boolean considerRobotsPreconditions(CrawlURI curi)
该方法的含义为:在进行对参数所表示的链接的抓取前,看一下是否存在一个由robots.txt所决定的先决条件。很显然,如果对每个链接都有这样的处理。那么,很有可能导致整个抓取任务的失败。因此,需要对它进行调整。
这个方法返回true时的含义为需要考虑 robots.txt文件,返回false时则表示不需要考虑robots.txt文件,可以继续将链接传递给后面的处理器。所以,最简单的修改办法就是 将这个方法整个注释掉,只留下一个false的返回值。经过笔者的试验,这种方法完全可行,抓取的速度提高了至少一半以上!