系列教程: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。