Part.01 Webmagic介紹
webmagic是一個開源的Java垂直爬蟲框架,目標是簡化爬蟲的開發流程,讓開發者專注於邏輯功能的開發
WebMagic項目代碼分為核心和擴展兩部分
- 核心部分(webmagic-core)是一個精簡的、模塊化的爬蟲實現,而擴展部分則包括一些便利的、實用性的功能。WebMagic的架構設計參照了Scrapy,目標是盡量的模塊化,並體現爬蟲的功能特點。這部分提供非常簡單、靈活的API,在基本不改變開發模式的情況下,編寫一個爬蟲
- 擴展部分(webmagic-extension)提供一些便捷的功能,例如注解模式編寫爬蟲等。同時內置了一些常用的組件,便於爬蟲開發
Part.02 Webmagic設計原理
WebMagic的結構分為Downloader、PageProcessor、Scheduler、Pipeline四大組件,並由Spider將它們彼此組織起來。
WebMagic總體架構圖如下:
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。
用於數據流轉的對象
- Request:Request是對URL地址的一層封裝,一個Request對應一個URL地址。它是PageProcessor與Downloader交互的載體,也是PageProcessor控制Downloader唯一方式。除了URL本身外,它還包含一個Key-Value結構的字段extra。你可以在extra中保存一些特殊的屬性,然后在其他地方讀取,以完成不同的功能。例如附加上一個頁面的一些信息等。
- Page:Page代表了從Downloader下載到的一個頁面——可能是HTML,也可能是JSON或者其他文本格式的內容。Page是WebMagic抽取過程的核心對象,它提供一些方法可供抽取、結果保存等。
- ResultItems:ResultItems相當於一個Map,它保存PageProcessor處理的結果,供Pipeline使用。它的API與Map很類似,值得注意的是它有一個字段skip,若設置為true,則不應被Pipeline處理。
Part.03 Webmagic 實例(爬取 筆趣閣&bilibili的數據)
-
完整代碼下載:<點擊進入>
webmagic使用maven管理依賴,在項目中添加對應的依賴即可使用webmagic
<!-- web magic -->
<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>
核心代碼
- BilibiliReptile.java
package com.reptile.bilibili;
import com.mysql.dao.BilibiliDao;
import com.mysql.entity.Bilibili;
import com.mysql.pipeline.MysqlPipelineBilibili;
import com.tool.SplitJson;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.pipeline.ConsolePipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import java.sql.SQLException;
import static com.reptile.json.GetHttpInterface.GetHttpInterface;
public class BilibiliReptile implements PageProcessor{
//設置拼接的url變量
//爬取av號從1至1000000
private static int start =1;
private static int end =1000000;
//設置網站相關配置
//重試次數和抓取間隔
private Site site = Site.me().setRetryTimes(5).setSleepTime(0).setUserAgent("Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36");
public synchronized void process(Page page) {
//視頻標題
page.putField("title",page.getHtml().xpath("//h1[@class='video-title']/span/text()").get());
//如果title為空則跳過
if (page.getResultItems().get("title") == null) {
page.setSkip(true);
}
//標題圖
page.putField("image",page.getHtml().xpath("/html/head/meta[10]").get());
//up
page.putField("up",page.getHtml().xpath("//div[@class='name']/a[1]/text()").get());
//簡介
page.putField("info",page.getHtml().xpath("//div[@class='u-info']/div[2]/text()").get());
//分p
page.putField("part",page.getHtml().xpath("//*[@id=\"multi_page\"]/div[1]/div/span/text()").get());
//時間戳
page.putField("date",page.getHtml().xpath("//div/time/text()").get());
}
public Site getSite() {
// TODO Auto-generated method stub
return site;
}
public static void main(String[] args) throws SQLException {
int id = 1;
BilibiliDao bilidao = new BilibiliDao();
Bilibili bilibili = new Bilibili();
SplitJson sj = new SplitJson();
while (start<end) {
Spider.create(new BilibiliReptile()).addUrl("https://www.bilibili.com/video/av" + start + "/")
//輸出到控制台
.addPipeline(new ConsolePipeline())
//傳輸到數據庫
// .addPipeline(new MysqlPipelineBilibili())
//開啟5個線程抓取
.thread(5)
//啟動爬蟲
.run();
String str = GetHttpInterface("https://api.bilibili.com/x/web-interface/archive/stat?aid=" + start);
if((sj.splitCode(str)).equals("0"))
{
bilibili.setId(id);
bilibili.setPlay(sj.splitView(str));
bilibili.setBarrage(sj.splitDanmaku(str));
bilidao.addData(bilibili);
System.out.println(str);
System.out.println("view:" + sj.splitView(str));
System.out.println("danmuke:" + sj.splitView(str));
id++;
}
start++;
}
}
}
- BiQuGeReptile.java
package com.reptile.biquge;
import com.mysql.pipeline.MysqlPipelineBiQuGe;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.Spider;
import us.codecraft.webmagic.pipeline.ConsolePipeline;
import us.codecraft.webmagic.processor.PageProcessor;
import java.util.List;
public class BiQuGeReptile implements PageProcessor {
//regex of URL:http://www.xbiquge.la/
public static final String FIRST_URL = "http://www\\.xbiquge\\.la/\\w+";
public static final String HELP_URL = "/\\d+/\\d+/";
public static final String TARGET_URL = "/\\d+/\\d+/\\d+\\.html/";
private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);
public Site getSite() {
// TODO Auto-generated method stub
return site;
}
public void process(Page page) {
if(page.getUrl().regex(FIRST_URL).match()){
List<String> urls = page.getHtml().links().regex(HELP_URL).all();
page.addTargetRequests(urls);
//標題
page.putField("title",page.getHtml().xpath("//div[@id='info']/h1/text()").get());
//如果title為空則跳過
if (page.getResultItems().get("title") == null) {
page.setSkip(true);
}
//作者
page.putField("author",page.getHtml().xpath("//div[@id='info']/p/text()").get());
//簡介
page.putField("info",page.getHtml().xpath("//div[@id='intro']/p[2]/text()").get());
//首圖url
page.putField("image",page.getHtml().xpath("//div[@id='fmimg']/img").get());
//下一深度的網頁爬取章節和內容
if(page.getUrl().regex(HELP_URL).match()){
List<String> links = page.getHtml().links().regex(TARGET_URL).all();
page.addTargetRequests(links);
//章節
page.putField("chapter", page.getHtml().xpath("//div[@class='bookname']/h1/text()").get());
//內容
page.putField("content", page.getHtml().xpath("//div[@id='content']/text()").get());
}
}
}
public static void main(String[] args){
Spider.create(new BiQuGeReptile()).addUrl("http://www.xbiquge.la/xiaoshuodaquan/")
//輸出到控制台
.addPipeline(new ConsolePipeline())
//傳輸到數據庫
// .addPipeline(new MysqlPipelineBiQuGe())
//開啟5個線程抓取
.thread(5)
//啟動爬蟲
.run();
}
}
注意事項
在0.7.3版本中,爬取只支持TLS1.2的https站點的時候會報錯:
javax.net.ssl.SSLException: Received fatal alert: protocol_version
解決辦法:https://github.com/code4craft/webmagic/issues/701
Part.04 Webmagic 拓展
URL 去重
Scheduler是WebMagic中進行 URL 管理的組件。一般來說,Scheduler包括兩個作用:
對待抓取的URL隊列進行管理。
對已抓取的URL進行去重。
Scheduler的內部實現進行了重構,去重部分被單獨抽象成了一個接口:DuplicateRemover,從而可以為同一個Scheduler選擇不同的去重方式,以適應不同的需要,目前提供了三種去重方式。
HashSet
使用 java 中 HashSet 不能重復的特點去重。占用內存大,性能低
Redis 去重
使用 Redis 的 set 進行去重。優點是速度快,而且不會占用爬蟲服務器的資源。可以處理更大數據量的數據爬取;缺點是需要 redis 服務器,增加開發和使用成本
布隆過濾器(BloomFilter)
優點是占用內存比 HashSet 小的多,也適合大數據量的去重操作。
布隆過濾器的使用實例:
@Scheduled(initialDelay = 1000, fixedDelay = 60 * 1000 * 60 * 12)
public void start() {
Spider.create(new BdProcessor())
.addUrl(URL)
.thread(10)
// 設置布隆過濾器去重操作(默認使用HashSet來進行去重,占用內存較大;使用BloomFilter來進行去重,占用內存較小,但是可能漏抓頁面)
.setScheduler(new QueueScheduler().setDuplicateRemover(new BloomFilterDuplicateRemover(10000000)))
.addPipeline(dbPipeline)
.run();
}
網頁去重
指紋碼對比
最常見的去重方案是生成文檔的指紋門。例如對一篇文章進行 MD5 加密生成一個字符串,我們可以認為這是文章的指紋碼,再和其他的文章指紋碼對比,一致則說明文章重復。
但是這種方式是完全一致則是重復的,如果文章只是多了幾個標點符號,那仍舊被認為是重復的,這種方式並不合理。
BloomFilter
這種方式就是我們之前對 url 進行去重的方式,使用在這里的話,也是對文章進行計算得到一個數,再進行對比,缺點和方法 1 是一樣的,如果只有一點點不一樣,也會認為不重復,這種方式不合理。
KMP 算法
KMP 算法是一種改進的字符串匹配算法。KMP 算法的關鍵是利用匹配失敗后的信息,盡量減少模式串與主串的匹配次數以達到快速匹配的目的。能夠找到兩個文章有哪些是一-樣的,哪些不一樣
這種方式能夠解決前面兩個方式的“只要一點不一樣就是不重復”的問題。但是它的時空復雜度太高了,不適合大數據量的重復比對
SimHash (主要)
Google 的 simhash 算法產生的簽名,可以滿足上述要求。這個算法並不深奧,比較容易理解。這種算法也是目前 Google 搜索引擎所目前所使用的網頁去重算法
分詞,把需要判斷文本分詞形成這個文章的特征單詞。
hash,通過 hash 算法把每個詞變成 hash 值,比如“美國”通過 hash 算法計算為 100101,“51 區”通過 hash 算法計算為 101011。這樣我們的字符串就變成了一串串數字。
加權,通過 2 步驟的 hash 生成結果,需要按照單詞的權重形成加權數字串,。“美國”的 hash 值為“100101”,通過加權計算為“4-4-44-44”。“51 區”計算為‘“5-55-555”。
合並,把上面各個單詞算出來的序列值累加,變成只有一一個序列串。。“美國”的“4-4-44-44”,“51 區”的“5-55-555”。
代理的使用
有些網站不允許爬蟲進行數據爬取,因為會加大服務器的壓力。其中一種最有效的方式是通過 ip+時間進行鑒別,因為正常人不可能短時間開啟太多的頁面,發起太多的請求。
我們使用的 WebMagic 可以很方便的設置爬取數據的時間(參考第二天的的 3.1. 爬蟲的配置、啟動和終止)。但是這樣會大大降低我們 J 爬取數據的效率,如果不小心 ip 被禁了,會讓我們無法爬去數據,那么我們就有必要使用代理服務器來爬取數據。
代理 L(英語:Proxy),也稱網絡代理,是一-種特殊的網絡服務,允許一個網絡終端(一般為客戶端)通過這個服務與另一個網絡終端(一般為服務器)進行非直接的連接。
提供代理服務的電腦系統或其它類型的網絡終端稱為代理服務器(英文:Proxy. Server)。一個完整的代理請求過程為:客戶端首先與代理服務器創建連接,接着根據代理服務器所使用的代理協議,請求對目標服務器創建連接、或者獲得目標服務器的指定資源。
我們就需要知道代理服務器在哪里(ip 和端口號)才可以使用。網上有很多代理服務器的提供商,但是大多是免費的不好用,付費的還行。推薦個免費的服務網站:
-
米撲代理:<點擊進入>
配置代理
WebMagic的代理API ProxyProvider。因為相對於 Site 的“配置”,ProxyProvider定位更多是一個“組件”,所以代理不再從Site設置,而是由HttpClientDownloader設置。
- 設置代理:HttpClientDownloader.setProxyProvider(ProxyProvider proxyProvider)
ProxyProvider有一個默認實現:SimpleProxyProvider。它是一個基於簡單Round-Robin的、沒有失敗檢查的ProxyProvider。可以配置任意個候選代理,每次會按順序挑選一個代理使用。它適合用在自己搭建的比較穩定的代理的場景。
代理示例:
設置單一的普通HTTP代理為101.101.101.101的8888端口,並設置密碼為"username","password"
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(new Proxy("101.101.101.101",8888,"username","password")));
spider.setDownloader(httpClientDownloader);
HttpClientDownloader httpClientDownloader = new HttpClientDownloader();
httpClientDownloader.setProxyProvider(SimpleProxyProvider.from(
new Proxy("101.101.101.101",8888)
,new Proxy("102.102.102.102",8888)));
Part.05 總結
-
關於Webmagic使用說明的總結
- Webmagic屬於可快速上手的簡易爬蟲框架,在閱讀官方文檔后可快速上手開發,主要難點在於對於xpath(會正則的同學會很快就上手)的學習以及對於部分網站需要進行的cookie驗證、代理以及登陸驗證時有一定難度,對於部分動態渲染的前端頁面可通過Chrome內核內嵌代碼渲染的方式解決
Part.06 參考文獻
PS:確實覺得寫的很好,轉給大家分享,文中提到的一些操作自己打算日后試試