一、介紹
webmagic的是一個無須配置、便于二次開發的爬蟲架構,它提供簡單靈活的API,隻需少量代碼即可實作一個爬蟲。webmagic采用完全子產品化的設計,功能覆寫整個爬蟲的生命周期(連結提取、頁面下載下傳、内容抽取、持久化),支援多線程抓取,分布式抓取,并支援自動重試、自定義UA/cookie等功能。
二、概覽
WebMagic項目代碼分為核心和擴充兩部分。核心部分(webmagic-core)是一個精簡的、子產品化的爬蟲實作,而擴充部分則包括一些便利的、實用性的功能(例如注解模式編寫爬蟲等)。
WebMagic的結構分為Downloader、PageProcessor、Scheduler、Pipeline四大元件,并由Spider将它們彼此組織起來。這四大元件對應爬蟲生命周期中的下載下傳、處理、管理和持久化等功能。而Spider則将這幾個元件組織起來,讓它們可以互互相動,流程化的執行,可以認為Spider是一個大的容器,它也是WebMagic邏輯的核心。
WebMagic總體架構圖如下:
2.1 WebMagic的四個元件
-
Downloader
Downloader負責從網際網路上下載下傳頁面,以便後續處理。WebMagic預設使用了Apache HttpClient作為下載下傳工具。
-
PageProcessor
PageProcessor負責解析頁面,抽取有用資訊,以及發現新的連結。WebMagic使用Jsoup作為HTML解析工具,并基于其開發了解析XPath的工具Xsoup。在這四個元件中,PageProcessor對于每個站點每個頁面都不一樣,是需要使用者定制的部分。
-
Scheduler
Scheduler負責管理待抓取的URL,以及一些去重的工作。WebMagic預設提供了JDK的記憶體隊列來管理URL,并用集合來進行去重。也支援使用Redis進行分布式管理。除非項目有一些特殊的分布式需求,否則無需自己定制Scheduler。
-
Pipeline
Pipeline負責抽取結果的處理,包括計算、持久化到檔案、資料庫等。WebMagic預設提供了“輸出到控制台”和“儲存到檔案”兩種結果處理方案。Pipeline定義了結果儲存的方式,如果你要儲存到指定資料庫,則需要編寫對應的Pipeline。對于一類需求一般隻需編寫一個Pipeline。
2.2 用于資料流轉的對象
-
Request
Request是對URL位址的一層封裝,一個Request對應一個URL位址。它是PageProcessor與Downloader互動的載體,也是PageProcessor控制Downloader唯一方式。
-
Page
Page代表了從Downloader下載下傳到的一個頁面——可能是HTML,也可能是JSON或者其他文本格式的内容。Page是WebMagic抽取過程的核心對象,它提供一些方法可供抽取、結果儲存等。
-
ReusltItems
ReusltItems相當于一個Map,它儲存PageProcessor處理的結果,供Pipeline使用。它的API與Map很類似,值得注意的是它有一個字段skip,若設定為true,則不應被Pipeline處理。
2.3 控制爬蟲運轉的引擎—Spider
Spider是WebMagic内部流程的核心。Downloader、PageProcessor、Scheduler、Pipeline都是Spider的一個屬性,這些屬性是可以自由設定的,通過設定這個屬性可以實作不同的功能。Spider也是WebMagic操作的入口,它封裝了爬蟲的建立、啟動、停止、多線程等功能。
對于編寫一個爬蟲,PageProcessor是需要編寫的部分,而Spider則是建立和控制爬蟲的入口。
2.4 WebMagic項目組成
WebMagic項目代碼包括幾個部分,在根目錄下以不同目錄名分開。它們都是獨立的Maven項目。
WebMagic主要包括兩個包,這兩個包經過廣泛實用,已經比較成熟:
-
webmagic-core
webmagic-core是WebMagic核心部分,隻包含爬蟲基本子產品和基本抽取器。
-
webmagic-extension
webmagic-extension是WebMagic的主要擴充子產品,提供一些更友善的編寫爬蟲的工具。包括注解格式定義爬蟲、JSON、分布式等支援。
三、 基本的爬蟲
3.1 爬蟲的流程 (可以參考上邊的架構架構圖)
- Downloader-頁面下載下傳
-
頁面下載下傳是一切爬蟲的開始。
大部分爬蟲都是通過模拟http請求,接收并分析響應來完成。這方面,JDK自帶的HttpURLConnection可以滿足最簡單的需要,而Apache HttpClient(4.0後整合到HttpCompenent項目中)則是開發複雜爬蟲的不二之選。它支援自定義HTTP頭(對于爬蟲比較有用的就是User-agent、cookie等)、自動redirect、連接配接複用、cookie保留、設定代理等諸多強大的功能。
webmagic使用了HttpClient 4.2,并封裝到了HttpClientDownloader。學習HttpClient的使用對于建構高性能爬蟲是非常有幫助的,官方的Tutorial就是很好的學習資料。目前webmagic對HttpClient的使用仍在初步階段,不過對于一般抓取任務,已經夠用了
-
- PageProcessor-頁面分析及連結抽取
- 這裡說的頁面分析主要指HTML頁面的分析。頁面分析可以說是垂直爬蟲最複雜的一部分,在webmagic裡,PageProcessor是定制爬蟲的核心。通過編寫一個實作PageProcessor接口的類,就可以定制一個自己的爬蟲
- HTML分析是一個比較複雜的工作,Java世界主要有幾款比較友善的分析工具:
- Jsoup
- HtmlParser
- Apache tika
- HtmlCleaner與Xpath
- webmagic的Selector
- Selector是webmagic為了簡化頁面抽取開發的獨立子產品,是整個項目中我最得意的部分。這裡整合了CSS Selector、XPath和正規表達式,并可以進行鍊式的抽取,很容易就實作強大的功能。即使你使用自己開發的爬蟲工具,webmagic的Selector仍然值得一試
- Scheduler-URL管理
- URL管理的問題可大可小。對于小規模的抓取,URL管理是很簡單的。我們隻需要将待抓取URL和已抓取URL分開儲存,并進行去重即可。使用JDK内置的集合類型Set、List或者Queue都可以滿足需要。如果我們要進行多線程抓取,則可以選擇線程安全的容器,例如LinkedBlockingQueue以及ConcurrentHashMap。因為小規模的URL管理非常簡單,很多架構都并不将其抽象為一個子產品,而是直接融入到代碼中。但是實際上,抽象出Scheduler子產品,會使得架構的解耦程度上升一個檔次,并非常容易進行橫向擴充,這也是我從scrapy中學到的。
- Pipeline-離線處理和持久化
- Pipeline其實也是容易被忽略的一部分。大家都知道持久化的重要性,但是很多架構都選擇直接在頁面抽取的時候将持久化一起完成,例如crawer4j。但是Pipeline真正的好處是,将頁面的線上分析和離線處理拆分開來,可以在一些線程裡進行下載下傳,另一些線程裡進行處理和持久化。
3.2 使用WebMagic爬取一個桌面網站
首先引入WebMagic的依賴,webmagic-core-{version}.jar和webmagic-extension-{version}.jar。在項目中添加這兩個包的依賴,即可使用WebMagic。
maven中引入依賴jar包
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-core</artifactId>
<version>0.5.3</version>
</dependency>
<dependency>
<groupId>us.codecraft</groupId>
<artifactId>webmagic-extension</artifactId>
<version>0.5.3</version>
</dependency>12345678910
不使用maven的使用者,可以去http://webmagic.io中下載下傳最新的jar包。
3.3 爬蟲的實作
- 實作PageProcessor接口
- 在WebMagic裡,實作一個基本的爬蟲隻需要編寫一個類,實作PageProcessor接口即可。這個類基本上包含了抓取一個網站,你需要寫的所有代碼
- 爬蟲的配置
- 爬蟲相關配置,如抓取間隔、休息時間等
- 編寫爬蟲代碼
- 這裡我們爬取的是一個桌面網站 https://wallhaven.cc/
package photo; import us.codecraft.webmagic.Page; import us.codecraft.webmagic.Site; import us.codecraft.webmagic.Spider; import us.codecraft.webmagic.processor.PageProcessor; import java.io.FileNotFoundException; import java.io.IOException; public class GetPhoto implements PageProcessor { // 設定參數 private Site site = Site.me().setRetryTimes(3).setSleepTime(1000).setTimeOut(3000); /** * 主方法啟動爬蟲 */ public static void main(String[] args) { // 這裡隻爬取第一頁的桌面,如果要爬取其他頁數修改for循環參數即可 for (int i = 1; i <= 1; i++) { // 啟動爬蟲 Spider.create(new GetPhoto()) // 添加初始化的URL .addUrl("https://wallhaven.cc/toplist?page=" + i) // 使用單線程 .thread(1) // 運作 .run(); } } /** * 頁面處理邏輯 * 也就是通路主程式中的URL後得到的頁面 * * 爬蟲思路: * 1. 主程式通路URL後得到頁面 * 2. 将得到的頁面解析出需要的參數,并将解析出來并且需要爬取的連結放入爬取目标中 (45-59行) * TIPS:WebMagic會自動去識别哪些連接配接爬取過哪些沒有。 * 3. 通路第二步中放入的連結得到頁面,并解析(62行) * 4. 将圖檔的名字和字尾提取出來以便儲存(64-77行) */ public void process(Page page) { // 一頁是24張圖檔 for (int i = 1; i <= 24; i++) { // 使用Xpath解析,擷取到單個圖檔的網頁 // WebMagic支援使用Xpath、CSS選擇器、正規表達式、JsonPath來解析頁面 String str = page.getHtml().xpath("//div[@id=thumbs]/section/ul/li[" + i + "]/figure/a[@class=preview]/@href").toString(); // 擷取到的連接配接為null則退出循環,不添加進爬取目标 if (str == null) break; // 将爬取到的連結添加到待爬取頁面中 page.addTargetRequest(str); } // 将頁面中圖檔的位址提取出來,以便于使用工具類儲存 String pageURL = page.getHtml().xpath("//img[@id=wallpaper]/@src").toString(); if (pageURL != null) { try { // 擷取圖檔的名字和字尾提取出來用于儲存 String name = pageURL.substring(pageURL.length() - 9, pageURL.length() - 4); String suffix = pageURL.substring(pageURL.length() - 4); // 将圖檔的位址、名字、儲存路徑傳入檔案工具類進行下載下傳 DownloadImage.downLoadFromUrl(pageURL, name + suffix, "src/file/"); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } public Site getSite() { return site; } }
- 儲存資源(工具類)
package photo; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; class DownloadImage { public static void downLoadFromUrl(String urlStr, String fileName, String savePath) throws IOException { URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 設定逾時間為3秒 conn.setConnectTimeout(3 * 1000); // 防止屏蔽程式抓取而傳回403錯誤 conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)"); // 得到輸入流 InputStream inputStream = conn.getInputStream(); // 擷取自己數組 byte[] getData = readInputStream(inputStream); // 檔案儲存位置 File saveDir = new File(savePath); if (!saveDir.exists()) { saveDir.mkdir(); } File file = new File(saveDir + File.separator + fileName); FileOutputStream fos = new FileOutputStream(file); fos.write(getData); if (fos != null) { fos.close(); } if (inputStream != null) { inputStream.close(); } System.out.println("info:" + url + " download success"); } private static byte[] readInputStream(InputStream inputStream) throws IOException { byte[] buffer = new byte[1024]; int len = 0; ByteArrayOutputStream bos = new ByteArrayOutputStream(); while ((len = inputStream.read(buffer)) != -1) { bos.write(buffer, 0, len); } bos.close(); return bos.toByteArray(); } }
參考連接配接:
https://blog.csdn.net/u012385190/article/details/51517466
https://my.oschina.net/flashsword/blog/145796#h4_7
http://webmagic.io/docs/zh/posts/ch1-overview/