Spiderman源码分析(三)Fetcher

来源:互联网 发布:证书查询系统源码php 编辑:程序博客网 时间:2024/06/07 19:12

            这节我们来看看Spiderman中的Fetcher模块的设计和实现。

            从这个模块开始就可以很明显的体现出Spiderman本身的微内核+插件化的总体设计思想,纵观Spiderman的源码设计,我们可以总结出,所谓的微内核,其本质上就是抽象出业务系统中不变的东西,例如:通用的配置,通用的业务框架,另外就是从变得东西中总结出不变的,再将这些东西转换成面向对象设计的类,用设计模式加以组织,就可以作为一个系统中底层和框架性的东西,从而使之具有高度的可扩展性和生命力,对于一个普通的工程师来讲,要能很好的具备这方面的能力,必须要经过反复的实践,总结,再实践,在这个螺旋式上升的过程中始终坚守设计模式的七大基本原则(哪七大原则?大家都回想一下,呵呵),只有这样,才能由量变到质变,最终才能在通往卓越的架构师之路上更进一步,有点跑题了,言归正传,下面我们就具体来看看Fetcher模块是如何做到微内核+插件化的。

            在Spiderman的core包的fetcher包中,可以看到关于Fetcher的类并不多也不复杂,总共就这几个类FetchRequest,FetchResult,Page,PageFetcher,SpiderConfig,Status。不多不少刚刚好,这些类基本覆盖了Fetcher模块所有的共性和不变的东西,例如:FetchRequest定义了fetcher的输入,FetchResult定义了输出,Page保存了下载下来的页面,由于一个页面有很多自身的属性,因此把页面单独作为一个类是合理的,PageFetcher则定义了所有具体的Fetcher类要遵守的接口,SpiderConfig则包含了下载页面时会用到的一些基本参数设置,而且这些参数基本上都是可以在每个Site的配置文件可配的。另外在core包的plugin包中提供了FetchPoint接口,由于这些类的定义都比较通俗易懂,在这里不在累述。

            可以看出没有具体的负责fetch的类,而在plugin包的util包中,提供了各式各样的fetche实现类,他们分别基于不同的开源包,有:基于htmlunit的HtmlUnitDownloader,基于httpclient的HttpClientDownloader,基于selenium的WebDriverDownloader,而且他们都实现了PageFetcher接口。

            那么内核是如何与上面这些插件联系起来的呢?下面我们就跟随源码来一步一步谈个究竟,前一节讲到,每个爬取任务(Task)的具体执行都是在一个Spider对象的run函数中,那么,我们就从这个函数作为入口来抽丝剥茧,寻根朔源。

          

//扩展点:fetch 获取HTTP内容FetchResult result = null;Collection<FetchPoint> fetchPoints = task.site.fetchPointImpls;if (fetchPoints != null && !fetchPoints.isEmpty()){for (Iterator<FetchPoint> it = fetchPoints.iterator(); it.hasNext(); ){FetchPoint point = it.next();result = point.fetch(task, result);}}

这里的FetchPoint就是所有Fetch实现的父接口,而在site对象中的fetchPointImpls包含了用户提供的所有实现了FetchPoint的插件类的实例(这些实例的加载在第一章节有详细描述),因此在这里的调度只需要逐一执行这些实例即可,我们看看FetchPoint的定义,有助于后面内容的理解:

public interface FetchPoint extends Point{//void context(Task task) throws Exception;FetchResult fetch(Task task, FetchResult result) throws Exception;}


果然,就只有一个fetch接口,参数很清晰,对当前Task进行解析,并把解析的结果返回,Spiderman在Plugin包的impl包中提供了FetchPoint的默认实现FetchPointImpl,注意,这个实现只是实现了FetchPoint接口,并不是实现了前面提到的具体fetch类,有人可能会问,那么那些具体的fetch类是如何由用户提供并被内核使用的呢?好,下面我们就来看看FetchPointImpl的fetch接口都做了什么:

public FetchResult fetch(Task task, FetchResult result) throws Exception {synchronized (site) {if (site.fetcher == null){SpiderConfig config = new SpiderConfig();if (task.site.getCharset() != null && task.site.getCharset().trim().length() > 0)config.setCharset(task.site.getCharset());if (task.site.getUserAgent() != null && task.site.getUserAgent().trim().length() > 0)config.setUserAgentString(task.site.getUserAgent());if ("1".equals(task.site.getIncludeHttps()) || "true".equals(task.site.getIncludeHttps()))config.setIncludeHttpsPages(true);if ("1".equals(task.site.getIsFollowRedirects()) || "true".equals(task.site.getIsFollowRedirects()))config.setFollowRedirects(true);String sdelay = task.site.getReqDelay();if (sdelay == null || sdelay.trim().length() == 0)sdelay = "200";int delay = CommonUtil.toSeconds(sdelay).intValue()*1000;if (delay < 0)delay = 200;config.setPolitenessDelay(delay);String timeout = task.site.getTimeout();if (timeout != null && timeout.trim().length() > 0){int to = CommonUtil.toSeconds(sdelay).intValue()*1000;if (to > 0)config.setConnectionTimeout(to);}<span style="color:#ff0000;">PageFetcher fetcher = null;String downloader = site.getDownloader();if (!CommonUtil.isBlank(downloader)) {    try {        Class<?> cls = Class.forName(downloader);        fetcher = (PageFetcher) cls.newInstance();    } catch (Throwable e) {        e.printStackTrace();    }}//默认是HttpClient下载器if (fetcher == null) {    fetcher = new HttpClientDownloader();}fetcher.init(config, site);site.fetcher = fetcher;</span>}String url = task.url.replace(" ", "%20");FetchRequest req = new FetchRequest();req.setUrl(url);req.setHttpMethod(task.httpMethod);return site.fetcher.fetch(req);}}

上面代码已经很清晰了,也比较容易理解,总结下来,可以得出三个结论:第一:每个Site的爬取Task都使用一个fetch实例,这个可以最大化复用现有类,而且也可以节省资源,还可以降低设计和实现的复杂度;第二:执行第一个Task的fetch时,其所在Site的fetch还没有创建,会进入if语句进行创建,创建细节稍后分析;第三:如果已经创建则直接创建一个基于当前url的FetchRequest并调用fetcher来实现具体的fetch,最后返回FetchResult。下面我们来看看fetcher的创建:

fetcher的创建分为两步:

第一步:基于配置中设置的属性来创建一个具体的fetcher的配置对象SpiderConfig

第二步:生成fetcher实例,用哪个实例呢?当然由用户指定了,这就是site.getDownloader,这个是在配置文件中可以配置的,用户自己可以实现自定义的fetcher(只需要实现PageFetcher接口即可),这里保存的就是这个自定义类的全名,在这里会基于此来动态创建对应的实例对象;如果没有定义,则会创建默认的HttpClientDownloader对象,并用前面生成的配置来初始化该fetcher实例,而且要赋值给site.fetcher,这样该site的下一个Task需要fetch时,就不需要再创建fetcher了。当然了,完全可以在配置中设置fetcher为前面提到的Spiderman已经提供的三个downloader,不用白不用嘛!

现在总算真相大白,这里的FetchPointImpl作用相当于一个中介者,负责内核(Spider)和插件(不同的Downloader)进行交互(是不是有点中介者模式的意思?),而且fetch函数里面的逻辑可以说属于共性的东西基本上不会改变,因此可以单独抽取出来作为一个单独的插件(这个插件可以理解为管理插件(具有管理职能),和复杂单一任务的插件,如每个具体的实现了PageFetcher接口的Downloader不同),之所以提取出来作为单独的插件没有放在内核包,我想作者的意思是,用户还可以实现自己的类似FetchPointImpl的不同实现,只要实现了FetchPoint接口就行,具体的那些Downloader所做的就是基于给定的参数各显神通把指定的页面下载下来并保存的既定的FetchResult中,这个和使用者没有关系,你们爱咋实现咋实现。

哎,归根结底,不论什么时候,要想开发出一个可扩展的框架还是要严格做到抽象与具体分离,定义和实现分离,实现和使用分离!把不变放在代码中,把变放在配置中,时刻牢记并熟练掌握迪米特法则和依赖倒换原则等等设计模式的基本原则,做到活学活用方能创造出高质量的产品。。。。

 

这一节就分析到这里,下一节我们来看看Spiderman中的Frontier。



 

0 0