互联网的发展,使人类提前进入了信息爆炸的年代,在浩瀚无边的信息海洋里,如何快速、准确找到对自己有用的信息,就成了一个很有价值的研究课题,于是,搜索引擎应运而生。现在,国内外大大小小的搜索引擎有很多,搜搜也是这搜索引擎大军中的一员悍将。笔者有幸参与了搜搜研发过程中的一些工作,在这里写一些自己的理解与看法,权当是抛砖引玉,希望能够得到业内前辈们的一些指点。
对于网页搜索引擎来说,它的基本处理流程,通常可以分为三个步骤:一是对海量互联网网页的抓取,也称下载;二是对已下载的网页进行预处理,包括抽取正文、建立正向索引、建立逆向索引等;三是向用户提供检索服务,包括对最终的检索结果进行打分排序,展示给用户等。从上面的介绍中可以看出,网页数据是网页搜索引擎最为根本的内容,没有丰富的网页数据做支撑的网页搜索引擎,它所能提供的服务是非常局限的。因此,一个强大的网页抓取系统(也称爬虫系统,又称Spider系统),对网页搜索引擎来说,是至关重要的。
一个强大的Spider系统,不仅要能够及时发现每天互联网上新产生的网页,还要能够及时地更新已经抓取的网页,使之最大程度上与互联网上真实存在的页面一致。下图是一个完整的Spider系统的基本结构:
从上图可以看出,一个完整的Spider系统,包括调度器(Scheduler)、抓取器(Crawler)、抽取器(Extractor)、Url库(Url DB)。调度器负责从Url库中选择需要抓取的Url列表,它是整个Spider系统的大脑;抓取器收到调度器所发送的待抓取Url列表,对其进行实际的抓取;抽取器收到抓取器下载完成的页面,对其进行处理,抽取页面正文,提取页面里包含的子链接;Url库接收并存储新发现的子链接相关信息,并对已抓取的Url的相关信息进行更新。这就是一个完整的Spider系统的基本的处理主流程。从本文的题目可以看出,本文要讲的重点是抓取器(Crawler),并不是整个Spider系统。下面就结合笔者之前所做的一些工作来谈谈,如何构建一个高性能的抓取器。
通过上面的介绍,我们知道,抓取器的主要工作就是从互联网上抓取网页,看上去好像很简单,其实任何简单的事情,要做的特别的好,都不是那么的简单。首先,我们来看一下一个抓取器的整体的一个结构视图,如下图所示:
从上图可以看出,一个抓取器总体上可以分为两层:一层是网络层(Network Frame),主要负责与上下游模块的通信交互,以及与Web服务器的交互进,行下载网页;一层是应用层(App Logic),完成抓取器内部的应用逻辑,比如Url对应的域名的IP的获取,压力控制(保证同一时间段内,对同IP的主机不会造成太大的压力),跳转识别(包括Http的跳转和页面内的跳转),页面附件(Js, Css, Frame等)解析及获取等一系列应用逻辑。因为抓取的网页内容经过一定的处理后,最终是要展示给用户的,而用户是通过浏览器来浏览页面的,所以我们希望抓取器所获取的页面与用户用浏览器打开的是一致的,因此,抓取器需要模拟一些浏览器的行为,做一些相关的工作,比如跳转识别等。下面分别从网络层和应用层两个方面来介绍一下具体的一些内容。
抓取器的网络层,具体来说有两个方面的工作要做,一个方面是与上下游模块的通信交互,一个方面是与互联网上的Web服务器进行交互,获取待抓取Url列表中Url所对应的页面。一方面要及时响应上游的抓取请求,另一方面要到互联网上不停地下载待抓取Url列表中Url所对应的页面,同时还要把抓取完成的页面经过程一定的处理后发给下游,这就需要我们有一个高性能、大吞吐量、高并发量的网络层做为支撑。在这里笔者将介绍一个比较常用的异步网络通讯框架的设计,供感兴趣的朋友做参考。下图所示是该框架的一个线程模型:
在上图中,IOMonitor是一个独立的线程,它的主要任务是检测网络IO事件,WorkThread是具体的工作线程。IOMonitor检测到网络IO事件后,向WorkThread发送消息进行通知。WorkThread在自己的线程中,不断地处理的收到的消息,实际的网络IO是发生在它里面的。WorkThread可以有多个,具体个数可以根据实际的应用需求进行设置。通常做法是与机器的逻辑CPU个数一样,这样会更加充分的利用机器的CPU资源。
在Linux下,IOMonitor可以用Epoll来实现,通过调用epoll_wait()函数来获取有事件发生的socket及其上下文信息(context),然后把该socket上的事件,通知到处理其IO的WorkThread中去。关于这里Epoll的使用,再多讲一点,在这里推荐使用Epoll的ET(Edge Triggered)模式,如果要用LT(Level Triggered)模式,建议使用OneShot方式的LT。至于ET和LT各是什么,这里不赘言,感兴趣的朋友可以参考Linux下的Mannual。
这里需要说明的是,这样做的目的是想更高效的地利用CPU资源。如果是在非Linux的平台下, 应该也有类似Epoll的机制,原理大同小异,这里也不再赘言。另外,关于socket本身,还有两点需要说明一下:一是在这里最好用非阻塞的socket, 这样在每一个环节都不会产生阻塞现象,不会产生因为前面的socket的阻塞而影响后面socket上事件的处理的情况;二是socket本身是有状态的,要在每个socket的上下文信息(context)中要维护该socket的当前状态,当WorkThread处理到某个socket上的事件时,要根据其状态采取不同的处理措施,可以采用类似下面的机制:
应用层的主要任务是有效地组织整个抓取过程、完成相关的应用逻辑,具体包括DNS获取,IP压力控制,跳转识别,附件提取等一系统功能。事实上不同的抓取器的应用层所要做的工作会因整个爬虫架构的不同而千差万别。但这里重点不在于此,而是在于介绍一种事件触发(或者说消息通知)的模型,正是这种模型的运用,才使得我们的抓取器能够达到每秒几百甚至上千篇页面的抓取速度。
实际上,上一部分中讲到的网络层的模型,也是一种简单事件触发的模型,只不过上面提到的事件,只有网络的可读(Read)、可写(Write)和异常(Exception)三种。事件触发模型的关键,首先是要把内部的任务抽象成一个有状态的Task(比如我们可以把抓取任务抽象为CrawlTask),然后对于每一个Task,它的整个处理流程是一个完整的状态机(State Machine),而且它的每次状态转移,都是由某个事件(Event)来触发的。这里的事件(Event)也是一种抽象,它可以是来自外部的网络事件,也可以是内部的某种事件,它的特点就是当事件处理器处理到它的时候,必然会伴随着某个Task的状态机的状态转移,换言之,Task的状态机是由事件(Event)来驱动的。通常的事件触发模型的设计中,会有一个或者多个事件处理器,当有事件发生的时候,其中某个事件处理器就会处理它,首先根据该事件找到它所对应的Task,然后调用该Task在当前状态下对应于该类型事件的处理函数,最后根据上面的处理结果,对该Task进行状态转移。
以上即是本文的主要内容,最后想谈一点对今后的一些想法。记得09年及之前的几年,互联网上有一个叫SaaS的概念很火。SaaS的全称是“软件即服务(Software as a Service)”,大致的意思是说要把软件都做成服务,开发者不再向消费者直接卖软件,而是把软件做成一种服务,就像日常生活中的水、电一样,提供给用户。未来的抓取器,也不再仅仅是一个抓取器,它也可以发展成为一种抓取服务(Crawl Service),它不仅能够做为搜索引擎的Spider系统中用来抓取网页的一个单元来完成网页抓取的任务,还要能够满足其他用户的各种各样的抓取(下载)需求,因此,我们有理由相信未来的抓取器的发展也将是CaaS------Crawl as a Service。