最近的一个项目是写一个爬虫框架,这个框架主要采用Master-Slave的结构,Master负责管理要爬取的Url和已经爬取过的Url,Slave可以有多个,主要负责爬取网页内容,以及对爬取下来的网页内容进行持久化的工作。整个项目用Thrift作为RPC通信框架。

1. 爬虫流程

如果是一个单机版的爬虫,其实代码非常简单:

Initialize:
    UrlsDone = ∅
    UrlsTodo = {‘‘yahoo.com/index.htm’’, ..}

Repeat:
    url = UrlsTodo.getNext()
    ip = DNSlookup( url.getHostname() )
    html = DownloadPage( ip , url.getPath() )

    UrlsDone.insert( url )
        newUrls = parseForLinks( html )

    For each newUrl
        If not UrlsDone.contains( newUrl )
        then UrlsTodo.insert( newUrl )        

如果需要将UrlsDone和UrlsToDo这两个数据结构交由一个Master来管理,Master的接口可以定义成如下的:

public interface SpiderManager extends Closeable {
    /**
     * 获取一个待爬取的URL
     * @return URL
     */
    URLData poll();

    /**
     * 将一个待爬取的URL交给Manger
     */
    void offer(URLData url);

    /**
     * 将一个已经爬取的URL返回给Manager
     */
    void done(URLData url);

    /**
     * 判断一个URL是否已经爬取过
     * @param url
     * @return
     */
    boolean isDone(URLData url);

    /**
     * 已经处理过的URL数量
     */
    long doneSize();

    /**
     * 待处理的URL数量
     */
    long toDoSize();


    boolean usingAck();
}

  

2.分布式爬虫框架要解决的问题

上述单机的版的爬虫,在数据量不大和数据更新频率要求不高的情况下,可以很好的工作,但是当需要爬取的页面数量过多,或者网站有反爬虫限制的时候,上述代码并不能很好的工作。

例如通用的搜索爬虫需要爬取很多网页的时候,就需要多个爬虫来一起工作,这个时候各个爬虫必然要共享上述两个数据结构。

其次,现在很多网站对于爬虫都有限制,如果要是爬取的过于频繁,会被封Ip,为了应对这种情况,对应的策略是休眠一段时间,这样的话,又浪费了CPU资源。

最后,当要求实现不同的爬取策略,或者统一管理爬虫作业生命周期的时候,必然要一个Master来协调各个Slave的工作。

 

3. 设计实现

3.1 Master:

我们框架的主节点称为WebCrawlerMaster,针对不同的爬虫任务,WebCrawlerMaster会生成不同的WebCrawlerManager,WebCrawlerManger的功能是管理UrlsToDo和UrlsDone两个数据结构。Master主要的功能是管理WebCrawlerManager的实例,并且将不同的请求路由到对应的WebCrawlerManager上去。

爬虫框架设计

对于Master来说,最主要的组件是一个叫做MetaDataHolder的成员,它主要用来管理元数据信息。为了加强系统的健壮性,这部分信息是一定需要持久化的,至于持久化的选择,可以是Redis,或者关系型数据库,甚至写文件都可以。如果用Mysql来做持久化的工作,则需要做应用层的cache(通常用一个HashMap来实现)。

3.2 数据结构

对于一个CrawlManager,它主要管理两个数据结构UrlToDo,和UrlDone,前者可以抽象成一个链表,栈或者有优先级的队列,后者对外的表现是一个Set,做去重的工作。当定义出ADT(abstract data type)以后,则可以考虑出怎么样的去实现这个数据结构。这样的设计方法其实和实现一个数据结构是一样的,只不过当我们实现数据结构的时候,操作的对象是内存中的数组和列表,而在这个项目中,我们操作的对象是各种存储中提供给我们的功能,例如Redis中的List、Set,关系型数据库中的表等等。

 

4. 后记

这次的爬虫框架,从最开始的伪代码来看,是很简单的事情,但是一旦涉及到分布式的环境和系统的可扩展性,要真的实现起来,还是需要考虑到一些额外的东西,例如并发状态下共享数据结构的读写、系统的高可用等等,但是我觉得这个项目真正让我满意的地方,是通过合理的数据结构行为层面的抽象,让这个爬虫系统有着很强的扩展性。例如现在默认的UrlToDo是一个FIFO的队列,这样的话,爬虫实际上是按照BSF的策略去爬取的。但是当UrlToDo配置成一个LIFO的stack以后,爬虫实际上按照DSF的策略去爬取的,而这样的变化,只需要的更改一下请求新的WebCrawlerManager的参数,爬虫的业务代码并不需要任何的修改。