目标网站:http://www.vip.com/
方法:scrapy Mysql 正则表达式
思路
第一步:分析爬虫入口,需要爬取唯品会在售的所有商品,打开首页可以看到有个分类按钮,点击进去分为商品分类和品牌分类。想要一并分析一下各品牌的受欢迎程度和在售商品数量,所以选择品牌分类。入口找到:http://category.vip.com/?act=brand
第二步:找到各品牌页面的url
第三步:爬取各品牌页面下的商品数据和品牌收藏数、商品数量。
第四步:在mysql数据库中,分表存储品牌数据和商品数据,两表以品牌ID来关联。
实战
基本思路就是上面的,下面开干。
首先,打开品牌分类页,可以看到各大类下都有小的分类,不过最前面有个全部,里面包涵了该大类下的全部品牌,点击品牌图片可进入对应的品牌页,我们只需要通过xpath定位到每个品牌小格子来提取url即可,不过我们查看品牌分类页的网页源码并没有找到这些url,所以可以怀疑这个是异步加载的。右键审查元素,打开log,刷新页面,页面往下拉,显示出第一个分类品质女装的品牌信息。可以看到加载了一个XHR格式的页面和一堆的图片,看一下这个页面返回的数据,看到data的值中是一个列表,展开第一个元素,可以发现’name :"乐为COMME LA VIE帽子专场"
,和第一个品牌对上了,link的值也和该品牌的url对上了,再看后面的元素也能对上。
这时,可以确定这个页面就是品牌数据页面,反过来看看请求数据。是一个get请求,但是有很多请求参数,对比多个大类的品牌数据页可以发现只有三个值是变动的
get请求参数
callback
分析可以看到是每个大类的英文名称0department_id
、new_cat_id
这都是一些数据,看似毫无规律,不过在查看品牌分类页面源码时发现中间有很大一段的javascript代码,完全看不懂,不过倒也是有一些数字,观察可以发现其实都出自这里。网页源码中包涵的数据 请求参数都找到了,通过正则表达式获取这段代码,再用eval函数来格式化,后面就可以以字典的形式来取值。之后直接构造get请求的url。测试发现,每个品牌的url只是ID的改变,这个id就是brand_id,那么只需要爬取到这个即可。返回的代码也不是js代码,同样使用正则表达式和eval来格式化数据方便取值。
得到品牌页面的url后,可以看到有一个收藏品牌数,我们就以此作为品牌受欢迎的数值提现。那么我们先来找到这个数据。在页面源码中同样搜不到现在的收藏数。审查元素可以初步判断是来自javascript。
抓包查看js,能找到这个收藏数。
Referer
,这样就正常了。再来抓包分析商品数据,发现也是来自XHR类型页面,打开品牌页面时加载了3个XHR页面,第一个返回的是该品牌下的商品分类,第二个返回的主要是一些数字列表,第三个就是我们想要的商品数据。
抓包分析商品数据 不过这个get请求的参数有点多,变化的只有两个值
productIds
,r
,其中r为品牌id,productIds
这里面是数字列表,分析发现这些都来自第二个XHR页面返回的数据
商品信息请求参数抓包
其中products
的数据即是前面的productIds
,这也是商品ID,只是默认一次只加载50个商品。'total'为该品牌商品总数。接下来,我们分析该get请求,
fromIndex
为起始值,batchSize
截止值。接下来就可以带入参数发起get请求获取到
products
后,再带入商品数据页面的请求,获取到商品信息,然后存储到mysql数据库。不多说,直接上代码:
class VipSpider(scrapy.Spider):
name = 'vip'
allowed_domains = ['vip.com']
start_urls = ['http://category.vip.com/?act=brand']
def parse(self, response):
#获取源码里的json数据,包涵分类名称,品牌信息页面的ID连接所需的数据
data = eval(re.findall(r'<script>var brandCategoryData = (\[.*?\]);', response.text)[0])
for x in data:
callback = x['key']
department_id = x['param']['department_id']
new_cat_id = ",".join([z['cids'] for z in x['word']])
url = 'http://category.vip.com/ajax/getSearchBrandBase.php?callback={}&page=190022&domain=www&show_in=0%2C1%2C2&ps=200&warehouse=VIP_HZ&fields=logo%2Clink%2Cimg%2Cname%2Cagio%2Cforeword%2Cbrand_id%2Csell_time_from&sort=operate-desc&department_id={}&new_cat_id={}'.format(callback,department_id,new_cat_id)
yield Request(url,callback=self.getid,meta={'fl_name':x['chns']}) #传递分类名称fl_name
def getid(self,response):
#分析一个分类下品牌信息,找出品牌id,组成品牌店铺url和店铺下所有商品ID的url
for x in eval(re.findall(r'"success","data":(\[.*?\]),"info":', response.text)[0]):
mm = Sql.select_pp(x['brand_id'])#判断该品牌是否已经保存
if mm == 1:
print u'该品牌数据已经保存'
else:
name_url = 'http://list.vip.com/%s.html' %x['brand_id'] #获取品牌名称的
data_url ='http://list.vip.com/api-ajax.php?callback=getMerchandiseIds&getPart=getMerchandiseRankList&r=%s' %x['brand_id']
yield Request(name_url,callback=self.getname,meta={'brand_id':x['brand_id'],'fl_name':response.meta['fl_name']})
yield Request(data_url,callback=self.getdata_url,meta={'brand_id':x['brand_id']})
def getname(self,response):
#获取品牌的名称,构造品牌收藏数所在的页面url
name = response.xpath('//meta[@name="keywords"]/@content').extract()[0]
scs_url = 'http://fav.vip.com/api/fav/sales/isfavanducount?callback=inquiryFavStatusAmountCB&business=VIPSALES&brand_id='+str(response.meta['brand_id'])
yield Request(scs_url,callback=self.getshoucang,meta={'name':name,'brand_id':response.meta['brand_id'],'fl_name':response.meta['fl_name']},headers={'User-Agent':'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
'Referer':response.url,
})#注意,收藏数所在的页面,在请求的时候请求头需要加上Referer,否则为空
def getshoucang(self,response):
#获取品牌的收藏数,构造店铺商品数量统计的页面url
shoucang = re.findall(r'"ucount":(.*?),"brandfav"', response.text)[0]
lei_url = 'http://list.vip.com/api-ajax.php?callback=getCategoryListCB&getPart=getCategoryList&r='+str(response.meta['brand_id'])
yield Request(lei_url,callback=self.getlei,meta={'name':response.meta['name'],'brand_id':response.meta['brand_id'],'shoucang':shoucang,'fl_name':response.meta['fl_name']})
def getlei(self,response):
#获取商品总数,并且返回数据品牌相关的数据,传给pipelines保存到数据库
item = WeipinghuiItem()
lei = 0
for x in eval(re.findall(r'{"category":(\[.*?\]),"size":', response.text)[0]):
lei =lei + int(x['total'])
item['pp_name'] = response.meta['name']
item['fl_name'] = response.meta['fl_name']
item['pp_shoucang'] = response.meta['shoucang']
item['pp_url'] = 'http://list.vip.com/%s.html' %response.meta['brand_id']
item['pp_id'] = response.meta['brand_id']
item['pp_shu'] = lei
yield item
def getdata_url(self,response):
#获取店铺所有商品的id,由于每一次请求商品详情,最多只能50个,故以for循环进行分割,构造多个商品详情列表所在页面的url
data =eval(re.findall(r'"products":(\[.*?\]),"keepTime"', response.text)[0])
if data==[]:
print '已经抢购一空'
else:
b =[data[i:i+50] for i in range(0,len(data),50)]
for u in [",".join(x) for x in b]:
url = 'http://list.vip.com/api-ajax.php?callback=getMerchandiseDroplets1&getPart=getMerchandiseInfoList&productIds=%s&r=%s' %(u,str(response.meta['brand_id']))
yield Request(url,callback=self.getdata,meta={'brand_id':response.meta['brand_id']})
def getdata(self,response):
#获取商品信息,传给pipelines保存到数据库
item = DataItem()
null = ''
data =eval(re.findall(r'"merchandiseInfoList":(\[.*?\])', response.text)[0])
for d in data:
item['data_pp_id'] = response.meta['brand_id']
item['data_id'] = d['mid']
item['data_name'] = d['productName']
item['data_url'] = d['detailUrl'].replace('\/','/')
item['data_jiage'] = d['vipshopPrice']
mm = Sql.select_data(d['mid'])
if mm == 1:
print '该商品已经存在'
else:
yield item
数据展示
品牌信息商品信息
两张表可以通过pp__id来联接查询。
代码缺陷
上面的思路是在写完代码之后又反思得到的最佳线路,而以上代码是初步写成,还没有优化,所以还有一些缺陷,和以上思路有些区别。
各品牌的商品总数是通过各品牌商品分类数据中得到的值累加得到,这样有个优点,就是可以提前得到商品总数,在获取商品ID时直接带上batchSize
,一次完整获取所有的商品ID。而思路中是通过商品ID页返回的'total',在商品数未知的情况下,默认一次获取200各商品ID,需要发起多次请求来获取全部的商品ID(这个没有在代码中写出)
总结
唯品会良心,没有设置复杂的反爬机制,不限制单IP请求数和请求频率,虽然使用了javascript看不懂,不过请求参数全部在源码中,简单分析即可得到。不过应该需要补充javascript相关的知识才好。
网友评论