之前就有網友在博客里留言,覺得webmagic的實現比較有意思,想要借此研究一下爬蟲。最近終於集中精力,花了三天時間,終於寫完了這篇文章。之前垂直爬蟲寫了一年多,webmagic框架寫了一個多月,這方面倒是有一些心得,希望對讀者有幫助。
webmagic的目標
一般來說,一個爬蟲包括幾個部分:
-
頁面下載
頁面下載是一個爬蟲的基礎。下載頁面之后才能進行其他后續操作。
-
鏈接提取
一般爬蟲都會有一些初始的種子URL,但是這些URL對於爬蟲是遠遠不夠的。爬蟲在爬頁面的時候,需要不斷發現新的鏈接。
-
URL管理
最基礎的URL管理,就是對已經爬過的URL和沒有爬的URL做區分,防止重復爬取。
-
內容分析和持久化
一般來說,我們最終需要的都不是原始的HTML頁面。我們需要對爬到的頁面進行分析,轉化成結構化的數據,並存儲下來。
不同的爬蟲,對這幾部分的要求是不一樣的。
對於通用型的爬蟲,例如搜索引擎蜘蛛,需要指對互聯網大部分網頁無差別進行抓取。這時候難點就在於頁面下載和鏈接管理上–如果要高效的抓取更多頁面,就必須進行更快的下載;同時隨着鏈接數量的增多,需要考慮如果對大規模的鏈接進行去重和調度,就成了一個很大的問題。一般這些問題都會在大公司有專門的團隊去解決,比如這里有一篇來自淘寶的快速構建實時抓取集群。對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來實現擴展。
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()); }
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最簡單的例子:
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結構的情況(例如處理標簽嵌套)。例如,我想要抽取一個
“作為終止符截取。為了解決這個問題,我們就需要進行HTML的分析。
HTML分析是一個比較復雜的工作,Java世界主要有幾款比較方便的分析工具:
Jsoup
Jsoup是一個集強大和便利於一體的HTML解析工具。它方便的地方是,可以用於支持用jquery中css selector的方式選取元素,這對於熟悉js的開發者來說基本沒有學習成本。
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意義不是很大。
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進行抽取的一段代碼:
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則需要手動寫程序去處理標簽了,比較麻煩。
webmagic的Selector
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博客時有不錯的效果。
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


