垂直爬虫,可以简单理解为针对某个领域,例如针对jd.com,tmall.com这一系列电商网站进行爬取,并且只爬取它们的详细页面。例如针对http://www.oschina.net/blog这下面的所有网页爬取,只提取所有的博客文章,并且只对标题和正文进行抽取。开源领域里,目前学习了两个框架,一个是droid,项目中有正式在使用了,一个是webmagic, 计划学习中。
先来说下droid这个框架的大概流程,首先,我们需要一个或多个网站的入口页面。这些页面可以理解为初始爬取链接,例如http://www.jd.com/。droid框架会将初始爬取链接注入到队列queue中,并且调用
org.apache.droids.api.TaskMaster这个类对queue里面的任务进行爬取,每次从队列获取出url之后,就会从
org.apache.droids.api.Droid.getNewWorker()获取worker对象,这个worker对象是真正干活的,worker对象调用org.apache.droids.protocol.http.HttpProtocol,这个类对uri进行httpclient调用操作,并且把数据封装为
org.apache.droids.api.ContentEntity对象,之后根据这个ContentEntity对象,droid会调用org.apache.droids.api.Parser这个对象,这个对象对ContentEntity进行解析,并抽取所有的链接(a标签)
之后再把抽取完的所有链接加入队列,不断循环这样的动作(像是其他的一些爬虫,都是差不多这样的流程:给定一些起始链接,抽取出链接,链接下面再类似递归的不断的抽取链接,最后就可以把整个站点所有的页面爬取下来了。)。
droid框架主要有这几个组件:Handler,对页面数据进行处理的,例如将页面保存到本地。Protocol,
主要用到的是httpProtol,对链接uri进行处理,例如用httpclient根据uri获取网页数据。Parser,对页面数据进行解析,获取一个页面的所有链接,加入到队列中,不断循环。URLFilter,对链接进行过滤和后置处理,例如我们可能需要说满足一定正则的uri才进行爬取,甚至可能需要判断链接是否已经爬取过了。
为什么需要使用droid呢,因为上述说的droid的所有组件皆可以定制,大大方便了一些开发。在droid里面,职责链设计模式基本随处可见。
好了,上面说了这么多,下面直接上一段例子吧(以京东为例子)。这里先介绍一下项目规划部分:
.爬虫需要一个容器,保存所有爬取过的url,这里我们直接使用redis,redis提供了Set类似的数据结构
.爬取后的数据需要保存下来,这里我们可以考虑分布式文件系统或者bdb(BERKELEY DB)
.爬取后的数据需要抽取,甚至需要建索引,我们这里直接考虑solr吧
List<String> seedUrl = Arrays.asList("http://www.jd.com/");
TaskMaster<Link> taskMaster = new SequentialTaskMaster<>();
//京东每个爬取都休眠5s
taskMaster.setDelayTimer(new SimpleDelayTimer(5000));
taskMaster.setExceptionHandler(new DefaultTaskExceptionHandler());
//这个droid为调度类,要设置worker,handler,urlfilter信息
CrawlingDroid droid = new SaveCrawlingDroid(linkQueue, taskMaster);
droid.setInitialLocations(seedUrl);
HttpClient httpClient = new DroidsHttpClient();
droid.setHttpClient(httpClient);
//对uri进行httpclient调用
ProtocolFactory protocolFactory = new ProtocolFactory();
Protocol httpProtocol = new HttpProtocol(httpClient);
//下面对http和https处理,都是用相同的httpprotocol
protocolFactory.getMap().put("http", httpProtocol);
protocolFactory.getMap().put("https", httpProtocol);
droid.setProtocolFactory(protocolFactory);
//用于对页面内容所有链接进行抽取的
ParserFactory parserFactory = new ParserFactory();
parserFactory.getMap().put("text/html", new TikaDocumentParser());
droid.setParserFactory(parserFactory);
//一开始,这个history肯定是空的,因为还没爬取过京东的网页
Set<String> history =...;
URLFiltersFactory filtersFactory = new URLFiltersFactory();
RegexURLFilter regexFilter = new RegexURLFilter();
//从配置文件中加载哪些网页可以被爬取,例如http://item.jd.com/(\\d+).html,这个配置文件可以定制多个规则
regexFilter.setFile("classpath:/jd-regex-urlfilter.txt");
BloomFilter bloomFilter = new SimpleBloomFilter();
history.forEach(dataIdOrUri -> {
bloomFilter.add(dataIdOrUri);
});
//已经爬取过,就不再对链接进行爬取
AlreadyVisitedFilter alreadyVisitedFilter = new AlreadyVisitedFilter(bloomFilter);
//对页面进行etl处理,比如京东的可能需要补全链接
Function<String, String> etlFunction = ...;
filtersFactory.getMap().put("etl", new URLFilter() {
@Override
public String filter(String urlString) {
String uri = urlString;
uri = etlFunction.apply(urlString);
//先进行etl然后再进行正则匹配,之后在进行bloom过滤
uri = regexFilter.filter(uri);
if (uri != null) {
return alreadyVisitedFilter.filter(uri);
}
return null;
}
});
droid.setFiltersFactory(filtersFactory);
Pattern pattern = ...
HandlerFactory handlerFactory = new HandlerFactory();
handlerFactory.getMap().put("default", new Handler() {
@Override
public void handle(URI uri, ContentEntity entity)
throws IOException, DroidsException {
String taskUri = uri.toString();
Matcher matcher = pattern.matcher(taskUri);
boolean flag = matcher.find();
String dataIdOrUrl = taskUri;
if (flag) {
//如果满足一定的正则,那么证明这个页面肯定是详细页面,后续处理.
dataIdOrUrl = matcher.group(1);//处理一下dataid
}
//爬取完成后,加入到历史队列中
history.add(dataIdOrUrl);
}
});
droid.setHandlerFactory(handlerFactory);
在自己的实际开发中,用到droid,还抽象出了几个类,下面也来贴下类的设计(ps,下次应该直接在oschina上开放源码了,写文章太累了。。。)
//代表一个站点,一个站点应该有下面的信息
public class WebSite implements Serializable {
private static final long serialVersionUID = 1885115065486773761L;
/**
* 例如jd
*/
private final String name;
private final Pattern pattern;
/**
* 休眠多久
*/
private final long sleepTime;
/**
* url处理,例如补全
*/
private final Function<String, String> urlFunction;
//regex代表详细页面正则,例如http://item.jd.com/(\\d+).html
public WebSite(String name, String regex, long sleepTime,
Function<String, String> urlFunction) {
Assert.isTrue(sleepTime > 0);
this.name = name;
this.pattern = Pattern.compile(regex);
this.sleepTime = sleepTime;
//默认的处理规则,去掉最后一个/
Function<String, String> defaultFunction = uri -> {
if (uri.endsWith("/")) {
uri = uri.substring(0, uri.length());
}
return uri;
};
this.urlFunction = defaultFunction.andThen(urlFunction);
}
}
//这个代表一个详细页面
public class DetailPageTask implements Serializable {
private static final long serialVersionUID = -1564944784760107402L;
private final WebSite webSite;
//每个详细页面的html页面内容
private final InputStream pageContent;
private final String dataId;
//dataid,每个详细页面应该有自己的dataid,例如http://item.jd.com/10666474046.html
//中的10666474046
public DetailPageTask(WebSite webSite, InputStream pageContent,
String dataId) {
Assert.notNull(webSite);
Assert.notNull(pageContent);
this.webSite = webSite;
this.pageContent = pageContent;
this.dataId = dataId;
}
}
public interface DroidHandler<R extends Serializable> {
public R handle(InputStream pageContent,
Function<byte[], R> function) throws IOException;
}
//对droid中的handler进行扩展,droid的handler是没有返回值的,我们可能需要
//返回值,例如我们对页面操作后,可能需要返回一个boolean值
public class GzipDroidHandler<R extends Serializable>
implements DroidHandler<R> {
//function,代表一个闭包,将gzip之后的页面交给闭包处理,例如存入bdb
@Override
public R handle(InputStream pageContent,
Function<byte[], R> function) throws IOException {
return function.apply(gzip(pageContent));
}
private byte[] gzip(InputStream input) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
GZIPOutputStream output = null;
try {
output = new GZIPOutputStream(dataStream);
IOUtils.copy(input, output);
IOUtils.closeQuietly(output);
return dataStream.toByteArray();
} catch (IOException e) {
throw e;
}
}
}
TODO这篇文章只是挖了一个浅坑,后续任务:闲暇时间自己写个爬虫巩固和学习,把webmagic和droids的源码读通,并写几篇源码解读的文章。任重而道远啊