以前就有網友在博客裏留言,以爲webmagic的實現比較有意思,想要藉此研究一下爬蟲。最近終於集中精力,花了三天時間,終於寫完了這篇文章。以前垂直爬蟲寫了一年多,webmagic框架寫了一個多月,這方面卻是有一些心得,但願對讀者有幫助。css
通常來講,一個爬蟲包括幾個部分:html
頁面下載node
頁面下載是一個爬蟲的基礎。下載頁面以後才能進行其餘後續操做。python
連接提取mysql
通常爬蟲都會有一些初始的種子URL,可是這些URL對於爬蟲是遠遠不夠的。爬蟲在爬頁面的時候,須要不斷髮現新的連接。jquery
URL管理nginx
最基礎的URL管理,就是對已經爬過的URL和沒有爬的URL作區分,防止重複爬取。git
內容分析和持久化github
通常來講,咱們最終須要的都不是原始的HTML頁面。咱們須要對爬到的頁面進行分析,轉化成結構化的數據,並存儲下來。web
不一樣的爬蟲,對這幾部分的要求是不同的。
對於通用型的爬蟲,例如搜索引擎蜘蛛,須要指對互聯網大部分網頁無差異進行抓取。這時候難點就在於頁面下載和連接管理上–若是要高效的抓取更多頁面,就必須進行更快的下載;同時隨着連接數量的增多,須要考慮若是對大規模的連接進行去重和調度,就成了一個很大的問題。通常這些問題都會在大公司有專門的團隊去解決,好比這裏有一篇來自淘寶的快速構建實時抓取集羣。對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-core中,webmagic-samples裏有一些定製爬蟲的例子,能夠做爲參考。而webmagic-plugin目前還不完善,後期準備加入一些經常使用的功能。下面主要介紹webmagic-core的內容。
前面說到,webmagic參考了scrapy的模塊劃分,分爲Spider(整個爬蟲的調度框架)、Downloader(頁面下載)、PageProcessor(連接提取和頁面分析)、Scheduler(URL管理)、Pipeline(離線分析和持久化)幾部分。只不過scrapy經過middleware實現擴展,而webmagic則經過定義這幾個接口,並將其不一樣的實現注入主框架類Spider來實現擴展。
Spider是爬蟲的入口類,Spider的接口調用採用了鏈式的API設計,其餘功能所有經過接口注入Spider實現,下面是啓動一個比較複雜的Spider的例子。
Spider.create(sinaBlogProcessor) .scheduler(new FileCacheQueueScheduler("/data/temp/webmagic/cache/")) .pipeline(new FilePipeline()) .thread(10).run();
Spider的核心處理流程很是簡單,代碼以下:
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()); }
頁面下載是一切爬蟲的開始。
大部分爬蟲都是經過模擬http請求,接收並分析響應來完成。這方面,JDK自帶的HttpURLConnection能夠知足最簡單的須要,而Apache HttpClient(4.0後整合到HttpCompenent項目中)則是開發複雜爬蟲的不二之選。它支持自定義HTTP頭(對於爬蟲比較有用的就是User-agent、cookie等)、自動redirect、鏈接複用、cookie保留、設置代理等諸多強大的功能。
webmagic使用了HttpClient 4.2,並封裝到了HttpClientDownloader。學習HttpClient的使用對於構建高性能爬蟲是很是有幫助的,官方的Tutorial就是很好的學習資料。目前webmagic對HttpClient的使用仍在初步階段,不過對於通常抓取任務,已經夠用了。
下面是一個使用HttpClient最簡單的例子:
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。
這裏說的頁面分析主要指HTML頁面的分析。頁面分析能夠說是垂直爬蟲最複雜的一部分,在webmagic裏,PageProcessor是定製爬蟲的核心。經過編寫一個實現PageProcessor接口的類,就能夠定製一個本身的爬蟲。
頁面抽取最基本的方式是使用正則表達式。正則表達式好處是很是通用,解析文本的功能也很強大。可是正則表達式最大的問題是,不能真正對HTML進行語法級別的解析,沒有辦法處理關係到HTML結構的狀況(例如處理標籤嵌套)。例如,我想要抽取一個
「做爲終止符截取。爲了解決這個問題,咱們就須要進行HTML的分析。
HTML分析是一個比較複雜的工做,Java世界主要有幾款比較方便的分析工具:
Jsoup是一個集強大和便利於一體的HTML解析工具。它方便的地方是,能夠用於支持用jquery中css selector的方式選取元素,這對於熟悉js的開發者來講基本沒有學習成本。
String content = "blabla"; Document doc = JSoup.parse(content); Elements links = doc.select("a[href]");
Jsoup還支持白名單過濾機制,對於網站防止XSS攻擊也是很好的。
HtmlParser的功能比較完備,也挺靈活,但談不上方便。這個項目好久沒有維護了,最新版本是2.1。HtmlParser的核心元素是Node,對應一個HTML標籤,支持getChildren()等樹狀遍歷方式。HtmlParser另一個核心元素是NodeFilter,經過實現NodeFilter接口,能夠對頁面元素進行篩選。這裏有一篇HtmlParser的使用文章:使用 HttpClient 和 HtmlParser 實現簡易爬蟲。
tika是專爲抽取而生的工具,還支持PDF、Zip甚至是Java Class。使用tika分析HTML,須要本身定義一個抽取內容的Handler並繼承org.xml.sax.helpers.DefaultHandler
,解析方式就是xml標準的方式。crawler4j中就使用了tika做爲解析工具。SAX這種流式的解析方式對於分析大文件頗有用,我我的卻是認爲對於解析html意義不是很大。
InputStream inputStream = null; HtmlParser htmlParser = new HtmlParser(); htmlParser.parse(new ByteArrayInputStream(page.getContentData()), contentHandler, metadata, new ParseContext());
HtmlCleaner最大的優勢是:支持XPath的方式選取元素。XPath是一門在XML中查找信息的語言,也能夠用於抽取HTML元素。XPath與CSS Selector大部分功能都是重合的,可是CSS Selector專門針對HTML,寫法更簡潔,而XPath則是通用的標準,能夠精確到屬性值。XPath有必定的學習成本,可是對常常須要編寫爬蟲的人來講,這點投入絕對是值得的。
學習XPath能夠參考w3school的XPath 教程。下面是使用HtmlCleaner和xpath進行抽取的一段代碼:
HtmlCleaner htmlCleaner = new HtmlCleaner(); TagNode tagNode = htmlCleaner.clean(text); Object[] objects = tagNode.evaluateXPath("xpathStr");
在這裏評價這些工具的主要標準是「方便」。就拿抽取頁面全部連接這一基本任務來講,幾種代碼分別以下:
XPath:
tagNode.evaluateXPath("//a/@href")
CSS Selector:
//使用相似js的實現 $("a[href]").attr("href")
HtmlParser:
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則須要手動寫程序去處理標籤了,比較麻煩。
Selector是webmagic爲了簡化頁面抽取開發的獨立模塊,是整個項目中我最得意的部分。這裏整合了CSS Selector、XPath和正則表達式,並能夠進行鏈式的抽取,很容易就實現強大的功能。即便你使用本身開發的爬蟲工具,webmagic的Selector仍然值得一試。
例如,我已經下載了一個頁面,如今要抽取某個區域的全部包含"blog"的連接,我能夠這樣寫:
//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博客時有不錯的效果。
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其實也是容易被忽略的一部分。你們都知道持久化的重要性,可是不少框架都選擇直接在頁面抽取的時候將持久化一塊兒完成,例如crawer4j。可是Pipeline真正的好處是,將頁面的在線分析和離線處理拆分開來,能夠在一些線程裏進行下載,另外一些線程裏進行處理和持久化。
你能夠擴展Pipeline來實現抽取結果的持久化,將其保存到你想要保存的地方-本地文件、數據庫、mongodb等等。Pipeline的處理目前仍是在線的,可是修改成離線的也並不困難。
webmagic目前只支持控制檯輸出和文件持久化,可是持久化到數據庫也是很容易的。
webmagic確實是一個山寨的框架,自己也沒有太多創新的東西,可是確實對Java爬蟲的實現有了一些簡化。在強大便利的功能和較高的靈活性中間,webmagic選擇了後者,目標就是要打造一個熟練的Java開發者也用的比較順手的工具,而且能夠集成到本身的業務系統中,這一點我本身開發了很多這樣的業務,對其靈活性仍是比較有信心的。webmagic目前的代碼實現還比較簡單(不到2000行),若是有興趣的閱讀代碼可能也會有一些收穫,也很是歡迎建議和指正。
最後再次附上代碼地址:https://github.com/code4craft/webmagic