webmagic的設計機制及原理-如何開發一個Java爬蟲

image

此文章是webmagic 0.1.0版的設計手冊,後續版本的入門及用戶手冊請看這裏:https://github.com/code4craft/webmagic/blob/master/user-manual.mdcss

以前就有網友在博客裏留言,以爲webmagic的實現比較有意思,想要藉此研究一下爬蟲。最近終於集中精力,花了三天時間,終於寫完了這篇文章。以前垂直爬蟲寫了一年多,webmagic框架寫了一個多月,這方面卻是有一些心得,但願對讀者有幫助。html

webmagic的目標

通常來講,一個爬蟲包括幾個部分:java

  • 頁面下載node

    頁面下載是一個爬蟲的基礎。下載頁面以後才能進行其餘後續操做。python

  • 連接提取mysql

    通常爬蟲都會有一些初始的種子URL,可是這些URL對於爬蟲是遠遠不夠的。爬蟲在爬頁面的時候,須要不斷髮現新的連接。jquery

  • URL管理git

    最基礎的URL管理,就是對已經爬過的URL和沒有爬的URL作區分,防止重複爬取。github

  • 內容分析和持久化web

    通常來講,咱們最終須要的都不是原始的HTML頁面。咱們須要對爬到的頁面進行分析,轉化成結構化的數據,並存儲下來。

不一樣的爬蟲,對這幾部分的要求是不同的。

<!--more-->

對於通用型的爬蟲,例如搜索引擎蜘蛛,須要指對互聯網大部分網頁無差異進行抓取。這時候難點就在於頁面下載和連接管理上--若是要高效的抓取更多頁面,就必須進行更快的下載;同時隨着連接數量的增多,須要考慮若是對大規模的連接進行去重和調度,就成了一個很大的問題。通常這些問題都會在大公司有專門的團隊去解決,好比這裏有一篇來自淘寶的快速構建實時抓取集羣。對Java來講,若是你要研究通用爬蟲,那麼能夠看一下heritrix或者nutch

而垂直類型的爬蟲要解決的問題則不同,好比想要爬取一些網站的新聞、博客信息,通常抓取數量要求不是很大,難點則在於如何高效的定製一個爬蟲,能夠精確的抽取出網頁的內容,並保存成結構化的數據。這方面需求不少,webmagic就是爲了解決這個目的而開發的。

使用Java語言開發爬蟲是比較複雜的。雖然Java有很強大的頁面下載、HTML分析工具,可是每一個都有不小的學習成本,並且這些工具自己都不是專門爲爬蟲而生,使用起來也沒有那麼順手。我曾經有一年的時間都在開發爬蟲,重複的開發讓人頭痛。Java還有一個比較成熟的框架crawler4j,可是它是爲通用爬蟲而設計的,擴展性差一些,知足不了個人業務須要。我也有過本身開發框架的念頭,可是終歸以爲抽象的不是很好。直到發現python的爬蟲框架scrapy,它將爬蟲的生命週期拆分的很是清晰,我參照它進行了模塊劃分,並用Java的方式去實現了它,因而就有了webmagic。

代碼已經託管到github,地址是https://github.com/code4craft/webmagic,Javadoc:http://code4craft.github.io/webmagic/docs/

webmagic的實現還參考了另外一個Java爬蟲SpiderMan。SpiderMan是一個全棧式的Java爬蟲,它的設計思想跟webmagic稍有不一樣,它但願將Java語言的實現隔離,僅僅讓用戶經過配置就完成一個垂直爬蟲。理論上,SpiderMan功能更強大,不少功能已經內置,而webmagic則比較靈活,適合熟悉Java語法的開發者,能夠比較很是方便的進行擴展和二次開發。


webmagic的模塊劃分

webmagic目前的核心代碼都在webmagic-core中,webmagic-samples裏有一些定製爬蟲的例子,能夠做爲參考。而webmagic-plugin目前還不完善,後期準備加入一些經常使用的功能。下面主要介紹webmagic-core的內容。

前面說到,webmagic參考了scrapy的模塊劃分,分爲Spider(整個爬蟲的調度框架)、Downloader(頁面下載)、PageProcessor(連接提取和頁面分析)、Scheduler(URL管理)、Pipeline(離線分析和持久化)幾部分。只不過scrapy經過middleware實現擴展,而webmagic則經過定義這幾個接口,並將其不一樣的實現注入主框架類Spider來實現擴展。

image

Spider類-核心調度

Spider是爬蟲的入口類,Spider的接口調用採用了鏈式的API設計,其餘功能所有經過接口注入Spider實現,下面是啓動一個比較複雜的Spider的例子。

<!-- lang: java -->
Spider.create(sinaBlogProcessor)
.scheduler(new FileCacheQueueScheduler("/data/temp/webmagic/cache/"))
.pipeline(new FilePipeline())
.thread(10).run();

Spider的核心處理流程很是簡單,代碼以下:

<!-- lang: java -->
private void processRequest(Request request) {
    Page page = downloader.download(request, this);
    if (page == null) {
        sleep(site.getSleepTime());
        return;
    }
    pageProcessor.process(page);
    addRequest(page);
    for (Pipeline pipeline : pipelines) {
        pipeline.process(page, this);
    }
    sleep(site.getSleepTime());
}

Downloader-頁面下載

頁面下載是一切爬蟲的開始。

大部分爬蟲都是經過模擬http請求,接收並分析響應來完成。這方面,JDK自帶的HttpURLConnection能夠知足最簡單的須要,而Apache HttpClient(4.0後整合到HttpCompenent項目中)則是開發複雜爬蟲的不二之選。它支持自定義HTTP頭(對於爬蟲比較有用的就是User-agent、cookie等)、自動redirect、鏈接複用、cookie保留、設置代理等諸多強大的功能。

webmagic使用了HttpClient 4.2,並封裝到了HttpClientDownloader。學習HttpClient的使用對於構建高性能爬蟲是很是有幫助的,官方的Tutorial就是很好的學習資料。目前webmagic對HttpClient的使用仍在初步階段,不過對於通常抓取任務,已經夠用了。

下面是一個使用HttpClient最簡單的例子:

<!-- lang: java -->
HttpClient httpClient = new DefaultHttpClient();
HttpGet httpGet = new HttpGet("http://youhost/xxx");
HttpResponse httpResponse = httpClient.execute(httpGet);
System.out.println(EntityUtils.toString(httpResponse.getEntity().getContent()));

對於一些Javascript動態加載的網頁,僅僅使用http模擬下載工具,並不能取到頁面的內容。這方面的思路有兩種:一種是抽絲剝繭,分析js的邏輯,再用爬蟲去重現它(好比在網頁中提取關鍵數據,再用這些數據去構造Ajax請求,最後直接從響應體獲取想要的數據); 另外一種就是:內置一個瀏覽器,直接獲取最後加載完的頁面。這方面,js可使用PhantomJS,它內部集成了webkit。而Java可使用Selenium,這是一個很是強大的瀏覽器模擬工具。考慮之後將它整理成一個獨立的Downloader,集成到webmagic中去。

通常沒有必要去擴展Downloader。

PageProcessor-頁面分析及連接抽取

這裏說的頁面分析主要指HTML頁面的分析。頁面分析能夠說是垂直爬蟲最複雜的一部分,在webmagic裏,PageProcessor是定製爬蟲的核心。經過編寫一個實現PageProcessor接口的類,就能夠定製一個本身的爬蟲。

頁面抽取最基本的方式是使用正則表達式。正則表達式好處是很是通用,解析文本的功能也很強大。可是正則表達式最大的問題是,不能真正對HTML進行語法級別的解析,沒有辦法處理關係到HTML結構的狀況(例如處理標籤嵌套)。例如,我想要抽取一個<div>裏的內容,能夠這樣寫:"<div>(.*?)</div>"。可是若是這個div內部還包含幾個子div,這個時候使用正則表達式就會將子div的"</div>"做爲終止符截取。爲了解決這個問題,咱們就須要進行HTML的分析。

HTML分析是一個比較複雜的工做,Java世界主要有幾款比較方便的分析工具:

####Jsoup

Jsoup是一個集強大和便利於一體的HTML解析工具。它方便的地方是,能夠用於支持用jquery中css selector的方式選取元素,這對於熟悉js的開發者來講基本沒有學習成本。

<!-- lang: java -->
String content = "blabla";
Document doc = JSoup.parse(content);
Elements links = doc.select("a[href]");

Jsoup還支持白名單過濾機制,對於網站防止XSS攻擊也是很好的。

####HtmlParser

HtmlParser的功能比較完備,也挺靈活,但談不上方便。這個項目好久沒有維護了,最新版本是2.1。HtmlParser的核心元素是Node,對應一個HTML標籤,支持getChildren()等樹狀遍歷方式。HtmlParser另一個核心元素是NodeFilter,經過實現NodeFilter接口,能夠對頁面元素進行篩選。這裏有一篇HtmlParser的使用文章:使用 HttpClient 和 HtmlParser 實現簡易爬蟲

####Apache tika

tika是專爲抽取而生的工具,還支持PDF、Zip甚至是Java Class。使用tika分析HTML,須要本身定義一個抽取內容的Handler並繼承org.xml.sax.helpers.DefaultHandler,解析方式就是xml標準的方式。crawler4j中就使用了tika做爲解析工具。SAX這種流式的解析方式對於分析大文件頗有用,我我的卻是認爲對於解析html意義不是很大。

<!-- lang: java -->
InputStream inputStream = null;
HtmlParser htmlParser = new HtmlParser();
htmlParser.parse(new ByteArrayInputStream(page.getContentData()), 
contentHandler, metadata, new ParseContext());

####HtmlCleaner與XPath

HtmlCleaner最大的優勢是:支持XPath的方式選取元素。XPath是一門在XML中查找信息的語言,也能夠用於抽取HTML元素。XPath與CSS Selector大部分功能都是重合的,可是CSS Selector專門針對HTML,寫法更簡潔,而XPath則是通用的標準,能夠精確到屬性值。XPath有必定的學習成本,可是對常常須要編寫爬蟲的人來講,這點投入絕對是值得的。

學習XPath能夠參考w3school的XPath 教程。下面是使用HtmlCleaner和xpath進行抽取的一段代碼:

<!-- lang: java -->
HtmlCleaner htmlCleaner = new HtmlCleaner();
TagNode tagNode = htmlCleaner.clean(text);
Object[] objects = tagNode.evaluateXPath("xpathStr");

幾個工具的對比

在這裏評價這些工具的主要標準是「方便」。就拿抽取頁面全部連接這一基本任務來講,幾種代碼分別以下:

XPath:

<!-- lang: java -->
tagNode.evaluateXPath("//a/@href")

CSS Selector:

<!-- lang: java -->
//使用相似js的實現
$("a[href]").attr("href")

HtmlParser:

<!-- lang: java -->
Parser p = new Parser(value);
NodeFilter aFilter = new TagNameFilter("a");
NodeList nodes = p.extractAllNodesThatMatch(aFilter);
for (int i = 0; i < nodes.size(); i++) {
    Node eachNode = nodes.elementAt(i);
    if (eachNode instanceof LinkTag) {
        LinkTag linkTag = (LinkTag) eachNode;
        System.out.println(linkTag.extractLink());
    }
}

XPath是最簡單的,能夠精確選取到href屬性值;而CSS Selector則次之,能夠選取到HTML標籤,屬性值須要調用函數去獲取;而HtmlParser和SAX則須要手動寫程序去處理標籤了,比較麻煩。

webmagic的Selector

Selector是webmagic爲了簡化頁面抽取開發的獨立模塊,是整個項目中我最得意的部分。這裏整合了CSS Selector、XPath和正則表達式,並能夠進行鏈式的抽取,很容易就實現強大的功能。即便你使用本身開發的爬蟲工具,webmagic的Selector仍然值得一試。

例如,我已經下載了一個頁面,如今要抽取某個區域的全部包含"blog"的連接,我能夠這樣寫:

<!-- lang: java -->
//content是用別的爬蟲工具抽取到的正文
String content = "blabla";
List<String> links = Html.create(content)
.$("div.title")  //css 選擇,Java裏雖然不多有$符號出現,不過貌似$做爲方法名是合法的
.xpath("//@href")  //提取連接
.regex(".*blog.*") //正則匹配過濾
.all(); //轉換爲string列表

另外,webmagic的抓取連接須要顯示的調用Page.addTargetRequests()去添加,這也是爲了靈活性考慮的(不少時候,下一步的URL不是單純的頁面href連接,可能會根據頁面模塊進行抽取,甚至多是本身拼湊出來的)。

補充一個有意思的話題,就是對於頁面正文的自動抽取。相信用過Evernote Clearly都會對其自動抽取正文的技術印象深入。這個技術又叫Readability,webmagic對readability有一個粗略的實現SmartContentSelector,用的是P標籤密度計算的方法,在測試oschina博客時有不錯的效果。

Scheduler-URL管理

URL管理的問題可大可小。對於小規模的抓取,URL管理是很簡單的。咱們只須要將待抓取URL和已抓取URL分開保存,並進行去重便可。使用JDK內置的集合類型Set、List或者Queue均可以知足須要。若是咱們要進行多線程抓取,則能夠選擇線程安全的容器,例如LinkedBlockingQueue以及ConcurrentHashMap。

由於小規模的URL管理很是簡單,不少框架都並不將其抽象爲一個模塊,而是直接融入到代碼中。可是實際上,抽象出Scheduler模塊,會使得框架的解耦程度上升一個檔次,並不是常容易進行橫向擴展,這也是我從scrapy中學到的。

在webmagic的設計中,除了Scheduler模塊,其餘的處理-從下載、解析到持久化,每一個任務都是互相獨立的,所以能夠經過多個Spider共用一個Scheduler來進行擴展。排除去重的因素,URL管理天生就是一個隊列,咱們能夠很方便的用分佈式的隊列工具去擴展它,也能夠基於mysql、redis或者mongodb這樣的存儲工具來構造一個隊列,這樣構建一個多線程乃至分佈式的爬蟲就垂手可得了。

URL去重也是一個比較複雜的問題。若是數據量較少,則使用hash的方式就能很好解決。數據量較大的狀況下,可使用Bloom Filter或者更復雜的方式。

webmagic目前有兩個Scheduler的實現,QueueScheduler是一個簡單的內存隊列,速度較快,而且是線程安全的,FileCacheQueueScheduler則是一個文件隊列,它能夠用於耗時較長的下載任務,在任務中途中止後,下次執行仍然從停止的URL開始繼續爬取。

Pipeline-離線處理和持久化

Pipeline其實也是容易被忽略的一部分。你們都知道持久化的重要性,可是不少框架都選擇直接在頁面抽取的時候將持久化一塊兒完成,例如crawer4j。可是Pipeline真正的好處是,將頁面的在線分析和離線處理拆分開來,能夠在一些線程裏進行下載,另外一些線程裏進行處理和持久化。

你能夠擴展Pipeline來實現抽取結果的持久化,將其保存到你想要保存的地方-本地文件、數據庫、mongodb等等。Pipeline的處理目前仍是在線的,可是修改成離線的也並不困難。

webmagic目前只支持控制檯輸出和文件持久化,可是持久化到數據庫也是很容易的。

結語

webmagic確實是一個山寨的框架,自己也沒有太多創新的東西,可是確實對Java爬蟲的實現有了一些簡化。在強大便利的功能和較高的靈活性中間,webmagic選擇了後者,目標就是要打造一個熟練的Java開發者也用的比較順手的工具,而且能夠集成到本身的業務系統中,這一點我本身開發了很多這樣的業務,對其靈活性仍是比較有信心的。webmagic目前的代碼實現還比較簡單(不到2000行),若是有興趣的閱讀代碼可能也會有一些收穫,也很是歡迎建議和指正。

最後再次附上代碼地址:https://github.com/code4craft/webmagic

相關文章
相關標籤/搜索