垂直爬虫不同于通用爬虫,其目的在于解决如何快速高效定制,精确抽取网页内容,并保存为结构化数据。WebMagic就是一个设计十分灵活的垂直爬虫框架,非常适合二次开发和插件式地集成。本文希望通过这个简单的爬虫框架,探寻无处不在的设计原则和设计模式。

WebMagic核心模块设计

webmagic参考了scrapy的模块划分,分为Spider(整个爬虫的调度框架)、Downloader(页面下载)、PageProcessor(链接提取和页面分析)、Scheduler(URL管理)、Pipeline(离线分析和持久化)几部分。只不过scrapy通过middleware实现扩展,而webmagic则通过定义这几个接口,并将其不同的实现注入主框架类Spider来实现扩展。

Image

在这一部分,我们仅仅通过爬虫入口类的API设计对WebMagic的松耦合模块化设计有一个直观的感受。Spider的接口调用采用了链式的API设计,其他功能全部通过接口注入Spider实现,下面是启动一个比较复杂的Spider的例子。

Spider.create(sinaBlogProcessor)
.scheduler(new FileCacheQueueScheduler("/data/temp/webmagic/cache/"))
.pipeline(new FilePipeline())
.thread(10).run();

链式API设计充分体现各个模块的插件化设计,同时,如果我们需要二次开发其他功能,可以方便地通过接口注入Spider实现。

Downloader模块具体设计

在这一部分,我们选择Downloader模块来深入了解进一步的细节设计。

屏幕快照 2015-11-17 下午5.07.37.png

本模块仅对外暴露单一接口Downloader,由AbstractDownloader进行第一步实现,同时加入一些状态监控函数。HttpClientDownloader继承AbstractDownloader做具体网页内容爬取实现。

在这里很有趣的是使用了一个工厂模式,HttpClientDownloader组合了一个HttpClientGenerator对象,负责组装HttpClient的各项参数,如爬虫访问代理,压缩解码格式,cookie的生成,中断重连时间间隔等,并决定返回结果是CloseableHttpClient,顾名思义一定是HttpClient的子类。

我们来看HttpClientDownloader类的部分代码:

    private HttpClientGenerator httpClientGenerator = new HttpClientGenerator();
    //组合工厂类
    private CloseableHttpClient getHttpClient(Site site) {
        if (site == null) {
            return httpClientGenerator.getClient(null);
        }
        String domain = site.getDomain();
        CloseableHttpClient httpClient = httpClients.get(domain);
        if (httpClient == null) {
            synchronized (this) {
                httpClient = httpClients.get(domain);
                if (httpClient == null) {
                    httpClient = httpClientGenerator.getClient(site);//获取配置完毕的httpClient对象
                    httpClients.put(domain, httpClient);
                }
            }
        }
        return httpClient;
    }

再来看HttpClientGenerator类中的generateClient()方法代码:

private CloseableHttpClient generateClient(Site site) {
        HttpClientBuilder httpClientBuilder = HttpClients.custom().setConnectionManager(connectionManager);
        if (site != null && site.getUserAgent() != null) {
            httpClientBuilder.setUserAgent(site.getUserAgent());
        } else {
            httpClientBuilder.setUserAgent("");
        }//设置代理
        if (site == null || site.isUseGzip()) {
            httpClientBuilder.addInterceptorFirst(new HttpRequestInterceptor() {

                public void process(
                        final HttpRequest request,
                        final HttpContext context) throws HttpException, IOException {
                    if (!request.containsHeader("Accept-Encoding")) {
                        request.addHeader("Accept-Encoding", "gzip");
                    }//设置解压缩方式

                }
            });
        }
        SocketConfig socketConfig = SocketConfig.custom().setSoKeepAlive(true).setTcpNoDelay(true).build();//配置套接字
        httpClientBuilder.setDefaultSocketConfig(socketConfig);
        if (site != null) {
            httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(site.getRetryTimes(), true));
        }
        generateCookie(httpClientBuilder, site);//生成cookie
        return httpClientBuilder.build();
    }

工厂类是整个模式的关键所在。它包含必要的判断逻辑,能够根据外界给定的信息,决定究竟应该创建哪个具体类的对象。如果我们现在需要配置不同的httpclient对象,我们只需要在工厂类中设定判定逻辑,用户在使用时可以直接根据工厂类去创建所需的实例,而无需了解这些对象是如何创建以及如何组织的,这样HttpClientDownloader无需修改和重新编译。分离对象的创建和使用进一步解耦,将信息局部化,有利于整个软件体系结构的优化和维护,是黄金原则-创建与实现分离的典范。

然而,当我们需要获取不同配置的HttpClient时,我们还是需要修改这个Generator的工厂类啊,开放封闭原则何在?工厂类既需要判定如何组装产品配置,又要创建产品对象,单一职责原则何在?

所以,什么事情都没有完美,即使是经典的设计模式也不能保证满足所有的设计原则,优雅的结构和可维护性往往会占据更为重要的地位。即使是JDBC在使用数据库厂商提供的驱动程序接口与数据库管理系统进行数据交互的时候,也是使用工厂模式来屏蔽底层不同的数据库厂商的。所以,设计模式和设计原则是死的人是活的,如何设计和权衡利弊是门艺术。

回到这个模块本身的结构上来。大多数情况下,HttpClient已经足以获取html网页内容了,然而对于一些js动态加载的网页该怎么办呢?这方面的思路有两种:一种是抽丝剥茧,分析js的逻辑,再用爬虫去重现它(比如在网页中提取关键数据,再用这些数据去构造Ajax请求,最后直接从响应体获取想要的数据);另一种就是:内置一个浏览器,直接获取最后加载完的页面。对于这类定制的需求,我们只需要整理封装成一个独立的downloader,实现Dowloader接口即可完美嵌入爬虫中来,符合开闭原则。

使用WebMagic框架时的一些设计

上文中,我们“由面如点”观察了框架整体设计,并选取了Downloader模块深入探究。在这最后一部分,我讲一下我们项目中是如何使用这个框架的。

对我们而言,垂直爬虫的意义在于强大的页面分析能力。WebMagic集成了Xpath,可以简单精确选取html属性值。在webmagic里,PageProcessor是定制爬虫的核心。其中只要用户定义url的正则表达式,需要提取的html页面元素的表达式(Xpath或者自定义正则表达式),就可以轻松获取Page对象,该对象会以map的形式包含我们所需要的元素值。通过编写一个实现PageProcessor接口的类,就可以定制一个自己的爬虫。

现在我们面临一个问题,我们想复用以前写过的爬虫代码,特别是复杂的页面处理流程,自然而然,我们想到了使用适配器模式

适配器类代码如下:

public class PageProcessorAdapter implements PageProcessor{

    site = Site.me().setDomain("blog.csdn.net");
        site.setSleepTime(1);
    private CSDNPageCrawler csdnProcessor;//适配对象

    PageProcessorAdapter(CSDNPageCrawler c){
        this.csdnProcessor = c;
    }
    public void process(Page page) {
        // TODO Auto-generated method stub
        csdnProcessor.generateRegex();//调用适配对象方法
        List<String> links = csdnProcessor.getLinks();
        page.addTargetRequests(links);
        page.putField("content", page.getHtml().$(csdnProcessor.getContentRegex()));
    }

    public Site getSite() {
        // TODO Auto-generated method stub
        return site;
    }

}

Adapter模式是当一个类需要使用另一个类,而接口不同时,对良方的不同接口进行适配,其要达到的目的是,在调用方,采用统一的接口进行调用,而不管被调用者是什么,而被调用房更不会知道自己将会被谁调用,所以无法实现为调用者定制其接口,因此就没有意义去考虑调用方采用什么样的接口,这些接口的适配工作就由Adapter来完成。适配器模式是一种包装模式,它与装饰模式同样具有包装的功能,此外,对象适配器模式还具有委托的意思。总的来说,适配器模式属于补偿模式,专用来在系统后期扩展、修改时使用。然而,过多的使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。

我们使用适配器模式主要用于解决以前代码同WebMagic框架接口不匹配的问题,我们在实现PageProcessor中的process方法时,调用的是CSDNPageCrawler的对象的方法,这样我们无需修改任何以前代码的组织逻辑。

至此,我们以分析设计模式为主线,从整体设计,模块细节实现以及框架使用三个方面介绍了WebMagic这样一个极其简单的爬虫框架。更多细节,请参阅源代码~

相关引文:
  1. http://www.cnblogs.com/poissonnotes/archive/2010/12/01/1893871.html
  2. http://www.ibm.com/developerworks/cn/java/j-lo-adapter-pattern/index.html
  3. http://blog.sina.com.cn/s/blog_5c4dd33301014hv4.html
  4. http://my.oschina.net/flashsword/blog/145796#OSC_h2_1

登录发表评论 注册

反馈意见