文档章节

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

j
 javayfs
发布于 2016/11/02 19:28
字数 1794
阅读 118
收藏 0

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

© 著作权归作者所有

上一篇: beetlsql应用
下一篇: java8函数式编程
j
粉丝 4
博文 12
码字总数 11740
作品 0
广州
程序员
私信 提问
加载中

评论(0)

使用Xamarin开发手机聊天程序 -- 基础篇(大量图文讲解 step by step,附源码下载)

如果是.NET开发人员,想学习手机应用开发(Android和iOS),Xamarin 无疑是最好的选择,编写一次,即可发布到Android和iOS平台,真是利器中的利器啊!而且,Xamarin已经被微软收购并被大力推...

CSharpKit
2017/12/15
0
0
前端深入之css篇丨2020年前,彻底掌握css动画【transform】

写在前面 马上就2020年了,不知道小伙伴们今年学习了css3动画了吗? 说起来css动画是一个很尬的事,一方面因为公司用css动画比较少,另一方面大部分开发者习惯了用JavaScript来做动画,所以就...

不是酸柠檬
2019/10/30
0
0
Dart语法进阶篇(一)-- Dart源码的排序算法详解

版权声明:本文为博主原创文章,未经博主允许不得转载。https://www.jianshu.com/p/44ae73a58ebc 转载请标明出处: https://www.jianshu.com/p/44ae73a58ebc 本文出自 AWeiLoveAndroid的博客...

AWeiLoveAndroid
2019/07/04
0
0
能量视角下的GAN模型:GAN=“挖坑”+“跳坑”

作者丨苏剑林 单位丨广州火焰信息科技有限公司 研究方向丨NLP,神经网络 个人主页丨kexue.fm 在这个系列中,我们尝试从能量的视角理解 GAN。我们会发现这个视角如此美妙和直观,甚至让人拍案...

Paper_weekly
2019/02/12
0
0
thinkphp集成系列之phpmailer批量发送邮件

前段时间写了一篇博客:thinkphp集成系列之短信验证码、订单通知 说了这是一个短信通知泛滥的年代;大部分网站的邮箱注册都已经被短信注册所取代; 但是邮件和短信相比在一些场景依然有着重要...

白俊遥
2016/05/29
54
0

没有更多内容

加载失败,请刷新页面

加载更多

LiteOS云端对接教程10-LiteOS使用NB模组基于LWM2M对接华为OC平台实战

1. 在云端添加测试设备 打开之前教程新建的LWM2M产品,进入设备管理界面新建真实设备,设备标识符要填写NB模组的IMEI号,可以使用如下命令查看: AT+CGSN=1 测试结果如下: +CGSN:86772503...

小熊派开源社区
23分钟前
48
0
多团队基于git代码管理协作流程

多团队git协同开发流程 一、版本管理的挑战 虽然有这么优秀的版本管理工具,但是我们面对版本管理的时候,依然有非常大得挑战,我们都知道大家工作在同一个仓库上,那么彼此的代码协作必然带...

kingbox2016
24分钟前
60
0
Elmedia Video Player Pro for Mac(苹果万能视频播放器) v7.9中文版

mac电脑用哪款视频播放器最合适呢?elmedia video player pro Mac版是适用于Mac OS的视频播放器。它可以播放几乎任何文件类型,无论是AVI,MP4,FLV,WMV,MKV,MP3,M4V等.Elmedia Video Pl...

云不若
29分钟前
71
0
11个默克尔树开源项目

Merkle树是一种可以有效验证部分数据存在于指定数据集并且未被篡改的高效的哈希树结构,作为一种底层技术广泛应用在各种区块链的实现当中,对于商品溯源、知识产权确认、区块链公证等区块链应...

区块链教程
56分钟前
64
0
Linux系统运维工程师入门绝招放送

运维是干嘛的?安装服务器系统?重装系统再装系统?背锅的? 我就稀里糊涂的,这样报着必死的决心,考下RHCE认证,走上了Linux运维的道路,成为了一名linux运维工程师。有些心得跟大家分享下...

linuxprobe2020
今天
64
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部