(10)Java爬蟲框架webmagic學習筆記
Java爬蟲框架webmagic學習筆記
參考自:webmagic文件
webmagic簡介
webmagic的github網址:https://github.com/code4craft/webmagic
使用webmagic的原因:
- webmagic是一個無須配置、便於二次開發的爬蟲框架,它提供簡單靈活的API,只需少量程式碼即可實現一個爬蟲。
- webmagic採用完全模組化的設計,功能覆蓋整個爬蟲的生命週期(連結提取、頁面下載、內容抽取、持久化),支援多執行緒抓取,分散式抓取,url自動去重,並支援自動重試、自定義UA/cookie等功能。
- webmagic包含強大的頁面抽取功能,開發者可以便捷的使用css selector、xpath和正則表示式進行連結和內容的提取,支援多個選擇器鏈式呼叫。
- webmagic也可以很方便的作為一個模組,嵌入Java專案中執行。
- 文件相對齊全。
webmagic的主要特色:
- 完全模組化的設計,強大的可擴充套件性。
- 核心簡單但是涵蓋爬蟲的全部流程,靈活而強大,也是學習爬蟲入門的好材料。
- 提供豐富的抽取頁面API。
- 無配置,但是可通過POJO+註解形式實現一個爬蟲。
- 支援多執行緒。
- 支援分散式。
- 支援爬取js動態渲染的頁面。
- 無框架依賴,可以靈活的嵌入到專案中去。
總體架構
WebMagic的結構分為Downloader
、PageProcessor
、Scheduler
Pipeline
四大元件,並由Spider將它們彼此組織起來,讓它們可以互相互動,流程化地執行。這四大元件對應爬蟲生命週期中的下載、處理、管理和持久化等功能。
WebMagic總體架構圖如下:
WebMagic的四個元件
1. Downloader
Downloader負責從網際網路上下載頁面,以便後續處理。WebMagic預設使用了Apache HttpClient作為下載工具。
2. PageProcessor
PageProcessor負責解析頁面,抽取有用資訊,以及發現新的連結。WebMagic使用Jsoup作為HTML解析工具,並基於其開發瞭解析XPath的工具
在這四個元件中,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包括兩個作用:
- 對待抓取的URL佇列進行管理。
- 對已抓取的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包即可使用此功能。
在註解模式下,使用一個簡單物件加上註解,可以用極少的程式碼量就完成一個爬蟲的編寫。
註解模式的開發方式是這樣的:
- 首先定義你需要抽取的資料,並編寫類。
- 在類上寫明
@TargetUrl
註解,定義對哪些URL進行下載和抽取。 - 在類的欄位上加上
@ExtractBy
註解,定義這個欄位使用什麼方式進行抽取。 - 定義結果的儲存方式。
定義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();
}
}