1. 程式人生 > >(10)Java爬蟲框架webmagic學習筆記

(10)Java爬蟲框架webmagic學習筆記

Java爬蟲框架webmagic學習筆記

參考自:webmagic文件

webmagic簡介

webmagic的github網址:https://github.com/code4craft/webmagic

使用webmagic的原因:

  1. webmagic是一個無須配置、便於二次開發的爬蟲框架,它提供簡單靈活的API,只需少量程式碼即可實現一個爬蟲。
  2. webmagic採用完全模組化的設計,功能覆蓋整個爬蟲的生命週期(連結提取、頁面下載、內容抽取、持久化),支援多執行緒抓取,分散式抓取,url自動去重,並支援自動重試、自定義UA/cookie等功能。
  3. webmagic包含強大的頁面抽取功能,開發者可以便捷的使用css selector、xpath和正則表示式進行連結和內容的提取,支援多個選擇器鏈式呼叫。
  4. webmagic也可以很方便的作為一個模組,嵌入Java專案中執行。
  5. 文件相對齊全。

webmagic的主要特色:

  • 完全模組化的設計,強大的可擴充套件性。
  • 核心簡單但是涵蓋爬蟲的全部流程,靈活而強大,也是學習爬蟲入門的好材料。
  • 提供豐富的抽取頁面API。
  • 無配置,但是可通過POJO+註解形式實現一個爬蟲。
  • 支援多執行緒。
  • 支援分散式。
  • 支援爬取js動態渲染的頁面。
  • 無框架依賴,可以靈活的嵌入到專案中去。

總體架構

WebMagic的結構分為DownloaderPageProcessorScheduler

Pipeline四大元件,並由Spider將它們彼此組織起來,讓它們可以互相互動,流程化地執行。這四大元件對應爬蟲生命週期中的下載、處理、管理和持久化等功能。

WebMagic總體架構圖如下:

image

WebMagic的四個元件

1. Downloader

Downloader負責從網際網路上下載頁面,以便後續處理。WebMagic預設使用了Apache HttpClient作為下載工具。

2. PageProcessor

PageProcessor負責解析頁面,抽取有用資訊,以及發現新的連結。WebMagic使用Jsoup作為HTML解析工具,並基於其開發瞭解析XPath的工具

Xsoup

在這四個元件中,PageProcessor對於每個站點每個頁面都不一樣,是需要使用者定製的部分。

3. Scheduler

Scheduler負責管理待抓取的URL,以及一些去重的工作。WebMagic預設提供了JDK的記憶體佇列來管理URL,並用集合來進行去重。也支援使用Redis進行分散式管理。

除非專案有一些特殊的分散式需求,否則無需自己定製Scheduler。

4. Pipeline

Pipeline負責抽取結果的處理,包括計算、持久化到檔案、資料庫等。WebMagic預設提供了“輸出到控制檯”和“儲存到檔案”兩種結果處理方案。

Pipeline定義了結果儲存的方式,如果你要儲存到指定資料庫,則需要編寫對應的Pipeline。對於一類需求一般只需編寫一個Pipeline

用於資料流轉的物件

1. Request

Request是對URL地址的一層封裝,一個Request對應一個URL地址。

它是PageProcessor與Downloader互動的載體,也是PageProcessor控制Downloader唯一方式。

除了URL本身外,它還包含一個Key-Value結構的欄位extra。你可以在extra中儲存一些特殊的屬性,然後在其他地方讀取,以完成不同的功能。例如附加上一個頁面的一些資訊等。

2. Page

Page代表了從Downloader下載到的一個頁面——可能是HTML,也可能是JSON或者其他文字格式的內容。

Page是WebMagic抽取過程的核心物件,它提供一些方法可供抽取、結果儲存等。在第四章的例子中,我們會詳細介紹它的使用。

3. ResultItems

ResultItems相當於一個Map,它儲存PageProcessor處理的結果,供Pipeline使用。它的API與Map很類似,值得注意的是它有一個欄位skip,若設定為true,則不應被Pipeline處理。

使用maven安裝webmagic框架

webmagic使用maven管理依賴,在專案中新增對應的依賴即可使用webmagic:

<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>webmagic-core</artifactId>
    <version>0.7.3</version>
</dependency>
<dependency>
    <groupId>us.codecraft</groupId>
    <artifactId>webmagic-extension</artifactId>
    <version>0.7.3</version>
</dependency>

WebMagic 使用slf4j-log4j12作為slf4j的實現.如果你自己定製了slf4j的實現,請在專案中去掉此依賴。

<exclusions>
    <exclusion>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
    </exclusion>
</exclusions>

專案結構

webmagic主要包括兩個包:

  • webmagic-core

    webmagic核心部分,只包含爬蟲基本模組和基本抽取器。webmagic-core的目標是成為網頁爬蟲的一個教科書般的實現。

  • webmagic-extension

    webmagic的擴充套件模組,提供一些更方便的編寫爬蟲的工具。包括註解格式定義爬蟲、JSON、分散式等支援。

webmagic還包含兩個可用的擴充套件包,因為這兩個包都依賴了比較重量級的工具,所以從主要包中抽離出來,這些包需要下載原始碼後自己編譯::

  • webmagic-saxon

    webmagic與Saxon結合的模組。Saxon是一個XPath、XSLT的解析工具,webmagic依賴Saxon來進行XPath2.0語法解析支援。

  • webmagic-selenium

    webmagic與Selenium結合的模組。Selenium是一個模擬瀏覽器進行頁面渲染的工具,webmagic依賴Selenium進行動態頁面的抓取。

在專案中,你可以根據需要依賴不同的包。

通過PageProcessor實現基本的爬蟲

PageProcessor的定製分為三個部分,分別是爬蟲的配置、頁面元素的抽取和連結的發現。

public class GithubRepoPageProcessor implements PageProcessor {

    // 部分一:抓取網站的相關配置,包括編碼、抓取間隔、超時時間、重試次數等,也包括一些模擬的引數,例如User Agent、cookie,以及代理的設定
    private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);

    @Override
    // process是定製爬蟲邏輯的核心介面,在這裡編寫抽取邏輯
    public void process(Page page) {
        // 部分二:定義如何抽取頁面資訊,並儲存下來
        page.putField("author", page.getUrl().regex("https://github\\.com/(\\w+)/.*").toString());
        page.putField("name", page.getHtml().xpath("//h1[@class='entry-title public']/strong/a/text()").toString());
        if (page.getResultItems().get("name") == null) {
            //skip this page
            page.setSkip(true);
        }
        page.putField("readme", page.getHtml().xpath("//div[@id='readme']/tidyText()"));
        // 部分三:從頁面發現後續的url地址來抓取
        page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/[\\w\\-]+/[\\w\\-]+)").all());
    }

    @Override
    public Site getSite() {
        return site;
    }

    public static void main(String[] args) {
        Spider.create(new GithubRepoPageProcessor())
                //從"https://github.com/code4craft"開始抓
                .addUrl("https://github.com/code4craft")
                //開啟5個執行緒抓取
                .thread(5)
                //啟動爬蟲
                .run();
    }
}

1. 配置Site

通過設定Site物件來配置爬蟲的編碼、抓取間隔、超時時間、重試次數等,以及一些模擬的引數,例如User Agent、cookie,和代理的設定等。

方法 說明 示例
setCharset(String) 設定編碼 site.setCharset(“utf-8”)
setUserAgent(String) 設定UserAgent site.setUserAgent(“Spider”)
setTimeOut(int) 設定超時時間,單位是毫秒 site.setTimeOut(3000)
setRetryTimes(int) 設定重試次數 site.setRetryTimes(3)
setCycleRetryTimes(int) 設定迴圈重試次數 site.setCycleRetryTimes(3)
addCookie(String,String) 新增一條cookie site.addCookie(“dotcomt_user”,“code4craft”)
setDomain(String) 設定域名,需設定域名後,addCookie才可生效 site.setDomain(“github.com”)
addHeader(String,String) 新增一條addHeader site.addHeader(“Referer”,“https://github.com”)
setHttpProxy(HttpHost) 設定Http代理 site.setHttpProxy(new HttpHost(“127.0.0.1”,8080))

迴圈重試cycleRetry機制會將下載失敗的url重新放入佇列尾部重試,直到達到重試次數,以保證不因為某些網路原因漏抓頁面。

2. 抽取頁面元素

WebMagic裡主要使用了三種抽取技術:XPath、正則表示式和CSS選擇器。另外,對於JSON格式的內容,可使用JsonPath進行解析。

page.getHtml()返回的是一個Html物件,它實現了Selectable介面。使用Selectable介面,你可以直接完成頁面元素的鏈式抽取,而無需去關心抽取的細節。這個介面的方法可分為兩類:抽取元素API和獲取結果API。

抽取元素API

方法 說明 示例
xpath(String xpath) 使用XPath選擇 html.xpath("//div[@class=‘title’]")
$(String selector) 使用Css選擇器選擇 html.$(“div.title”)
$(String selector,String attr) 使用Css選擇器選擇 html.$(“div.title”,“text”)
css(String selector) 功能同$(),使用Css選擇器選擇 html.css(“div.title”)
links() 選擇所有連結 html.links()
regex(String regex) 使用正則表示式抽取 html.regex("<div>(.*?)")</div>
regex(String regex,int group) 使用正則表示式抽取,並指定捕獲組 html.regex("<div>(.*?)",1)</div>
replace(String regex, String replacement) 替換內容 html.replace("","")

抽取元素API返回的都是一個Selectable介面,意思是說,抽取是支援鏈式呼叫的。

獲取結果API

當鏈式呼叫結束時,我們一般都想要拿到一個字串型別的結果。這時候就需要用到獲取結果的API了。我們知道,一條抽取規則,無論是XPath、CSS選擇器或者正則表示式,總有可能抽取到多條元素。WebMagic對這些進行了統一,你可以通過不同的API獲取到一個或者多個元素。

方法 說明 示例
get() 返回一條String型別的結果 String link= html.links().get()
toString() 功能同get(),返回一條String型別的結果 String link= html.links().toString()
all() 返回所有抽取結果 List links= html.links().all()
match() 是否有匹配結果 if (html.links().match()){ xxx; }

3. 連結的發現

發現後續的連結,是一個爬蟲不可缺少的一部分。

page.addTargetRequests(page.getHtml().links().regex("(https://github\\.com/\\w+/\\w+)").all());

page.addTargetRequests()用於將新連結加入到待抓取的佇列中去。

4. 啟動爬蟲Spider

Spider是爬蟲啟動的入口。在啟動爬蟲之前,我們需要使用一個PageProcessor建立一個Spider物件,然後使用run()進行啟動。同時Spider的其他元件(Downloader、Scheduler、Pipeline)都可以通過set方法來進行設定。

方法 說明 示例
create(PageProcessor) 建立Spider Spider.create(new GithubRepoProcessor())
addUrl(String…) 新增初始的URL spider .addUrl(“http://webmagic.io/docs/”)
addRequest(Request…) 新增初始的Request spider .addRequest(“http://webmagic.io/docs/”)
thread(n) 開啟n個執行緒 spider.thread(5)
run() 啟動,會阻塞當前執行緒執行 spider.run()
start()/runAsync() 非同步啟動,當前執行緒繼續執行 spider.start()
stop() 停止爬蟲 spider.stop()
test(String) 抓取一個頁面進行測試 spider .test(“http://webmagic.io/docs/”)
addPipeline(Pipeline) 新增一個Pipeline,一個Spider可以有多個Pipeline spider .addPipeline(new ConsolePipeline())
setScheduler(Scheduler) 設定Scheduler,一個Spider只能有個一個Scheduler spider.setScheduler(new RedisScheduler())
setDownloader(Downloader) 設定Downloader,一個Spider只能有個一個Downloader spider .setDownloader(new SeleniumDownloader())
get(String) 同步呼叫,並直接取得結果 ResultItems result = spider .get(“http://webmagic.io/docs/”)
getAll(String…) 同步呼叫,並直接取得一堆結果 List results = spider .getAll(“http://webmagic.io/docs/”, “http://webmagic.io/xxx”)

5. 使用Pipeline儲存結果

WebMagic用於儲存結果的元件叫做Pipeline。例如我們通過“控制檯輸出結果”這件事也是通過一個內建的Pipeline完成的,它叫做ConsolePipeline。如果想把結果用Json的格式儲存下來,只需將Pipeline的實現換成"JsonFilePipeline"就可以了。

public static void main(String[] args) {
    Spider.create(new GithubRepoPageProcessor())
            //從"https://github.com/code4craft"開始抓
            .addUrl("https://github.com/code4craft")
            .addPipeline(new JsonFilePipeline("D:\\webmagic\\"))
            //開啟5個執行緒抓取
            .thread(5)
            //啟動爬蟲
            .run();
}

WebMagic中已經提供了將結果輸出到控制檯、儲存到檔案和JSON格式儲存的幾個Pipeline:

說明 備註
ConsolePipeline 輸出結果到控制檯 抽取結果需要實現toString方法
FilePipeline 儲存結果到檔案 抽取結果需要實現toString方法
JsonFilePipeline JSON格式儲存結果到檔案
ConsolePageModelPipeline (註解模式)輸出結果到控制檯
FilePageModelPipeline (註解模式)儲存結果到檔案
JsonFilePageModelPipeline (註解模式)JSON格式儲存結果到檔案 想要持久化的欄位需要有getter方法

6. 處理非HTTP GET請求

採用在Request物件上新增Method和requestBody來實現POST請求。

Request request = new Request("http://xxx/path");
request.setMethod(HttpConstant.Method.POST);
request.setRequestBody(HttpRequestBody.json("{'id':1}","utf-8"));

HttpRequestBody內建了幾種初始化方式,支援最常見的表單提交、json提交等方式。

API 說明
HttpRequestBody.form(Map<string,object> params, String encoding) 使用表單提交的方式
HttpRequestBody.json(String json, String encoding) 使用JSON的方式,json是序列化後的結果
HttpRequestBody.xml(String xml, String encoding) 設定xml的方式,xml是序列化後的結果
HttpRequestBody.custom(byte[] body, String contentType, String encoding) 設定自定義的requestBody

使用Scheduler

Scheduler是WebMagic中進行URL管理的元件。一般來說,Scheduler包括兩個作用:

  1. 對待抓取的URL佇列進行管理。
  2. 對已抓取的URL進行去重。

WebMagic內建了幾個常用的Scheduler。

說明 備註
DuplicateRemovedScheduler 抽象基類,提供一些模板方法 繼承它可以實現自己的功能
QueueScheduler 使用記憶體佇列儲存待抓取URL
PriorityScheduler 使用帶有優先順序的記憶體佇列儲存待抓取URL 耗費記憶體較QueueScheduler更大,但是當設定了request.priority之後,只能使用PriorityScheduler才可使優先順序生效
FileCacheQueueScheduler 使用檔案儲存抓取URL,可以在關閉程式並下次啟動時,從之前抓取到的URL繼續抓取 需指定路徑,會建立.urls.txt和.cursor.txt兩個檔案
RedisScheduler 使用Redis儲存抓取佇列,可進行多臺機器同時合作抓取 需要安裝並啟動redis

使用註解編寫爬蟲

WebMagic支援使用獨有的註解風格編寫一個爬蟲,引入webmagic-extension包即可使用此功能。

在註解模式下,使用一個簡單物件加上註解,可以用極少的程式碼量就完成一個爬蟲的編寫。

註解模式的開發方式是這樣的:

  1. 首先定義你需要抽取的資料,並編寫類。
  2. 在類上寫明@TargetUrl註解,定義對哪些URL進行下載和抽取。
  3. 在類的欄位上加上@ExtractBy註解,定義這個欄位使用什麼方式進行抽取。
  4. 定義結果的儲存方式。

定義Model類來實現爬蟲

定義一個Model類來抽取一個github專案的名稱、作者和簡介三個資訊。

@TargetUrl("https://github.com/\\w+/\\w+")
@HelpUrl("https://github.com/\\w+")
public class GithubRepo {

    @ExtractBy(value = "//h1[@class='entry-title public']/strong/a/text()", notNull = true)
    private String name;

    @ExtractByUrl("https://github\\.com/(\\w+)/.*")
    private String author;

    @ExtractBy("//div[@id='readme']/tidyText()")
    private String readme;

    public static void main(String[] args) {
        OOSpider.create(Site.me().setSleepTime(1000)
                , new ConsolePageModelPipeline(), GithubRepo.class)
                .addUrl("https://github.com/code4craft").thread(5).run();
    }
}

1. 定義抓取的Url

HelpUrl/TargetUrl是一個非常有效的爬蟲開發模式,TargetUrl是我們最終要抓取的URL,最終想要的資料都來自這裡;而HelpUrl則是為了發現這個最終URL,我們需要訪問的頁面。WebMagic定製了適合HelpUrl/TargetUrl的URL的正則表示式,主要有兩點改動:

  • 將URL中常用的字元.預設做了轉義,變成了\.
  • 將"*“替換成了”.*",直接使用可表示萬用字元。

2. 抽取頁面元素

@ExtractBy是一個用於抽取元素的註解,它描述了一種抽取規則。@ExtractBy註解主要作用於欄位,它表示“使用這個抽取規則,將抽取到的結果儲存到這個欄位中”。

@ExtractByUrl是一個單獨的註解,它的意思是“從URL中進行抽取”。它只支援正則表示式作為抽取規則。

3. 爬蟲的建立和啟動

註解模式的入口是OOSpider,它繼承了Spider類,提供了特殊的建立方法,其他的方法是類似的。建立一個註解模式的爬蟲需要一個或者多個Model類,以及一個或者多個PageModelPipeline——定義處理結果的方式。

public static OOSpider create(Site site, PageModelPipeline pageModelPipeline, Class... pageModels);

4. PageModelPipeline

註解模式下,處理結果的類叫做PageModelPipeline,通過實現它,你可以自定義自己的結果處理方式。

public interface PageModelPipeline<T> {
    public void process(T t, Task task);
}

PageModelPipeline與Model類是對應的,多個Model可以對應一個PageModelPipeline。除了建立時,你還可以通過

public OOSpider addPageModel(PageModelPipeline pageModelPipeline, Class... pageModels)

方法,在新增一個Model的同時,可以新增一個PageModelPipeline。

Formatter型別轉換

因為抽取到的內容總是String,而我們想要的內容則可能是其他型別。Formatter可以將抽取到的內容,自動轉換成一些基本型別,而無需手動使用程式碼進行轉換。

自動轉換支援所有基本型別和裝箱型別。另外,還支援java.util.Date型別的轉換。但是在轉換時,需要指定Date的格式。格式按照JDK的標準來定義。

@Formatter("yyyy-MM-dd HH:mm")
@ExtractBy("//div[@class='BlogStat']/regex('\\d+-\\d+-\\d+\\s+\\d+:\\d+')")
private Date date;

顯式指定轉換型別

一般情況下,Formatter會根據欄位型別進行轉換,但是特殊情況下,我們會需要手動指定型別。這主要發生在欄位是List型別的時候。

@Formatter(value = "",subClazz = Integer.class)
@ExtractBy(value = "//div[@class='id']/text()", multi = true)
private List<Integer> ids;

自定義Formatter(TODO)

實際上,除了自動型別轉換之外,Formatter還可以做一些結果的後處理的事情。例如,我們有一種需求場景,需要將抽取的結果作為結果的一部分,拼接上一部分字串來使用。在這裡,我們定義了一個StringTemplateFormatter

public class StringTemplateFormatter implements ObjectFormatter<String> {

    private String template;

    @Override
    public String format(String raw) throws Exception {
        return String.format(template, raw);
    }

    @Override
    public Class<String> clazz() {
        return String.class;
    }

    @Override
    public void initParam(String[] extra) {
        template = extra[0];
    }
}

那麼,我們就能在抽取之後,做一些簡單的操作了!

@Formatter(value = "author is %s",formatter = StringTemplateFormatter.class)
@ExtractByUrl("https://github\\.com/(\\w+)/.*")
private String author;

AfterExtractor

有的時候,註解模式無法滿足所有需求,我們可能還需要寫程式碼完成一些事情,這個時候就要用到AfterExtractor介面了。

public interface AfterExtractor {
    public void afterProcess(Page page);
}

afterProcess方法會在抽取結束,欄位都初始化完畢之後被呼叫,可以處理一些特殊的邏輯。

//TargetUrl的意思是隻有以下格式的URL才會被抽取出生成model物件

//這裡對正則做了一點改動,'.'預設是不需要轉義的,而'*'則會自動被替換成'.*',因為這樣描述URL看著舒服一點...

//繼承jfinal中的Model

//實現AfterExtractor介面可以在填充屬性後進行其他操作

@TargetUrl("http://my.oschina.net/flashsword/blog/*")
public class OschinaBlog extends Model<OschinaBlog> implements AfterExtractor {

    //用ExtractBy註解的欄位會被自動抽取並填充
    //預設是xpath語法
    @ExtractBy("//title")
    private String title;

    //可以定義抽取語法為Css、Regex等
    @ExtractBy(value = "div.BlogContent", type = ExtractBy.Type.Css)
    private String content;

    //multi標註的抽取結果可以是一個List
    @ExtractBy(value = "//div[@class='BlogTags']/a/text()", multi = true)
    private List<String> tags;

    @Override
    public void afterProcess(Page page) {
        //jfinal的屬性其實是一個Map而不是欄位,沒關係,填充進去就是了
        this.set("title", title);
        this.set("content", content);
        this.set("tags", StringUtils.join(tags, ","));
        //儲存
        save();
    }

    public static void main(String[] args) {
        C3p0Plugin c3p0Plugin = new C3p0Plugin("jdbc:mysql://127.0.0.1/blog?characterEncoding=utf-8", "blog", "password");
        c3p0Plugin.start();
        ActiveRecordPlugin activeRecordPlugin = new ActiveRecordPlugin(c3p0Plugin);
        activeRecordPlugin.addMapping("blog", OschinaBlog.class);
        activeRecordPlugin.start();
        //啟動webmagic
        OOSpider.create(Site.me().addStartUrl("http://my.oschina.net/flashsword/blog/145796"), OschinaBlog.class).run();
    }
}