抓取唯品会商品数据

2017-09-13  本文已影响0人  我来组成头部

目标网站: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 分析可以看到是每个大类的英文名称0
department_idnew_cat_id 这都是一些数据,看似毫无规律,不过在查看品牌分类页面源码时发现中间有很大一段的javascript代码,完全看不懂,不过倒也是有一些数字,观察可以发现其实都出自这里。
网页源码中包涵的数据 请求参数都找到了,通过正则表达式获取这段代码,再用eval函数来格式化,后面就可以以字典的形式来取值。之后直接构造get请求的url。测试发现,每个品牌的url只是ID的改变,这个id就是brand_id,那么只需要爬取到这个即可。返回的代码也不是js代码,同样使用正则表达式和eval来格式化数据方便取值。

得到品牌页面的url后,可以看到有一个收藏品牌数,我们就以此作为品牌受欢迎的数值提现。那么我们先来找到这个数据。在页面源码中同样搜不到现在的收藏数。审查元素可以初步判断是来自javascript。
抓包查看js,能找到这个收藏数。

抓包收藏数 该get请求的参数只有brand_id,这个简单。不过在调试过程中无法正常获值,后来测试发现请求头中需要加上Referer,这样就正常了。
再来抓包分析商品数据,发现也是来自XHR类型页面,打开品牌页面时加载了3个XHR页面,第一个返回的是该品牌下的商品分类,第二个返回的主要是一些数字列表,第三个就是我们想要的商品数据。
抓包分析商品数据 不过这个get请求的参数有点多,变化的只有两个值productIdsr,其中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相关的知识才好。

上一篇下一篇

猜你喜欢

热点阅读