1、背景介紹
多主題爬蟲中,我們一般先分析網站的url特點(重點是列表頁),再根據項目需求預先設定好關鍵詞,對待爬取url,或者稱爲種子url進行精準控制。
1.1、分析一
帶關鍵詞的url場景有很多,如網站的特定版塊、某模塊發送的AJAX請求等都嵌入了關鍵詞。
eg:我們需要爬取同程旅遊網杭州的旅遊景點信息,url是:https://so.ly.com/hot?q=%E6%9D%AD%E5%B7%9E;
其中%E6%9D%AD%E5%B7%9E是Unicode對關鍵詞--“杭州”編碼的結果
eg:同程旅遊網從杭州到北京的國內遊,url是:
https://gny.ly.com/list?src=%E6%9D%AD%E5%B7%9E&dest=%E5%8C%97%E4%BA%AC&prop=0,等同於https://gny.ly.com/list?src=杭州&dest=北京&prop=0,實際上也等同於https://gny.ly.com/list?src=杭州&dest=北京。在瀏覽器輸入上述url後會顯示該主題列表的第一頁,點擊下一頁我們會發現該主題列表第二頁url是:
https://gny.ly.com/list?src=%E6%9D%AD%E5%B7%9E&dest=%E5%8C%97%E4%BA%AC&start=2
第三頁是:
https://gny.ly.com/list?src=%E6%9D%AD%E5%B7%9E&dest=%E5%8C%97%E4%BA%AC&start=3
····
https://gny.ly.com/list?src=%E6%9D%AD%E5%B7%9E&dest=%E5%8C%97%E4%BA%AC&start=n
由此,我們就可以得出該模塊的url拼接規則爲:https://gny.ly.com/list?src=關鍵詞1(Unicode編碼)+“&dest=”+關鍵詞2(Unicode編碼)+“&start=”+index(頁面索引)
再比如:百度新聞,關鍵詞搜索url:
https://www.baidu.com/s?rtt=1&bsst=1&cl=2&tn=news&rsv_dl=ns_pc&word=浙江++消防&pn=10
https://www.baidu.com/s?rtt=1&bsst=1&cl=2&tn=news&rsv_dl=ns_pc&word=浙江++消防&pn=20
1.2、分析二
再進一步抽象,一般我們在配置文件中設定關鍵詞,或者寫入數據庫中,然後爬蟲從中讀取並存入kw1List和kw2List中,
兩種方式的配置舉例如下:
- 配置1(yaml文件)
filters:
searchfilter:
kwfixvalue: [ 浙江, 江蘇, 上海, 北京, 天津 ]
kwvalue: [ 火災, 坍塌, 爆炸, 事故, 安全, 傷亡 ]
- 配置2(數據庫)
起始城市ID | 起始城市名稱 | 目的地ID | 目的地名稱 |
---|---|---|---|
0510 | 無錫 | 0571 | 杭州 |
001 | 北京 | 021 | 南京 |
0519 | 常州 | 0996 | 烏魯木齊 |
在拼接下一列表頁的邏輯中(即換頁,列表頁切換),我們需要用到的變量是: 當前關鍵詞1、當前關鍵詞2、當前關鍵詞1所處list1中的索引index1、當前關鍵詞2所處list2中的索引index2,以及已爬取到的頁面index(即網站所顯示的第幾頁);
2、解決
經上述分析,將列表頁url拼接邏輯中表示關鍵詞的選擇切換抽取出來,並用一個pojo類定義,可以命名爲KeywordOptions,代碼如下:
- currenyPage: 當前頁面索引(表示關鍵詞1+關鍵詞2搜索結果中的第幾頁)
- currentFixIndex: 關鍵詞2所在list2中的index
- kwFixValue: 關鍵詞2
- currentIndex: 關鍵詞1所在list1中的index
- kwValue: 關鍵詞1
public class KeywordOptions {
Long currentPage;
Integer currentFixIndex;
String kwFixValue = null;
Integer currentIndex;
String kwValue = null;
public KeywordOptions() {
}
public Long getCurrentPage() {
return this.currentPage;
}
public void setCurrentPage(Long currentPage) {
this.currentPage = currentPage;
}
public Integer getCurrentFixIndex() {
return this.currentFixIndex;
}
public void setCurrentFixIndex(Integer currentFixIndex) {
this.currentFixIndex = currentFixIndex;
}
public Integer getCurrentIndex() {
return this.currentIndex;
}
public void setCurrentIndex(Integer currentIndex) {
this.currentIndex = currentIndex;
}
public String getKwFixValue() {
return this.kwFixValue;
}
public void setKwFixValue(String kwFixValue) {
this.kwFixValue = kwFixValue;
}
public String getKwValue() {
return this.kwValue;
}
public void setKwValue(String kwValue) {
this.kwValue = kwValue;
}
}
並基於webmagic框架中的PageProcessor接口編寫抽象類BasePageProcessor,在該抽象類中根據通用性業務需求編寫相關方法, 首先是關鍵詞切換邏輯:
private boolean nextKeyword(KeywordOptions ko) {
if (this.searchFilterConfig == null) {
return false;
} else {
int kwSize = this.kwValues.size();
int kwFixSize;
if (this.kwFixValues == null) {
kwFixSize = 0;
} else {
kwFixSize = this.kwFixValues.size();
}
if (ko.getCurrentIndex() >= kwSize - 1) {
ko.setCurrentIndex(0);
if (ko.getCurrentFixIndex() >= kwFixSize - 1) {
return false;
} else {
ko.setCurrentFixIndex(ko.getCurrentFixIndex() + 1);
ko.setKwValue((String)this.kwValues.get(ko.getCurrentIndex()));
if (this.kwFixValues != null) {
ko.setKwFixValue((String)this.kwFixValues.get(ko.getCurrentFixIndex()));
}
return true;
}
} else {
ko.setCurrentIndex(ko.getCurrentIndex() + 1);
ko.setKwValue((String)this.kwValues.get(ko.getCurrentIndex()));
if (this.kwFixValues != null) {
ko.setKwFixValue((String)this.kwFixValues.get(ko.getCurrentFixIndex()));
}
return true;
}
}
}
其中根據KeywordOptions對象拼接url的方法如下,將該方法設爲public,以便後續根據不同拼接規則可以繼承重寫
public String koToUrl(KeywordOptions ko) {
StringBuilder builder = new StringBuilder(this.baseUrl);
builder.append(ko.getCurrentPage());
if (this.searchFilterConfig == null) {
return builder.toString();
}else if (ko.getKwValue() == null && ko.getKwFixValue() == null) {
return builder.toString();
} else {
builder.append("&");
if (ko.getKwValue() != null) {
if (this.kwCharset != null) {
try {
builder.append(URLEncoder.encode(ko.getKwValue(), this.kwCharset));
} catch (UnsupportedEncodingException var5) {
var5.printStackTrace();
}
} else {
builder.append(ko.getKwValue());
}
}
if (ko.getKwFixValue() != null) {
builder.append("+");
if (this.kwCharset != null) {
try {
builder.append(URLEncoder.encode(ko.getKwFixValue(), this.kwCharset));
} catch (UnsupportedEncodingException var4) {
var4.printStackTrace();
}
} else {
builder.append(ko.getKwFixValue());
}
}
return builder.toString();
}
}
最後得到下一列表頁請求(封裝url)
public synchronized Request nextListPage(KeywordOptions ko) {
//判斷任務是否結束,列表切換是否鎖定
if (!this.listAddLock && !this.isComplete) {
//獲取配置文件解析器實例
ConfigParser parser = ConfigParser.getInstance();
Boolean fixed = (Boolean)parser.getValue(this.commonConfig, "fixed", false, this.spiderConfig.getConfigPath() + ".common");
//判斷頁面url是否爲固定
if (fixed) {
return null;
} else {
String url;
//判斷當前頁是否爲列表頁尾頁
if (ko.getCurrentPage() >= this.totalPages) {
//爲真則切換關鍵詞
ko.setCurrentPage(Long.valueOf(String.valueOf(this.commonConfig.get("firstpage"))));
if (this.nextKeyword(ko)) {
url = this.koToUrl(ko);
return this.nextListPageHook(this.pushRequest(url, ko));
} else {
this.isComplete = true;
return this.nextListPageHook((Request)null);
}
} else {
//非尾頁,則當前頁面索引加一
ko.setCurrentPage(ko.getCurrentPage() + 1L);
url = this.koToUrl(ko);
return this.nextListPageHook(this.pushRequest(url, ko));
}
}
} else {
return null;
}
}
在BasePageProcessor中編寫頁面處理邏輯,相關代碼如下:
public void process(Page page) {
Iterator var4;
if (page.getUrl().toString().contains(this.baseUrl)) {
//判斷是否下載異常,自定義錯誤碼600
if (page.getStatusCode() == 600) {
this.listAddLock = false;
return;
}
//解析列表頁,後續業務會重寫processList(page)方法
if (this.processList(page)) {
this.processSuccessListPageCount.incrementAndGet();
logger.info("list page crawl success url={}", page.getUrl());
this.listAddLock = false;
} else {
this.processErrorListPageCount.incrementAndGet();
logger.warn("list page crawl failed url={}", page.getUrl());
}
//每個List request中存儲KeywordOptions實例
KeywordOptions ko = (KeywordOptions)JSON.parseObject((String)page.getRequest().getExtra("ko"), KeywordOptions.class);
if (ko != null) {
List<Request> requests = page.getTargetRequests();
var4 = requests.iterator();
while(var4.hasNext()) {
Request request = (Request)var4.next();
request.putExtra("kw", ko.getKwValue());
}
}
//獲取下一列表頁
Request listpage = this.nextListPage(ko);
if (listpage != null) {
listpage.putExtra("nocheckdup", true);
page.putField("listPage", listpage);
}
} else {
//詳細頁解析,同樣先進行異常檢查
if (page.getStatusCode() == 600) {
return;
}
try {
//processPage方法也會被後續具體業務重寫
this.processPage(page);
this.processSuccessPageCount.incrementAndGet();
} catch (Exception var7) {
this.processErrorPageCount.incrementAndGet();
logger.warn("page process failed url={} , error:{}", new Object[]{page.getUrl(), var7});
}
ResultItems items = page.getResultItems();
String keyword = (String)page.getRequest().getExtra("kw");
if (keyword == null) {
keyword = this.kwValues != null ? (String)this.kwValues.get(0) : null;
}
if (keyword != null) {
var4 = items.getAll().entrySet().iterator();
while(var4.hasNext()) {
Map.Entry<String, Object> entry1 = (Map.Entry)var4.next();
Map<String, Object> map = (Map)entry1.getValue();
map.put("keyword", keyword);
}
}
}
}