垂直爬虫挖坑系列文章之droid

原创
2016/11/02 19:28
阅读数 118

       垂直爬虫,可以简单理解为针对某个领域,例如针对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的源码读通,并写几篇源码解读的文章。任重而道远啊

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部