使用手冊(cè)

一、Gecco是什么

Gecco是一款用java語(yǔ)言開(kāi)發(fā)的輕量化的易用的網(wǎng)絡(luò)爬蟲(chóng),不同于Nutch這樣的面向搜索引擎的通用爬蟲(chóng),Gecco是面向主題的爬蟲(chóng)。

  • 通用爬蟲(chóng)一般關(guān)注三個(gè)主要的問(wèn)題:下載、排序、索引。
  • 主題爬蟲(chóng)一般關(guān)注的是:下載、內(nèi)容抽取、靈活的業(yè)務(wù)邏輯處理。

Gecco的目標(biāo)是提供一個(gè)完善的主題爬蟲(chóng)框架,簡(jiǎn)化下載和內(nèi)容抽取的開(kāi)發(fā),利用管道過(guò)濾器模式,提供靈活的內(nèi)容清洗和持久化處理模式,讓開(kāi)發(fā)人員把更多的精力投入到與業(yè)務(wù)主題相關(guān)的內(nèi)容處理上。

主要特征

  • 簡(jiǎn)單易用,使用jquery的selector風(fēng)格抽取元素
  • 支持頁(yè)面中的異步ajax請(qǐng)求
  • 支持頁(yè)面中的javascript變量抽取
  • 利用Redis實(shí)現(xiàn)分布式抓取,參考gecco-redis
  • 支持下載時(shí)UserAgent隨機(jī)選取
  • 支持下載代理服務(wù)器隨機(jī)選取
  • 支持結(jié)合Spring開(kāi)發(fā)業(yè)務(wù)邏輯,參考gecco-spring
  • 支持htmlunit擴(kuò)展,參考gecco-htmlunit
  • 支持插件擴(kuò)展機(jī)制

二、一分鐘你就可以寫(xiě)一個(gè)簡(jiǎn)單爬蟲(chóng)

示例代碼

這里用抓取gecco這個(gè)項(xiàng)目的首頁(yè)為例。我們希望得到項(xiàng)目的作者名稱(chēng),項(xiàng)目名稱(chēng),項(xiàng)目的star和fork數(shù)量,以及項(xiàng)目的介紹。如果你稍有java基礎(chǔ),會(huì)寫(xiě)jquery的css selector我相信下面的代碼我不需要解釋你也能輕松的看明白。

    @Gecco(matchUrl="https://github.com/{user}/{project}", pipelines="consolePipeline")
    public class MyGithub implements HtmlBean {

        private static final long serialVersionUID = -7127412585200687225L;

        @RequestParameter("user")
        private String user;

        @RequestParameter("project")
        private String project;

        @Text
        @HtmlField(cssPath=".repository-meta-content")
        private String title;

        @Text
        @HtmlField(cssPath=".pagehead-actions li:nth-child(2) .social-count")
        private int star;

        @Text
        @HtmlField(cssPath=".pagehead-actions li:nth-child(3) .social-count")
        private int fork;

        @Html
        @HtmlField(cssPath=".entry-content")
        private String readme;

        public String getReadme() {
            return readme;
        }

        public void setReadme(String readme) {
            this.readme = readme;
        }

        public String getUser() {
            return user;
        }

        public void setUser(String user) {
            this.user = user;
        }

        public String getProject() {
            return project;
        }

        public void setProject(String project) {
            this.project = project;
        }

        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public int getStar() {
            return star;
        }

        public void setStar(int star) {
            this.star = star;
        }

        public int getFork() {
            return fork;
        }

        public void setFork(int fork) {
            this.fork = fork;
        }

        public static void main(String[] args) {
            GeccoEngine.create()
            //Gecco搜索的包路徑
            .classpath("com.geccocrawler.gecco.demo")
            //開(kāi)始抓取的頁(yè)面地址
            .start("https://github.com/xtuhcy/gecco")
            //開(kāi)啟幾個(gè)爬蟲(chóng)線(xiàn)程
            .thread(1)
            //單個(gè)爬蟲(chóng)每次抓取完一個(gè)請(qǐng)求后的間隔時(shí)間
            .interval(2000)
            .start();
        }
    }

代碼說(shuō)明

  • 接口HtmlBean說(shuō)明該爬蟲(chóng)是一個(gè)解析html頁(yè)面的爬蟲(chóng)(gecco還支持json格式的解析)
  • 注解@Gecco告知該爬蟲(chóng)匹配的url格式(matchUrl)和內(nèi)容抽取后的bean處理類(lèi)(pipelines處理類(lèi)采用管道過(guò)濾器模式,可以定義多個(gè)處理類(lèi))。
  • 注解@RequestParameter可以注入url中的請(qǐng)求參數(shù),如@RequestParameter("user")表示匹配url中的{user}
  • 注解@HtmlField表示抽取html中的元素,cssPath采用類(lèi)似jquery的css selector選取元素
  • 注解@Text表示獲取@HtmlField抽取出來(lái)的元素的text內(nèi)容
  • 注解@Html表示獲取@HtmlField抽取出來(lái)的元素的html內(nèi)容(如果不指定默認(rèn)為@Html)
  • GeccoEngine表示爬蟲(chóng)引擎,通過(guò)create()初始化,通過(guò)start()/run()運(yùn)行??梢耘渲靡恍﹩?dòng)參數(shù)如:掃描@Gecco注解的包名classpath;開(kāi)始抓取的url地址star;抓取線(xiàn)程數(shù)thread;抓取完一個(gè)頁(yè)面后的間隔時(shí)間interval(ms)等

三、軟件總體結(jié)構(gòu)

基本構(gòu)件介紹

GeccoEngine

GeccoEngine是爬蟲(chóng)引擎,每個(gè)爬蟲(chóng)引擎最好是一個(gè)獨(dú)立進(jìn)程,在分布式爬蟲(chóng)場(chǎng)景下,建議每臺(tái)爬蟲(chóng)服務(wù)器(物理機(jī)或者虛機(jī))運(yùn)行一個(gè)GeccoEngine。爬蟲(chóng)引擎包括主要Scheduler、Downloader、Spider、SpiderBeanFactory、PipelineFactory5個(gè)主要模塊。

Scheduler

通常爬蟲(chóng)需要一個(gè)有效管理下載地址的角色,Scheduler負(fù)責(zé)下載地址的管理。gecco對(duì)初始地址的管理使用StartScheduler,StartScheduler內(nèi)部采用一個(gè)阻塞的FIFO的隊(duì)列。初始地址通常會(huì)派生出很多其他待抓取的地址,派生出來(lái)的其他地址采用SpiderScheduler進(jìn)行管理,SpiderScheduler內(nèi)部采用線(xiàn)程安全的非阻塞FIFO隊(duì)列。這種設(shè)計(jì)使的gecco對(duì)初始地址采用了深度遍歷的策略,即一個(gè)線(xiàn)程抓取完一個(gè)初始地址后才會(huì)去抓取另外一個(gè)初始地址;對(duì)初始地址派生出來(lái)的地址,采用廣度優(yōu)先策略。

Downloader

Downloader負(fù)責(zé)從Scheduler中獲取需要下載的請(qǐng)求,gecco默認(rèn)采用httpclient4.x作為下載引擎。通過(guò)實(shí)現(xiàn)Downloader接口可以自定義自己的下載引擎。你也可以對(duì)每個(gè)請(qǐng)求定義BeforeDownload和AfterDownload,實(shí)現(xiàn)不同的請(qǐng)求下載的個(gè)性需求。

SpiderBeanFactory

Gecco將下載下來(lái)的內(nèi)容渲染為SpiderBean,所有爬蟲(chóng)渲染的JavaBean都統(tǒng)一繼承SpiderBean,SpiderBean又分為HtmlBean和JsonBean分別對(duì)應(yīng)html頁(yè)面的渲染和json數(shù)據(jù)的渲染。SpiderBeanFactroy會(huì)根據(jù)請(qǐng)求的url地址,匹配相應(yīng)的SpiderBean,同時(shí)生成該SpiderBean的上下文SpiderBeanContext。上下文SpiderBeanContext會(huì)告知這個(gè)SpiderBean采用什么渲染器,采用那個(gè)下載器,渲染完成后采用哪些pipeline處理等相關(guān)上下文信息。

PipelineFactory

pipeline是SpiderBean渲染完成的后續(xù)業(yè)務(wù)處理單元,PipelineFactory是pipeline的工廠(chǎng)類(lèi),負(fù)責(zé)pipeline實(shí)例化。通過(guò)擴(kuò)展PipelineFactory就可以實(shí)現(xiàn)和Spring等業(yè)務(wù)處理框架的整合。

Spider

Gecco框架最核心的類(lèi)應(yīng)該是Spider線(xiàn)程,一個(gè)爬蟲(chóng)引擎可以同時(shí)運(yùn)行多個(gè)Spider線(xiàn)程。Spider描繪了這個(gè)框架運(yùn)行的基本骨架,先從Scheduler獲取請(qǐng)求,再通過(guò)SpiderBeanFactory匹配SpiderBeanClass,再通過(guò)SpiderBeanClass找到SpiderBean的上下文,下載網(wǎng)頁(yè)并對(duì)SpiderBean做渲染,將渲染后的SpiderBean交個(gè)pipeline處理。

四、GeccoEngine

Gecco如何運(yùn)行

Gecco的初始化和啟動(dòng)通過(guò)GeccoEngine完成,GeccoEngine主要負(fù)責(zé)初始化配置、開(kāi)始請(qǐng)求的配置和啟動(dòng)爬蟲(chóng)運(yùn)行,最基本的啟動(dòng)方法:

GeccoEngine.create()
    .classpath("com.geccocrawler.gecco.demo")
    .start("https://github.com/xtuhcy/gecco")
    .start();

classpath是必填項(xiàng),指定掃描@Gecco的包路徑。start是初始請(qǐng)求地址。start()表示采用非阻塞方式運(yùn)行爬蟲(chóng)。

GeccoEngine基本配置項(xiàng)

  • loop(true):表示是否循環(huán)抓取,默認(rèn)為false
  • thread(2):表示開(kāi)啟的爬蟲(chóng)線(xiàn)程數(shù)量,默認(rèn)是1,需要注意的是線(xiàn)程數(shù)量要小于或者等于start請(qǐng)求的數(shù)量
  • interval(2000):表示某個(gè)線(xiàn)程在抓取完成一個(gè)請(qǐng)求后的間隔時(shí)間,單位是毫秒,系統(tǒng)會(huì)在左右1秒時(shí)間內(nèi)隨機(jī)。如果為2000,系統(tǒng)會(huì)在1000~3000之間隨機(jī)選取。
  • mobile(false):表示使用移動(dòng)端還是pc端的UserAgent。默認(rèn)為false使用pc端的UserAgent。
  • debug(true):是否開(kāi)啟debug模式,如果開(kāi)啟debug模式,會(huì)在控制臺(tái)輸出jsoup元素抽取的日志。
  • pipelineFactory(PipelineFactory):自定義Pipeline工廠(chǎng)類(lèi)
  • scheduler(Scheduler):自定義請(qǐng)求隊(duì)列管理器

非阻塞啟動(dòng)和阻塞啟動(dòng)

  • start():非阻塞啟動(dòng),GeccoEngine會(huì)單獨(dú)啟動(dòng)線(xiàn)程運(yùn)行,推薦以該方式運(yùn)行。線(xiàn)程模型如下:

Main Thread-->GeccoEngine Thread-->Spider Thread

  • run():阻塞啟動(dòng),GeccoEngine在主線(xiàn)程中啟動(dòng)運(yùn)行,非循環(huán)模式GeccoEngine需要等待其他爬蟲(chóng)線(xiàn)程運(yùn)行完畢后才會(huì)退出。線(xiàn)程模型r如下:

Main Thread-->Spider Thread

Gecco如何匹配URL

  • 匹配URL的作用

匹配URL是告知Gecco,這種格式的url對(duì)應(yīng)的網(wǎng)頁(yè)會(huì)被渲染成當(dāng)前的SpiderBean。

  • matchUrl如何寫(xiě)

完全匹配:@Gecco(matchUrl="https://github.com/"),完全匹配https://github.com/這個(gè)url

模糊匹配:@Gecco(matchUrl="https://github.com/{user}/{project}"),會(huì)匹配所有類(lèi)似格式的url,https://github.com/xtuhcy/gecco、https://github.com/xtuhcy/gecco-spring。模糊匹配的{}可以匹配任意非空白字符串但是不包含斜杠(/

任意匹配:@Gecco,如果你不寫(xiě)matchUrl,任何格式的url都會(huì)被匹配,這種方式一般用作通用爬蟲(chóng),例如:

@Gecco(pipelines="consolePipeline")
public class CommonCrawler implements HtmlBean {

    private static final long serialVersionUID = -8870768223740844229L;

    @Request
    private HttpRequest request;

    @HtmlField(cssPath="body")
    private String body;

    public HttpRequest getRequest() {
        return request;
    }

    public void setRequest(HttpRequest request) {
        this.request = request;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }

    public static void main(String[] args) {
        GeccoEngine.create()
        .classpath("com.geccocrawler.gecco.demo")
        .start("https://www.baidu.com/")
        .interval(2000)
        .start();
    }
}

五、從下載說(shuō)起

一、下載引擎

爬蟲(chóng)最基本的能力就是發(fā)起http請(qǐng)求,下載網(wǎng)頁(yè),gecco默認(rèn)采用httpclient4作為下載引擎。通過(guò)實(shí)現(xiàn)Downloader接口可以自定義自己的下載引擎,在啟動(dòng)GeccoEngine時(shí)需要設(shè)置自己的下載引擎。下面的代碼不是使用默認(rèn)的httpclient作為下載引擎,而是使用htmlUnit作為下載引擎。

@Gecco(matchUrl="https://github.com/{user}/{project}", pipelines="consolePipeline", downloader="htmlUnitDownloder")

二、HttpRequest和HttpResponse

HttpRequest表示下載請(qǐng)求,HttpResponse表示下載響應(yīng)。爬蟲(chóng)下載網(wǎng)頁(yè)都會(huì)模擬瀏覽器包裝成GET或者POST請(qǐng)求,HttpGetRequest和HttpPostRequest分別對(duì)應(yīng)GET和POST請(qǐng)求。

  • gecco的請(qǐng)求支持模擬userAgent,并且支持userAgent的隨機(jī)輪詢(xún),在calsspath的根目錄下定義userAgents文件,每行表示一個(gè)UserAgent; - gecco支持cookie定義,request.addCookie(),可以模擬用戶(hù)登錄; - gecco支持代理服務(wù)器的隨機(jī)輪詢(xún),在classpath的根目錄下定義proxys文件,每行表示一個(gè)代理服務(wù)器的主機(jī)和端口,如127.0.0.1:8888;

三、下載地址管理

通常爬蟲(chóng)需要一個(gè)有效管理下載地址的角色,Scheduler負(fù)責(zé)下載地址的管理。gecco對(duì)初始地址的管理使用StartScheduler,StartScheduler內(nèi)部采用一個(gè)阻塞的FIFO的隊(duì)列。初始地址通常會(huì)派生出很多其他待抓取的地址,派生出來(lái)的其他地址采用SpiderScheduler進(jìn)行管理,SpiderScheduler內(nèi)部采用線(xiàn)程安全的非阻塞FIFO隊(duì)列。這種設(shè)計(jì)使的gecco對(duì)初始地址采用了深度遍歷的策略,即一個(gè)線(xiàn)程抓取完一個(gè)初始地址后才會(huì)去抓取另外一個(gè)初始地址;對(duì)初始地址派生出來(lái)的地址,采用廣度優(yōu)先策略,派生地址的獲取會(huì)在下面詳細(xì)說(shuō)明。另外,gecco的分布式抓取也是通過(guò)Scheduler來(lái)完成了,準(zhǔn)確的說(shuō)是通過(guò)實(shí)現(xiàn)不同的StartScheduler來(lái)將初始地址分配到不同的服務(wù)器中來(lái)完成的。gecco的分布式抓取默認(rèn)采用redis來(lái)實(shí)現(xiàn),具體可以參考gecco-redis項(xiàng)目。

GeccoEngine.create().scheduler(new RedisStartScheduler("127.0.0.1:6379"))

四、初始地址和派生地址

爬蟲(chóng)通常會(huì)從若干個(gè)初始地址開(kāi)始爬取網(wǎng)頁(yè),爬取下來(lái)的網(wǎng)頁(yè)可能包含其他需要繼續(xù)抓取的頁(yè)面,典型的例子是列表頁(yè)和詳情頁(yè)。列表頁(yè)作為初始地址,抓取完成后需要針對(duì)每個(gè)列表項(xiàng)再抓取對(duì)應(yīng)的詳情頁(yè)。gecco派生地址的方式有兩種:一種是通過(guò)注解@Href(click=true),click為true時(shí),抽取出來(lái)的url會(huì)繼續(xù)抓取,如:

@Href(click=true)
@HtmlField(cssPath=".tm-qx-nrBox a")
private String detailUrl;

還一種是顯式的加入SpiderScheduler的方法,如:

SchedulerContext.into(currRequest.subRequest("subUrl");

使用@Request注解可以獲得當(dāng)前HttpRequest對(duì)象,使用當(dāng)前request的subRequest方法生成派生地址的請(qǐng)求,這樣在相應(yīng)的pipeline類(lèi)中即可控制派生請(qǐng)求的增加。

五、多個(gè)初始地址的配置

通過(guò)在classpath的根目錄下放置配置文件starts.json可以配置多個(gè)初始地址,如下:

[
    {
        "charset": "GBK",
        "url": "http://item.jd.com/1455427.html"
    },
    {
        "url": "https://github.com/xtuhcy/gecco"
    }
]

六、BeforeDownload和AfterDownload

一些特殊的場(chǎng)景,可能會(huì)需要在下載前做下載的預(yù)處理和在下載后做特定的處理,之后再傳遞給pipeline進(jìn)行處理,這時(shí)需要實(shí)現(xiàn)BeforeDownload和AfterDownload。如:

@GeccoClass(CruiseDetail.class)
public class CruiseDetailBeforeDownload implements BeforeDownload {
    @Override
    public void process(HttpRequest request) {

    }
}

上面的類(lèi)針對(duì)CruiseDetail這個(gè)bean的下載前會(huì)做自定義的處理。

@GeccoClass(CruiseRedirect.class)
public class CruiseRedirectAfterDownload implements AfterDownload {

    @Override
    public void process(HttpRequest request, HttpResponse response) {

    }

}

上面這個(gè)類(lèi)會(huì)對(duì)CruiseRedirect這個(gè)bean做下載后的自定義處理。

七、使用httpUnit作為下載引擎

gecco通過(guò)擴(kuò)展downloader實(shí)現(xiàn)了對(duì)htmlunit的支持。htmlunit是一款開(kāi)源的java 頁(yè)面分析工具,讀取頁(yè)面后,可以有效的使用htmlunit分析頁(yè)面上的內(nèi)容。項(xiàng)目可以模擬瀏覽器運(yùn)行,被譽(yù)為java瀏覽器的開(kāi)源實(shí)現(xiàn)。這個(gè)沒(méi)有界面的瀏覽器,運(yùn)行速度也是非常迅速的。htmlunit采用的是rhino作為javascript的解析引擎。

  • 下載

    <dependency>
        <groupId>com.geccocrawler</groupId>
        <artifactId>gecco-htmlunit</artifactId>
        <version>x.x.x</version>
    </dependency>
    
    
  • 優(yōu)缺點(diǎn)

使用htmlunit確實(shí)能省去很多工作,但是htmlunit也存在很多弊端:

1、效率低下,使用htmlunit后,下載器要將所有js一并下載下來(lái),同時(shí)要執(zhí)行所有js代碼,下載一個(gè)頁(yè)面有時(shí)需要5~10秒。

2、rhino引擎對(duì)js的兼容問(wèn)題,rhino的兼容性還是存在不少問(wèn)題的,上述demo就有很多js執(zhí)行錯(cuò)誤。如果大家在抓取時(shí)不想看到這些error日志輸出可以配置log4j:

log4j.logger.com.gargoylesoftware.htmlunit=OFF

3、使用selenium也可以達(dá)到類(lèi)似目的,selenium本身并不解析js,通過(guò)調(diào)用不同的瀏覽器驅(qū)動(dòng)達(dá)到模擬瀏覽器的目的。selenium支持chrome、IE、firefox等多個(gè)真實(shí)瀏覽器驅(qū)動(dòng),也支持htmlunit作為驅(qū)動(dòng),還支持PhantomJS這種js開(kāi)發(fā)的驅(qū)動(dòng)。

六、抽取頁(yè)面內(nèi)容

gecco的內(nèi)容抽取都是直接映射到j(luò)ava bean的屬性中,利用注解可以方便的注入頁(yè)面中的各種信息包括html頁(yè)面內(nèi)容、Ajax請(qǐng)求、javascript變量、request信息等

一、Html頁(yè)面內(nèi)容抽取

jsoup語(yǔ)法介紹

Selector選擇器概述  
tagname: 通過(guò)標(biāo)簽查找元素,比如:a  
ns|tag: 通過(guò)標(biāo)簽在命名空間查找元素,比如:可以用 fb|name 語(yǔ)法來(lái)查找 <fb:name> 元素  
#id: 通過(guò)ID查找元素,比如:#logo
.class: 通過(guò)class名稱(chēng)查找元素,比如:.masthead
[attribute]: 利用屬性查找元素,比如:[href]
[^attr]: 利用屬性名前綴來(lái)查找元素,比如:可以用[^data-] 來(lái)查找?guī)в蠬TML5 Dataset屬性的元素
[attr=value]: 利用屬性值來(lái)查找元素,比如:[width=500]
[attr^=value], [attr$=value], [attr*=value]: 利用匹配屬性值開(kāi)頭、結(jié)尾或包含屬性值來(lái)查找元素,比如:[href*=/path/]
[attr~=regex]: 利用屬性值匹配正則表達(dá)式來(lái)查找元素,比如: img[src~=(?i)\.(png|jpe?g)]
*: 這個(gè)符號(hào)將匹配所有元素
Selector選擇器組合使用  
el#id: 元素+ID,比如: div#logo  
el.class: 元素+class,比如: div.masthead  
el[attr]: 元素+class,比如: a[href]  
任意組合,比如:a[href].highlight
ancestor child: 查找某個(gè)元素下子元素,比如:可以用.body p 查找在"body"元素下的所有 p元素  
parent > child: 查找某個(gè)父元素下的直接子元素,比如:可以用div.content > p 查找 p 元素,也可以用body > * 查找body標(biāo)簽下所有直接子元素  
siblingA + siblingB: 查找在A元素之前第一個(gè)同級(jí)元素B,比如:div.head + div  
siblingA ~ siblingX: 查找A元素之前的同級(jí)X元素,比如:h1 ~ p  
el, el, el:多個(gè)選擇器組合,查找匹配任一選擇器的唯一元素,例如:div.masthead, div.logo  
偽選擇器selectors
:lt(n): 查找哪些元素的同級(jí)索引值(它的位置在DOM樹(shù)中是相對(duì)于它的父節(jié)點(diǎn))小于n,比如:td:lt(3) 表示小于三列的元素
:gt(n):查找哪些元素的同級(jí)索引值大于n,比如: div p:gt(2)表示哪些div中有包含2個(gè)以上的p元素
:eq(n): 查找哪些元素的同級(jí)索引值與n相等,比如:form input:eq(1)表示包含一個(gè)input標(biāo)簽的Form元素
:has(seletor): 查找匹配選擇器包含元素的元素,比如:div:has(p)表示哪些div包含了p元素
:not(selector): 查找與選擇器不匹配的元素,比如: div:not(.logo) 表示不包含 class=logo 元素的所有 div 列表
:contains(text): 查找包含給定文本的元素,搜索不區(qū)分大不寫(xiě),比如: p:contains(jsoup)
:containsOwn(text): 查找直接包含給定文本的元素
:matches(regex): 查找哪些元素的文本匹配指定的正則表達(dá)式,比如:div:matches((?i)login)
:matchesOwn(regex): 查找自身包含文本匹配指定正則表達(dá)式的元素
注意:上述偽選擇器索引是從0開(kāi)始的,也就是說(shuō)第一個(gè)元素索引值為0,第二個(gè)元素index為1等

詳細(xì)使用方式可以參考jsoup首頁(yè)手冊(cè)

@HtmlField

html屬性定義,表示該屬性是通過(guò)html查找解析,在html的渲染器下使用

  • cssPath:jquery風(fēng)格的元素選擇器,使用jsoup實(shí)現(xiàn)。jsoup在分析html方面提供了極大的便利。計(jì)劃實(shí)現(xiàn)xpath風(fēng)格的元素選擇器。

@Href

表示該字段是一個(gè)鏈接類(lèi)型的元素,jsoup會(huì)默認(rèn)獲取元素的href屬性值。屬性必須是String類(lèi)型。

  • value:默認(rèn)獲取href屬性值,可以多選,按順序查找
  • click:表示是否點(diǎn)擊打開(kāi),繼續(xù)讓爬蟲(chóng)抓取

@Image

表示該字段是一個(gè)圖片類(lèi)型的元素,jsoup會(huì)默認(rèn)獲取元素的src屬性值。屬性必須是String類(lèi)型。

  • value:默認(rèn)獲取src屬性值,可以多選,按順序查找
  • download:表示是否需要將圖片下載到本地

@Attr

獲取html元素的attribute。屬性支持java基本類(lèi)型的自動(dòng)轉(zhuǎn)換。

  • value:表示屬性名稱(chēng)

@Text

獲取元素的text或者owntext。屬性支持java基本類(lèi)型的自動(dòng)轉(zhuǎn)換。

  • own:是否獲取owntext,默認(rèn)為是

@Html

默認(rèn)類(lèi)型,可以不寫(xiě),獲取html元素的整個(gè)節(jié)點(diǎn)內(nèi)容。屬性必須是String類(lèi)型。

二、頁(yè)面中的Ajax請(qǐng)求

現(xiàn)在的頁(yè)面通常都會(huì)有很多ajax請(qǐng)求,很多主題爬蟲(chóng)在針對(duì)這種請(qǐng)求一般是使用htmlunit等類(lèi)似的渲染引擎,將頁(yè)面所有的信息都渲染完畢后交給爬蟲(chóng)處理,gecco可以通過(guò)實(shí)現(xiàn)自定義的downloader類(lèi)來(lái)實(shí)現(xiàn)類(lèi)似的方式。不過(guò)該方式會(huì)對(duì)整個(gè)頁(yè)面都進(jìn)行渲染,效率較低,很多時(shí)候你可能只需要某一個(gè)ajax請(qǐng)求就可以了。gecco實(shí)現(xiàn)了模擬ajax請(qǐng)求的定義,如:

@Ajax(url="http://p.3.cn/prices/mgets?skuIds=J_{code}")
private JDPrice price;

上述代碼通過(guò)ajax方式獲得jd商品的價(jià)格,{code}表示request中的屬性值,使用[...]可以引用已經(jīng)解析出來(lái)的html頁(yè)面中的屬性值。全部代碼可以參考源碼中的src/test/java/com/geccocrawler/gecco/demo/ajax

三、json格式內(nèi)容的抽取

有ajax請(qǐng)求涉及到j(luò)son格式的內(nèi)容抽取,gecco除了支持html的內(nèi)容抽取,同時(shí)也支持json內(nèi)容的抽取

@JSONPath

使用fastjson的jsonpath,jsonpath類(lèi)似是一種對(duì)象查詢(xún)語(yǔ)言,能方便的查詢(xún)json中個(gè)字段的值,詳情請(qǐng)查看fastjson-jsonpath

@JSONPath("$.p[0]")
private float price;

四、頁(yè)面中的javascript變量抽取

gecco支持抽取頁(yè)面中的javascript全局變量的內(nèi)容抽取。

@JSVar

  • var:表示變量名
  • jsonpath:如果變量是json格式的數(shù)據(jù),通過(guò)定義jsonpath抽取需要的內(nèi)容,如果不是json格式的數(shù)據(jù)可以不填

五、request信息的注入

@Request

將httpRequest信息注入到j(luò)ava bean中。

@RequestParameter

將url中定義的變量注入到j(luò)ava bean中。

六、自定義注解

通過(guò)實(shí)現(xiàn)CustomFieldRender接口可以實(shí)現(xiàn)自定義的注解。

@FieldRenderName("jdPricesFieldRender")
public class JdPricesFieldRender implements CustomFieldRender {
    /**
     * 
     * @param request HttpRequest
     * @param response HttpResponse
     * @param beanMap 將Field放入SpiderBean
     * @param bean 已經(jīng)注入后的SpiderBean
     * @param field 需要注入的Field
     */
    public void render(HttpRequest request, HttpResponse response, BeanMap beanMap, SpiderBean bean, Field field) {
        //獲取需要注入的內(nèi)容

        ...
        ...
        //進(jìn)內(nèi)容注入到對(duì)應(yīng)的field中
        beanMap.put(field.getName(), prices);
    }
}

在SpiderBean中引入自定義的注解

public class ProductList implements HtmlBean {

    ...

    @FieldRenderName("jdPricesFieldRender")
    private List<JDPrice> prices;

    ...
}

七、業(yè)務(wù)邏輯處理

至此頁(yè)面內(nèi)容已經(jīng)被gecco轉(zhuǎn)換為一個(gè)普通的javabean。剩下的工作就是將javabean進(jìn)一步清洗然后針對(duì)特定的業(yè)務(wù)邏輯進(jìn)行持久化等處理。

一、實(shí)現(xiàn)pipeline接口

gecco采用管道過(guò)濾器模式靈活的實(shí)現(xiàn)業(yè)務(wù)邏輯處理,首先實(shí)現(xiàn)一個(gè)特定的管道過(guò)濾器,如:

@PipelineName("consolePipeline")
public class ConsolePipeline implements Pipeline<SpiderBean> {

    @Override
    public void process(SpiderBean bean) {
        System.out.println(JSON.toJSONString(bean));
    }

}

然后將定義的pipeline關(guān)聯(lián)到對(duì)應(yīng)的javabean中,如:

@Gecco(matchUrl="https://github.com/{user}/{project}", pipelines="consolePipeline")

二、結(jié)合spring開(kāi)發(fā)業(yè)務(wù)邏輯

現(xiàn)在開(kāi)發(fā)java服務(wù)器端應(yīng)用已經(jīng)基本離不開(kāi)spring框架了,gecco實(shí)現(xiàn)了和spring框架的結(jié)合,請(qǐng)下載gecco-spring

使用注解方式配置Spring

掃描com.geccocrawler.gecco.spring,自動(dòng)注入springPipelineFactory和consolePipeline

<context:component-scan base-package="com.geccocrawler.gecco.spring" />

使用非注解方式配置Spring

手動(dòng)增加bean:springPipelineFactory和consolePipeline

<bean id="springPipelineFactory" class="com.geccocrawler.gecco.spring.SpringPipelineFactory"/>

<bean id="consolePipeline" class="com.geccocrawler.gecco.spring.ConsolePipeline"/>

初始化Gecco

加載完成bean后啟動(dòng)Gecco,可以通過(guò)繼承SpringGeccoEngine類(lèi),初始化你的GeccoEngine,需要特別注意的是GeccoEngine需要用非阻塞模式start()運(yùn)行:

@Service
public class InitGecco extends SpringGeccoEngine {

    @Override
    public void init() {
        GeccoEngine.create()
        .pipelineFactory(springPipelineFactory)
        .classpath("com.geccocrawler.gecco.spring")
        .start("https://github.com/xtuhcy/gecco")
        .interval(3000)
        .start();
    }

}

開(kāi)發(fā)Pipeline

pipeline的開(kāi)發(fā)和之前一樣,唯一不同的是不需要@PipelineName("consolePipeline")定義pipeline的名稱(chēng),而是使用spring的@Service定義,spring的bean名稱(chēng)即為pipeline的名稱(chēng)??梢詤⒖迹?/p>

@Service("consolePipeline")
public class ConsolePipeline implements Pipeline<SpiderBean> {
    @Override
    public void process(SpiderBean bean) {
        System.out.println(JSON.toJSONString(bean));
    }
}

八、爬蟲(chóng)的監(jiān)控

爬蟲(chóng)為什么要監(jiān)控

gecco是一個(gè)十分簡(jiǎn)單易用的java開(kāi)源爬蟲(chóng)框架,同時(shí)也一個(gè)款擁有很好擴(kuò)展性的框架,目前已經(jīng)有:

結(jié)合spring的插件gecco-spring

結(jié)合htmlunit的插件gecco-htmlunit

結(jié)合reids的插件gecco-reids

在開(kāi)發(fā)爬蟲(chóng)時(shí),由于要對(duì)很多網(wǎng)站和鏈接進(jìn)行抓取,并對(duì)抓取下來(lái)的網(wǎng)站進(jìn)行內(nèi)容的抽取。大量的鏈接下載和內(nèi)容抽取如果沒(méi)有監(jiān)控,很難發(fā)現(xiàn)問(wèn)題。特別是對(duì)于主題爬蟲(chóng),需要抽取頁(yè)面的具體內(nèi)容,如果網(wǎng)站改版務(wù)必要能盡快的發(fā)現(xiàn)并修正,gecco爬蟲(chóng)框架在完成了基本的框架和必要的插件的實(shí)現(xiàn)后,將重點(diǎn)放在了監(jiān)控的開(kāi)發(fā)上。

對(duì)擴(kuò)展開(kāi)放,對(duì)修改關(guān)閉的開(kāi)閉原則一致是gecco框架的基本設(shè)計(jì)原則。gecco爬蟲(chóng)的監(jiān)控模塊同樣基于該原則,基于jmx協(xié)議,使用aop模式。
輸入圖片說(shuō)明

監(jiān)控指標(biāo)

爬蟲(chóng)基本信息

  • 刷新基本信息:exec/com.geccocrawler.gecco:name=gecco/monitor

  • 讀取基本信息:read/com.geccocrawler.gecco:name=gecco

    {
    Interval: 5000,//抓取間隔時(shí)間ms
    StartTime: "2016-03-20 20:34:11",//抓取開(kāi)始時(shí)間
    ThreadCount: 1,//爬蟲(chóng)線(xiàn)程數(shù)量
    StarUrlCount: 8//初始url數(shù)量
    }
    
    

下載監(jiān)控

  • 獲取當(dāng)前正在抓取的所有域名:exec/com.geccocrawler.gecco:name=downloader/hosts

  • 獲取某個(gè)域名的下載監(jiān)控信息:exec/com.geccocrawler.gecco:name=downloader/statistics/xx.xx.com

  • 讀取下載監(jiān)控信息:read/com.geccocrawler.gecco:name=downloader

    Statistics: "{
        "exception":8,//該域名抓取異常的數(shù)量,主要是超時(shí)等異常
        "serverError":0,//該域名返回500,404等錯(cuò)誤信息的數(shù)量
        "success":3263//成功抓取數(shù)量
    }",
    Host: "xx.xx.com"http://域名
    
    

內(nèi)容抽取監(jiān)控

  • 刷新內(nèi)容抽取監(jiān)控信息:exec/com.geccocrawler.gecco:name=render/refresh

  • 獲取內(nèi)容抽取監(jiān)控信息:read/com.geccocrawler.gecco:name=render

    Statistics: "{
        "xx.xx.com":0,//域名xx.xx.com的網(wǎng)站內(nèi)容抽取的異常數(shù)量
        "yy.yy.com":0//域名yy.yy.com的網(wǎng)站內(nèi)容抽取的異常數(shù)量
    }"
    
    

jmxutils和jolokia

jmxutils

gecco的監(jiān)控使用了jmxutils這個(gè)開(kāi)源的mbean注解框架。在以前的開(kāi)發(fā)工作中要么就用原生的動(dòng)態(tài)mbean,要么是使用spring的jmx注解框架。原生的動(dòng)態(tài)mbean寫(xiě)起來(lái)太繁瑣,spring的jmx注解框架使用起來(lái)還是很方便的,但是現(xiàn)在spring感覺(jué)有些重。jmxutils這個(gè)框架很輕量,使用方法可以參考https://github.com/martint/jmxutils。

jolokia

Jolokia是一個(gè)利用JSON通過(guò)Http實(shí)現(xiàn)JMX遠(yuǎn)程管理的開(kāi)源項(xiàng)目。具有快速、簡(jiǎn)單等特點(diǎn)。除了支持基本的JMX操作之外,它還提供一些獨(dú)特的特性來(lái)增強(qiáng)JMX遠(yuǎn)程管理如:批量請(qǐng)求,細(xì)粒度安全策略等。也就是說(shuō)jmx的mbean可以通過(guò)http來(lái)訪(fǎng)問(wèn)不需要在啟動(dòng)java時(shí)配置那么多參數(shù)。只需要新增一個(gè)servlet:

<servlet>
    <servlet-name>jolokia-agent</servlet-name>
    <servlet-class>org.jolokia.http.AgentServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>jolokia-agent</servlet-name>
    <url-pattern>/jmx/*</url-pattern>
</servlet-mapping>

這樣應(yīng)用中的mbean就能輕松控制和訪(fǎng)問(wèn)。jolokia還提供了java客戶(hù)端和js客戶(hù)端來(lái)訪(fǎng)問(wèn)mbean,具體的使用方法和權(quán)限控制可以查看jolokia的官方文檔https://jolokia.org/reference/html/index.html

九、穩(wěn)定性測(cè)試

最近對(duì)開(kāi)源的java爬蟲(chóng)Gecco做了一個(gè)穩(wěn)定性測(cè)試,測(cè)試環(huán)境:一臺(tái)爬蟲(chóng)+web應(yīng)用服務(wù)器,一臺(tái)mongodb服務(wù)器。服務(wù)器配置很low,兩臺(tái)都是阿里云最低端的主機(jī),1核+512內(nèi)存。

單線(xiàn)程測(cè)試場(chǎng)景

爬蟲(chóng)采用單線(xiàn)程,測(cè)試時(shí)間3×24小時(shí),測(cè)試期間系統(tǒng)無(wú)異常,jvm內(nèi)存穩(wěn)定。測(cè)試結(jié)果:

  • 基本信息

    Interval: 5000,
    StartTime: "2016-03-22 14:47:40",
    ThreadCount: 1,
    StarUrlCount: 8
    
    

    單線(xiàn)程,共有8個(gè)初始抓取鏈接,每個(gè)請(qǐng)求抓取完成后休息5秒。

  • 爬蟲(chóng)監(jiān)控?cái)?shù)據(jù)

    taocan.ctrip.com
    Statistics: "{"exception":134,"serverError":0,"success":11270}"
    
    vacations.ctrip.com
    Statistics: "{"exception":61,"serverError":0,"success":17548}"
    
    huodong.ctrip.com
    Statistics: "{"exception":42,"serverError":0,"success":11814}"
    
    www.tuniu.com
    Statistics: "{"exception":4,"serverError":0,"success":228}"
    
    temai.tuniu.com
    Statistics: "{"exception":78,"serverError":0,"success":3507}"
    
    www.lvmama.com
    Statistics: "{"exception":0,"serverError":415,"success":41}"
    http://www.lvmama.com/tuangou/sale-623250 DOWNLOAD ERROR :500
    http://www.lvmama.com/tuangou/sale-612687 DOWNLOAD ERROR :400
    
    
  • 結(jié)果

從監(jiān)控?cái)?shù)據(jù)可以看到:

ctrip.com相關(guān)的數(shù)據(jù)抓取成功率較高,為99.99%,出現(xiàn)的失敗都是exception,也就是類(lèi)似超時(shí)之類(lèi)的錯(cuò)誤。

tuniu.com相關(guān)的數(shù)據(jù)抓取成功率也較高,為99.97%,出現(xiàn)的失敗也是exception。

lvmama.com的成功率就十分低了,而且返回都是serverError也就是服務(wù)器500或者400錯(cuò)誤,查看發(fā)日志發(fā)現(xiàn)可能對(duì)方服務(wù)器對(duì)ip做了訪(fǎng)問(wèn)限制,在成功抓取10多條后就一直報(bào)400或者500錯(cuò)誤。

多線(xiàn)程測(cè)試場(chǎng)景

爬蟲(chóng)采用3線(xiàn)程,測(cè)試時(shí)間2×24小時(shí),測(cè)試期間系統(tǒng)無(wú)異常,jvm內(nèi)存穩(wěn)定。測(cè)試結(jié)果:

  • 基本信息

    Interval: 5000,
    StartTime: "2016-03-26 11:16:57",
    ThreadCount: 3,
    StarUrlCount: 8
    
    

3線(xiàn)程,共有8個(gè)初始抓取鏈接,每個(gè)請(qǐng)求抓取完成后休息5秒。

  • 爬蟲(chóng)監(jiān)控?cái)?shù)據(jù)

    taocan.ctrip.com
    Statistics: "{"exception":58,"serverError":0,"success":19306}"
    
    vacations.ctrip.com
    Statistics: "{"exception":51,"serverError":0,"success":31402}"
    
    huodong.ctrip.com
    Statistics: "{"exception":62,"serverError":0,"success":17807}"
    
    www.tuniu.com
    Statistics: "{"exception":2,"serverError":0,"success":466}"
    
    temai.tuniu.com
    Statistics: "{"exception":118,"serverError":0,"success":5603}"
    
    www.lvmama.com
    Statistics: "{"exception":1,"serverError":410,"success":39}"
    http://www.lvmama.com/tuangou/deal-580212 DOWNLOAD ERROR :400
    
    
  • 結(jié)果

從監(jiān)控?cái)?shù)據(jù)可以看到和單線(xiàn)程結(jié)果基本一致

總結(jié)

從測(cè)試中可以發(fā)現(xiàn),開(kāi)源java爬蟲(chóng)Gecco對(duì)系統(tǒng)要求很低,體現(xiàn)其輕量化的特點(diǎn)。無(wú)論在單線(xiàn)程還是多線(xiàn)程環(huán)境下,系統(tǒng)均能穩(wěn)定運(yùn)行。對(duì)部分網(wǎng)站訪(fǎng)問(wèn)限制的問(wèn)題,需要通過(guò)代理服務(wù)器來(lái)完成,Gecco是支持代理服務(wù)器隨機(jī)選取的。

十、Gecco爬蟲(chóng)框架的線(xiàn)程和隊(duì)列模型

簡(jiǎn)述

爬蟲(chóng)在抓取一個(gè)頁(yè)面后一般有兩個(gè)任務(wù),一個(gè)是解析頁(yè)面內(nèi)容,一個(gè)是將需要繼續(xù)抓取的url放入隊(duì)列繼續(xù)抓取。因此,當(dāng)爬取的網(wǎng)頁(yè)很多的情況下,待抓取url的管理也是爬蟲(chóng)框架需要解決的問(wèn)題。本文主要說(shuō)的是gecco爬蟲(chóng)框架的隊(duì)列和線(xiàn)程模型。

線(xiàn)程和隊(duì)列模型

線(xiàn)程和隊(duì)列模型
  • gecco的隊(duì)列模型是兩級(jí)隊(duì)列模型。分為初始請(qǐng)求隊(duì)列和派生請(qǐng)求隊(duì)列。初始請(qǐng)求隊(duì)列在循環(huán)模式下是一個(gè)阻塞式的FIFO隊(duì)列,在非循環(huán)模式下是一個(gè)非阻塞式的FIFO隊(duì)列。派生隊(duì)列是一個(gè)非阻塞的剔重的FIFO隊(duì)列;
  • 線(xiàn)程首先去初始請(qǐng)求隊(duì)列按照FIFO原則獲取一個(gè)請(qǐng)求,如果線(xiàn)程數(shù)量大于初始請(qǐng)求隊(duì)列的數(shù)量,多余的線(xiàn)程就會(huì)待定新的初始請(qǐng)求入隊(duì),因此建議線(xiàn)程數(shù)量不要大于初始請(qǐng)求隊(duì)列的數(shù)量;
  • 對(duì)于循環(huán)模式loop(true),線(xiàn)程在抓取完成后,會(huì)將初始請(qǐng)求重新放入隊(duì)列;
  • 多線(xiàn)程只對(duì)初始請(qǐng)求隊(duì)列有效,每個(gè)線(xiàn)程會(huì)有自己的派生請(qǐng)求隊(duì)列,因此派生請(qǐng)求隊(duì)列是在單線(xiàn)程下運(yùn)行的,爬蟲(chóng)將派生請(qǐng)求放入隊(duì)列繼續(xù)抓取,直到?jīng)]有派生請(qǐng)求;
  • 線(xiàn)程在抓取完成派生請(qǐng)求后,會(huì)繼續(xù)向初始請(qǐng)求隊(duì)列獲取初始請(qǐng)求

為什么要用這種模型

  • Gecco的線(xiàn)程模型很像瀏覽器,每一個(gè)線(xiàn)程對(duì)應(yīng)一個(gè)瀏覽器的Tab。每個(gè)瀏覽器的Tab一次只能看一個(gè)頁(yè)面,因此就有了初始請(qǐng)求隊(duì)列多線(xiàn)程,派生請(qǐng)求隊(duì)列單線(xiàn)程的模型。
  • 使用這種隊(duì)列和線(xiàn)程模型開(kāi)發(fā)人員很好理解,結(jié)構(gòu)簡(jiǎn)單易懂,效率也能保證。想用多線(xiàn)程提高效率就想辦法放入初始請(qǐng)求隊(duì)列。

如何動(dòng)態(tài)的獲取初始請(qǐng)求隊(duì)列

如果想通過(guò)多線(xiàn)程提高爬蟲(chóng)的效率就需要想辦法將請(qǐng)求放入初始請(qǐng)求隊(duì)列。我們可以先通過(guò)一個(gè)爬蟲(chóng)引擎將待抓取的請(qǐng)求保存起來(lái)。另外一個(gè)爬蟲(chóng)引擎以第一個(gè)爬蟲(chóng)引擎獲取的請(qǐng)求作為初始請(qǐng)求開(kāi)啟多線(xiàn)程運(yùn)行。簡(jiǎn)單說(shuō)就是初始請(qǐng)求也是可以抓取出來(lái)的,并不一定非要寫(xiě)死。下面是jd采用多線(xiàn)程抓取的一段代碼,全部代碼已經(jīng)上傳github。

    //先獲取分類(lèi)列表,放入AllSortPipeline.sortRequests
    HttpGetRequest start = new HttpGetRequest("http://www.jd.com/allSort.aspx");
    start.setCharset("GBK");
    GeccoEngine.create()
    .classpath("com.geccocrawler.gecco.demo.jd")
    .start(start)
    .run();

    //分類(lèi)列表下的商品列表采用5線(xiàn)程抓取
    GeccoEngine.create()
    .classpath("com.geccocrawler.gecco.demo.jd")
    //從上面的GeccoEngine獲取初始請(qǐng)求
    .start(AllSortPipeline.sortRequests)
    .thread(5)
    .interval(2000)
    .start();

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容