webmagic框架使用java語言,抽象出爬蟲常用操作,封裝了爬蟲中使用頻繁的庫類,使整個爬取邏輯更加清晰有條理。
關於webmagic的使用指南在官網描述的很清楚,簡單介紹下項目結構。
先看下官網的一個流程圖介紹:
這裏展現了爬蟲的4個組件,Scheduler、Downloader、PageProcessor、Pipeline,它們相互獨立,協同合作。它們直接通過Request、Page、ResultItem來進行通信。
再看看webmagic-core源碼的結構:
- Page.java
- Request.java
- ResultItems.java
- Site.java
- Spider.java
- SpiderListener.java
- Task.java
Downloader
-
- AbstractDownloader.java
- Downloader.java
- HttpClientDownloader.java
- HttpClientGenerator.java
Scheduler
-
- DuplicateRemovedScheduler.java
- MonitorableScheduler.java
- PriorityScheduler.java
- QueueScheduler.java
- Scheduler.java
Pipeline
-
- CollectorPipeline.java
- ConsolePipeline.java
- FilePipeline.java
- Pipeline.java
- ResultItemsCollectorPipeline.java
Processor
-
- PageProcessor.java
- SimplePageProcessor.java
Proxy
-
- Proxy.java
- ProxyPool.java
- SimpleProxyPool.java
Selector
-
- AbstractSelectable.java
- CssSelector.java
- HtmlNode.java
- OrSelector.java
- RegexSelector.java
- Selector.java
- XpathSelector.java
- AndSelector.java
- ElementSelector.java
- Json.java
- PlainText.java
- ReplaceSelector.java
- Selectors.java
- BaseElementSelector.java
- Html.java
- JsonPathSelector.java
- RegexResult.java
- Selectable.java
- SmartContentSelector.java
Thread
-
- CountableThreadPool.java
Utils
-
- Experimental.java
- FilePersistentBase.java
- HttpConstant.java
- NumberUtils.java
- ProxyUtils.java
- UrlUtils.java
Spider是實現了Task,創建一個Spider就是新建一個任務,這個類中調用其他組件,是整體爬取流程的核心。
Page是downloader下載頁面後的保存結果,之後要將Page傳入processor中根據自己需求進行處理。
Site是對爬取頁面進行配置,如設置起始頁地址,重試次數,重試間隔等等。
ResultItems是Page處理之後的結果,之後要將ResultItems傳入Pipeline中進行結果處理。
SpiderListener是對Spider進行監聽,處理成功還是失敗。利用這個功能,你可以查看爬蟲的執行情況——已經下載了多少頁面、還有多少頁面、啓動了多少線程等信息。該功能通過JMX實現,你可以使用Jconsole等JMX工具查看本地或者遠程的爬蟲信息。
Request是對一次請求的抽象,其中包含請求的url和method等屬性。
downloader下載器,默認HttpClientDownloader。
secheduler負責待抓取urls隊列的維護。
pipeline主要負責對結果ResultItems的處理,默認ConsolePipeline,(可由用戶自定義)。
processor負責解析頁面,抽取結果保存到ResultItems,(可由用戶自定義)。
proxy代理相關配置。
selector頁面元素選擇器,包括css、正則、xpath等等。
thread線程池管理。
utils一些工具類。
這裏以maven安裝方式,舉一個簡單的例子來描述各個類的作用。
以爬取這個網址的列表爲例http://www.dailianmeng.com/p2pblacklist/index.html,我們需要把這個列表全部爬取並保存,使用webmagic只需要新建三個文件。
App.java
public class App{
Logger logger;
App() {
InputStream inputStream = App.class.getResourceAsStream("/log4j.properties");
if (inputStream != null)
PropertyConfigurator.configure(inputStream);
else
BasicConfigurator.configure();
logger = LoggerFactory.getLogger(getClass());
}
public static void main(String[] args) {
App app = new App();
app.run();
}
void run() {
logger.info("spider for dlm start....");
Spider.create(new DLMPageprocessor())
.addUrl(DLMPageprocessor.START_PAGE)
.addPipeline(new DLMPipeline())
.addPipeline(new ConsolePipeline())
.thread(5)
.run();
}
}
DLMPageprocessor.java
package com.xxx.dailianmeng;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Site;
import us.codecraft.webmagic.processor.PageProcessor;
import us.codecraft.webmagic.selector.Selectable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class DLMPageprocessor implements PageProcessor {
private Site site = Site.me().setRetryTimes(3).setSleepTime(1000);
static Logger logger = LoggerFactory.getLogger(DLMPageprocessor.class);
public static String START_PAGE = "http://www.dailianmeng.com/p2pblacklist/index.html";
@Override
public void process(Page page) {
page.addTargetRequests(page.getHtml().links().regex("/p2pblacklist/index.html\\?P2pBlacklist_page=\\w+").all());
ArrayList list = new ArrayList();
Selectable selectable = page.getHtml().xpath("//div[@class='table-responsive']//table//tbody//tr");
for (Selectable sel : selectable.nodes()) {
String[] str = new String[10];
sel.xpath("//td/html()").all().toArray(str);
Map map = new HashMap<>();
map.put("name", str[0].replace(" ", ""));
map.put("idcard", str[1].replace(" ", ""));
map.put("mobile", str[2].replace(" ", ""));
map.put("addr", str[3].replace(" ", ""));
map.put("totalAmount", str[4].replace(" ", "0.00"));
map.put("paidAmount", str[5].replace(" ", "0.00"));
map.put("unpaidAmount", str[6].replace(" ", "0.00"));
map.put("debtDate", str[7].replace(" ", "0000-00-00"));
map.put("debtTerms", str[8].replace(" ", ""));
map.put("extId", sel.xpath("//td//a").links().get());
list.add(map);
}
page.putField("mysql", list);
}
@Override
public Site getSite() {
return site;
}
}
DLMPipeline.java
package com.xxx.dailianmeng;
import com.u51.xsg.mysql.Mysql;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.codecraft.webmagic.ResultItems;
import us.codecraft.webmagic.Task;
import us.codecraft.webmagic.pipeline.Pipeline;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
public class DLMPipeline implements Pipeline {
Logger logger = LoggerFactory.getLogger(DLMPipeline.class);
public void process(ResultItems resultItems, Task task) {
Iterator iterator = resultItems.getAll().entrySet().iterator();
while (iterator.hasNext()) {
//處理結果
}
}
}
爬取操作非常簡單,App.java中一個main函數,Pageprocessor對爬取的頁面進行處理,Pipeline對處理結果進行操作,用戶只需關心業務邏輯,而不用關心爬蟲實現。
那麼在簡單操作的背後,具體是怎麼實現的呢?
首先看App.java中創建了一個Spider對象,Spider也控制着整個爬取流程。所以我們可以看下Spider的run方法裏都做了什麼。
@Override
public void run() {
checkRunningStat();
initComponent();
logger.info("Spider " + getUUID() + " started!");
while (!Thread.currentThread().isInterrupted() && stat.get() == STAT_RUNNING) {
Request request = scheduler.poll(this);
if (request == null) {
if (threadPool.getThreadAlive() == 0 && exitWhenComplete) {
break;
}
// wait until new url added
waitNewUrl();
} else {
final Request requestFinal = request;
threadPool.execute(new Runnable() {
@Override
public void run() {
try {
processRequest(requestFinal);
onSuccess(requestFinal);
} catch (Exception e) {
onError(requestFinal);
logger.error("process request " + requestFinal + " error", e);
} finally {
if (site.getHttpProxyPool()!=null && site.getHttpProxyPool().isEnable()) {
site.returnHttpProxyToPool((HttpHost) requestFinal.getExtra(Request.PROXY), (Integer) requestFinal
.getExtra(Request.STATUS_CODE));
}
pageCount.incrementAndGet();
signalNewUrl();
}
}
});
}
}
stat.set(STAT_STOPPED);
// release some resources
if (destroyWhenExit) {
close();
}
}
run方法總大致分成倆步操作,先初始化各個組件initComponent(),再從線程池中取一個Request出來處理。
1、initComponent():
protected void initComponent() {
if (downloader == null) {
this.downloader = new HttpClientDownloader();
}
if (pipelines.isEmpty()) {
pipelines.add(new ConsolePipeline());
}
downloader.setThread(threadNum);
if (threadPool == null || threadPool.isShutdown()) {
if (executorService != null && !executorService.isShutdown()) {
threadPool = new CountableThreadPool(threadNum, executorService);
} else {
threadPool = new CountableThreadPool(threadNum);
}
}
if (startRequests != null) {
for (Request request : startRequests) {
scheduler.push(request, this);
}
startRequests.clear();
}
startTime = new Date();
}
初始化默認的downloader,添加pipelines默認爲ConsolePipeline(),初始化線程池,如果設置了startRequests就會把裏面的request推入schedule隊列中。2、如果當前線程沒有被打斷,而且狀態是RUNNING那麼就從schedule去一個request出來處理,如果schedule取出的request是空,那麼等待新的請求加進來,如果設置了沒任務就結束爬取進程那麼此時就break結束。
在線程池中啓用一個線程來進行爬取,執行processRequest()方法。
protected void processRequest(Request request) {
Page page = downloader.download(request, this);
if (page == null) {
sleep(site.getRetrySleepTime());
onError(request);
return;
}
// for cycle retry
if (page.isNeedCycleRetry()) {
extractAndAddRequests(page, true);
sleep(site.getRetrySleepTime());
return;
}
pageProcessor.process(page);
extractAndAddRequests(page, spawnUrl);
if (!page.getResultItems().isSkip()) {
for (Pipeline pipeline : pipelines) {
pipeline.process(page.getResultItems(), this);
}
}
//for proxy status management
request.putExtra(Request.STATUS_CODE, page.getStatusCode());
sleep(site.getSleepTime());
}
首先調用downloader下載頁面,如果需要週期下載,那麼將頁面中的地址提取出來添加到待抓取隊列。
然後將page丟給pageProcessor的process方法,上面這個例子我們的pageProcessor = new DLMPageprocessor(),在process方法中
page.addTargetRequests(page.getHtml().links().regex("/p2pblacklist/index.html\\?P2pBlacklist_page=\\w+").all());
我們將下一步需要爬取的地址存入targetRequests變量中,這裏再用extractAndAddRequests()方法從targetRequests變量中提取地址,放入到schedule中。
process方法中的
page.putField("mysql", list);會將處理的結果存入到ResultItems變量裏,這裏循環pipelines將Resultem和當前Spider丟個各個註冊的pipeline進行處理,這裏便是能註冊多個pipeline的原因。
最後將請求狀態碼存入Request.extras變量中, 對代理管理有用。
到這裏簡單的介紹了一下爬取的流程,現在總結以下幾個點。
1、所謂的pageprocessor、pipeline組件化就是將這些對象設爲可變的變量,定義統一接口。
2、爬取的流程都在Spider類中,邏輯比較清晰不復雜。