系列教程:html
手把手教你寫電商爬蟲-第一課 找個軟柿子捏捏
手把手教你寫電商爬蟲-第二課 實戰尚妝網分頁商品採集爬蟲ajax
看完兩篇,相信你們已經從開始的小菜鳥晉升爲中級菜鳥了,好了,那咱們就繼續咱們的爬蟲課程。chrome
上一課呢必定是由於對手太強,致使咱們並無完整的完成尚妝網的爬蟲。json
吭吭~,咱們這一課繼續,爭取完全搞定尚妝網,不留任何遺憾。segmentfault
咱們先回顧一下,上一課主要遺留了兩個問題,兩個問題都和ajax有關。框架
一、因爲是ajax加載下一頁,致使下一頁url並不會被系統自動發現。dom
二、商品頁面的價格是經過ajax加載的,咱們直接從網頁中獲取不到信息自己。異步
好了,咱們先解決第一個問題:函數
第一個問題其實是一個爬蟲中比較常見的問題,即url的發現,默認狀況下,URL的發現是神箭手雲爬蟲框架自動處理的,可是若是在ajax的狀況下,框架則無從發現url,這個時候就須要咱們本身手動處理url的發現,這裏,神箭手給咱們提供了一個很方便的回調函數讓咱們來本身處理url的發現:工具
onProcessHelperUrl(url, content, site)
這個回調函數有兩個參數,分別是當前處理的頁面對象和整個爬取站的對象,咱們能夠經過獲取頁面對象的內容來分析是否有咱們須要的新一頁的url,經過site.addUrl()方法加入到url隊列中既可。這裏咱們能夠看到,當超出頁數的時候,尚妝網會給咱們返回一個這樣的頁面,咱們就知道頁數超過了,不須要在加入新的頁url:
這個頁面咱們很好判斷,只須要看內容中是否有"無匹配商品"關鍵字既可。
這裏咱們須要一些基礎的js能力,代碼以下:
configs.onProcessHelperUrl = function(url, content, site){ if(!content.indexOf("無匹配商品")){ //若是沒有到最後一頁,則將頁數加1 var currentPage = parseInt(url.substring(url.indexOf("&page=") + 6)); var page = currentPage + 1; var nextUrl = url.replace("&page=" + currentPage, "&page=" + page); site.addUrl(nextUrl); } }
原理很簡單,若是內容中沒有無匹配商品這個關鍵詞的時候,則把當前頁面的下一頁加入的待爬隊列中。
好了,ajax分頁問題完全解決,下面來看這個最棘手的ajax內容加載的問題,也就是如何獲取到商品頁面中的價格信息
首先,遇到這類問題,咱們一般有兩個思路:
一、經過js引擎將整個頁面渲染出來以後,在去作內容抽取,這個方案對於一些複雜js頁面是惟一解決方案,用神箭手來處理也很簡單,不過因爲須要執行js,致使抓取速度很慢,不到不得已狀況,咱們先不使用這個核武器
二、經過剛剛處理分頁的經驗,咱們能夠預先分析ajax請求,而後將這一步多出來的請求和原來的頁面請求作一個關聯。這種方案適合比較簡單的js頁面中。
OK,介紹完思路,根據經驗,咱們感受尚妝網的ajax加載並無十分複雜,因此咱們選擇方案二來處理這種ajax頁面加載。
一樣的,首頁咱們經過chrome開發者工具,抓取到這個ajax請求,這裏教你們一個小竅門,開發者工具中,能夠篩選請求對象未xhr,這種就是異步請求,咱們就很容易發現咱們的嫌疑url:
http://item.showjoy.com/product/getPrice?skuId=22912
咱們在頁面中找一下這個22912怎麼提取最方便,咱們很快就發現了一個標籤:
<input type="hidden" value="22912" id="J_UItemId" />
這個標籤很乾淨,獲取的xpath也很簡單:
//input[@id="J_UItemId"]/@value
這樣就好辦了,咱們再看下這個頁面請求的結果是什麼:
{"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,專門用來解決關聯請求的請求的問題,也就是某一個字段的值能夠經過一個關聯請求的內容中抽取出來。語法我就不介紹了,直接上代碼吧:
{ 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更加簡單,相信你們一看就懂了。
好了,弄了這麼多,完整的代碼以下:
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("無匹配商品")){ //若是沒有到最後一頁,則將頁數加1 var currentPage = parseInt(url.substring(url.indexOf("&page=") + 6)); var page = currentPage + 1; var nextUrl = url.replace("&page=" + currentPage, "&page=" + page); site.addUrl(nextUrl); } return true; } var crawler = new Crawler(configs); crawler.start();
終於搞定了,咱們趕忙測試一下爬取的結果吧:
欣賞本身艱苦的勞動成果是否是頗有成就感,不過如今的爬取結果依然有些美中不足,評論數和銷售額拿到的都是一個完整的句子,而咱們但願獲得的是具體的數字,這個怎麼操做呢?這個其實就是一個字段抽取到以後的進一步處理,框架中給咱們提供了一個回調函數爲:
afterExtractField(fieldName, data)
函數會將抽取名和抽取到的數據傳進來,咱們只須要經過js的字符串處理函數對數據進行進一步加工既可,直接上完整的修改過的代碼:
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("無匹配商品")){ //若是沒有到最後一頁,則將頁數加1 var currentPage = parseInt(url.substring(url.indexOf("&page=") + 6)); var page = currentPage + 1; var nextUrl = url.replace("&page=" + currentPage, "&page=" + page); site.addUrl(nextUrl); } return true; } configs.afterExtractField = function(fieldName, data){ if(fieldName == "comment" || fieldName == "sales"){ var regex = /.*((\d+)).*/; return (data.match(regex))[1]; } return data; } var crawler = new Crawler(configs); crawler.start();
咱們判斷了若是是comment和sales抽取項時,經過正則直接匹配到括號裏的數字,這裏注意,網頁上的括號原本是全角的括號,因此千萬不要寫錯了。
此次終於能夠開心的看着本身的爬蟲數據結果了:
對爬蟲感興趣的童鞋能夠加qq羣討論:342953471。