1. 程式人生 > >手把手教你寫電商爬蟲(3):實戰尚妝網AJAX請求處理和內容提取

手把手教你寫電商爬蟲(3):實戰尚妝網AJAX請求處理和內容提取

看完兩篇,相信大家已經從開始的小菜鳥晉升為中級菜鳥了,好了,那我們就繼續我們的爬蟲課程。

上一課呢一定是因為對手太強,導致我們並沒有完整的完成尚妝網的爬蟲。


吭吭~,我們這一課繼續,爭取徹底搞定尚妝網,不留任何遺憾。

我們先回顧一下,上一課主要遺留了兩個問題,兩個問題都和ajax有關。

1、由於是ajax載入下一頁,導致下一頁url並不會被系統自動發現。

2、商品頁面的價格是通過ajax載入的,我們直接從網頁中獲取不到資訊本身。

好了,我們先解決第一個問題:

第一個問題實際上是一個爬蟲中比較常見的問題,即url的發現,預設情況下,URL的發現是神箭手雲爬蟲框架自動處理的,但是如果在ajax的情況下,框架則無從發現url,這個時候就需要我們自己手動處理url的發現,這裡,神箭手給我們提供了一個很方便的回撥函式讓我們來自己處理url的發現:

onProcessHelperUrl(url, content, site)
這個回撥函式有兩個引數,分別是當前處理的頁面物件和整個爬取站的物件,我們可以通過獲取頁面物件的內容來分析是否有我們需要的新一頁的url,通過site.addUrl()方法加入到url佇列中既可。這裡我們可以看到,當超出頁數的時候,尚妝網會給我們返回一個這樣的頁面,我們就知道頁數超過了,不需要在加入新的頁url:

這個頁面我們很好判斷,只需要看內容中是否有”無匹配商品”關鍵字既可。
這裡我們需要一些基礎的js能力,程式碼如下:

12345678910 configs.onProcessHelperUrl=function(url,content,site){if(!content.indexOf("無匹配商品")){//如果沒有到最後一頁,則將頁數加1  varcurrentPage=parseInt(url.substring(url.indexOf("&page=")+6));varpage=currentPage+1;varnextUrl=url.replace("&page="+currentPage,"&page="
+page);site.addUrl(nextUrl);}}

原理很簡單,如果內容中沒有無匹配商品這個關鍵詞的時候,則把當前頁面的下一頁加入的待爬佇列中。
好了,ajax分頁問題徹底解決,下面來看這個最棘手的ajax內容載入的問題,也就是如何獲取到商品頁面中的價格資訊

首先,遇到這類問題,我們通常有兩個思路:
1、通過js引擎將整個頁面渲染出來之後,在去做內容抽取,這個方案對於一些複雜js頁面是唯一解決方案,用神箭手來處理也很簡單,不過由於需要執行js,導致抓取速度很慢,不到不得已情況,我們先不使用這個核武器
2、通過剛剛處理分頁的經驗,我們可以預先分析ajax請求,然後將這一步多出來的請求和原來的頁面請求做一個關聯。這種方案適合比較簡單的js頁面中。

OK,介紹完思路,根據經驗,我們感覺尚妝網的ajax載入並沒有十分複雜,所以我們選擇方案二來處理這種ajax頁面載入。
同樣的,首頁我們通過chrome開發者工具,抓取到這個ajax請求,這裡教大家一個小竅門,開發者工具中,可以篩選請求物件未xhr,這種就是非同步請求,我們就很容易發現我們的嫌疑url:

12 http://item.showjoy.com/product/getPrice?skuId=22912

我們在頁面中找一下這個22912怎麼提取最方便,我們很快就發現了一個標籤:

123 <code><input type="hidden"value="22912"id="J_UItemId"/></code>

這個標籤很乾淨,獲取的xpath也很簡單:

12 //input[@id="J_UItemId"]/@value  

這樣就好辦了,我們再看下這個頁面請求的結果是什麼:

1234 {"count":0,"data":{"discount":"6.2","discountMoney":"43.00","originalPrice":112,"price":"69.00","showjoyPrice":"69.00"},"isRedirect":0,"isSuccess":0,"login":0}

可以看出來,是一個典型的json物件,這個就好辦了,神箭手中給我們提供了通過jsonpath提取內容的方式,可以很簡單的提取到價格物件,即price對應的值。
那最後我們怎麼才能關聯這個請求呢?這裡也是框架中提供的一個方案,叫做attachedUrl,專門用來解決關聯請求的請求的問題,也就是某一個欄位的值可以通過一個關聯請求的內容中抽取出來。語法我就不介紹了,直接上程式碼吧:

123456789101112 {name:"skuid",selector:"//input[@id='J_UItemId']/@value",},{name:"price",sourceType:SourceType.AttachedUrl,attachedUrl:"http://item.showjoy.com/product/getPrice?skuId={skuid}",selectorType:SelectorType.JsonPath,selector:"$.data.price",}

簡單介紹一下attachedUrl的用法,首先我們要設定sourceType為attachedUrl,同時我們要設定一個attachedUrl,即為關聯請求的地址,其中由於有一個值是動態的,所以我們需要在這個抽取項之前先抽取一下這個動態的值,所以我們增加了一個抽取項的名字叫做skuid,在attachedUrl中的呼叫方法為{skuid},真實請求時,該項就會被自動替換成我們上一個skuid抽取項抽取到的值。接著,由於我們獲取到的是json返回,因此我們抽取的方式應該是通過jsonpath,最後,寫一個抽取規則既可,jsonpath比xpath更加簡單,相信大家一看就懂了。

好了,弄了這麼多,完整的程式碼如下:

Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 var configs={domains:["www.showjoy.com","list.showjoy.com","item.showjoy.com"],scanUrls:["http://list.showjoy.com/search/?q=cateIds%3A1,cateName%3A%E9%9D%A2%E8%86%9C"],contentUrlRegexes:["http://item\\.showjoy\\.com/sku/\\d+\\.html"],helperUrlRegexes:["http://list\\.showjoy\\.com/search/\\?q=cateIds%3A1,cateName%3A%E9%9D%A2%E8%86%9C(\\&page=\\d+)?"],//可留空fields:[{//第一個抽取項name:"title",selector:"//h3[contains(@class,'choose-hd')]",//預設使用XPath  required:true//是否不能為空},{//第二個抽取項name:"comment",selector:"//div[contains(@class,'dtabs-hd')]/ul/li[2]",//使用正則的抽取規則required:false//是否不能為空},{//第三個抽取項name:"sales",selector:"//div[contains(@class,'dtabs-hd')]/ul/li[3]",//使用正則的抽取規則required:false//是否不能為空},{name:"skuid",selector:"//input[@id='J_UItemId']/@value",},{name:"price",sourceType:SourceType.AttachedUrl,attachedUrl:"http://item.showjoy.com/product/getPrice?skuId={skuid}",selectorType:SelectorType.JsonPath,selector:"$.data.price",}]};configs.onProcessHelperUrl=function(url,content,site){if(!content.indexOf("無匹配商品")){//如果沒有到最後一頁,則將頁數加1var currentPage=parseInt(url.substring(url.indexOf("&page=")+6));var page=currentPage+1;var nextUrl=url.replace("&page="+currentPage,"&page="+page);site.addUrl(nextUrl);}returntrue;}var crawler=newCrawler(configs);crawler.start();

終於搞定了,我們趕緊測試一下爬取的結果吧:

欣賞自己艱苦的勞動成果是不是很有成就感,不過現在的爬取結果依然有些美中不足,評論數和銷售額拿到的都是一個完整的句子,而我們希望得到的是具體的數字,這個怎麼操作呢?這個其實就是一個欄位抽取到之後的進一步處理,框架中給我們提供了一個回撥函式為:
afterExtractField(fieldName, data)
函式會將抽取名和抽取到的資料傳進來,我們只需要通過js的字串處理函式對資料進行進一步加工既可,直接上完整的修改過的程式碼:

Python
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758 var configs={domains:["www.showjoy.com","list.showjoy.com","item.showjoy.com"],scanUrls:["http://list.showjoy.com/search/?q=cateIds%3A1,cateName%3A%E9%9D%A2%E8%86%9C"],contentUrlRegexes:["http://item\\.showjoy\\.com/sku/\\d+\\.html"],helperUrlRegexes:["http://list\\.showjoy\\.com/search/\\?q=cateIds%3A1,cateName%3A%E9%9D%A2%E8%86%9C(\\&page=\\d+)?"],//可留空fields:[{//第一個抽取項name:"title",selector:"//h3[contains(@class,'choose-hd')]",//預設使用XPath  required:true//是否不能為空},{//第二個抽取項name:"comment",selector:"//div[contains(@class,'dtabs-hd')]/ul/li[2]",//使用正則的抽取規則required:false//是否不能為空},{//第三個抽取項name:"sales",selector:"//div[contains(@class,'dtabs-hd')]/ul/li[3]",//使用正則的抽取規則required:false//是否不能為空},{name:"skuid",selector:"//input[@id='J_UItemId']/@value",},{name:"price",sourceType:SourceType.AttachedUrl,attachedUrl:"http://item.showjoy.com/product/getPrice?skuId={skuid}",selectorType:SelectorType.JsonPath,selector:"$.data.price",}]};configs.onProcessHelperUrl=function(url,content,site){if(!content.indexOf("無匹配商品")){//如果沒有到最後一頁,則將頁數加1var currentPage=parseInt(url.substring(url.indexOf("&page=")+6));var page=currentPage+1;var nextUrl=url.replace("&page="+currentPage,"&page="+page);site.addUrl(nextUrl);}returntrue;}configs.<