京东生鲜全品类爬虫--往期创作整理

目录

 

目标

爬取思路

网页加载流程

数据包获取

 JS逆向解析

抓包过程

关于并发和分布式

代码连接


 


目标

爬取京东到家的数据,京东到家没有反爬虫,只要速度不过分即可

品类:新鲜水果、海鲜水产、精选肉类、冷饮冻食、蔬菜蛋品

数量:每个大类 100+页 极限

单品信息:价格、会员价、来源(京东超市标签)、品名、规格(数量、重量)、特色标语(高档水果年货集市……)、评价数  量、特色标签(自营、放心购、京东检测…)

爬取思路

京东到家有五个分类,每个大分类点进去就能看到商品列表,商品列表分页显示,商品详细信息为动态数据。

首先从每个分类开始,每个分类的链接如下:

        self.url = {
            'Fruit': 'https://list.jd.com/list.html?cat=12218,12221&page=',
            'Fish': 'https://list.jd.com/list.html?cat=12218,12222&page=',
            'Meat': 'https://list.jd.com/list.html?cat=12218,13581&page=',
            'Frozen_snacks': 'https://list.jd.com/list.html?cat=12218,13591&page=',
            'Vegetable': 'https://list.jd.com/list.html?cat=12218,13553&page='
        }

相同分类的连接头部是相同的,即是上面代码部分,修改page参数即可完成url的生成,所以我们需要提前观察每个分类下有多少页数据带爬取。

由此,我们整理一下页数数据

        self.page = {
            'Fruit': 192,
            'Fish': 260,
            'Meat': 179,
            'Frozen_snacks':171,
            'Vegetable':259
        }

编写一个函数,生成商品列表的url

    def start_requests(self):
        for Key,Value in self.url.items():
            for i in range(self.page[Key]):
                url = Value + str(i+1)
                yield scrapy.Request(url=url, callback=self.parse)

到这里我们就有了url,随意一种方法获取即可,这里我使用了scrapy框架+redis插件,支持分布式爬取。

下面是parse函数,主要功能是通过selector选择商品列表页的名称、skuid、分类信息。

    def parse(self, response):
        productList = Selector(text=response.body).xpath('//li[contains(@class, "gl-item")]').extract()
        Class = Selector(text=response.body).xpath('//div[contains(@class, "s-title")]/h3').css('b::text').extract()[0]
        print(Class)
        for item in productList:
            name = Selector(text=item).xpath('//div[contains(@class, "p-name")]/a').css('em::text').extract()[0].strip()
            skuid = Selector(text=item).xpath('//div[contains(@class, "p-operate")]/a[1]/@data-sku').extract()[0]

            self.item['name'] = name
            self.item['skuid'] = skuid
            self.item['Class'] = Class
            yield self.item

网页加载流程

到上面为止,我们只是获得了简略信息,大部分信息是通过json动态加载的。

在接受到用户的请求后,首先发送过来的是html框架,这个框架没有详细信息,但是包含了请求详细信息所需要的商品编号skuid,之后如JS等脚本语言会使用skuid进行批量获取详细信息,以json形式发送过来。

数据包获取

耐心的查看网络监视器中的数据,最终找到和页面匹配的数据包。最终url格式如下:referenceIds也是skuid

urlPrice = 'https://p.3.cn/prices/mgets?callback=jQuery' + self.getParam() + '&ext=11101000&pin=&type=1&area=1_72_2799_0&skuIds='+str_J_
urlChatCount = 'https://club.jd.com/comment/productCommentSummaries.action?my=pinglun&referenceIds='+str2+'&callback=jQuery'+self.getParam()+'&_=1548229360349'
url_AD= 'https://ad.3.cn/ads/mgets?&callback=jQuery'+self.getParam()+'&my=list_adWords&source=JDList&skuids='+str_AD_

其中str_J_、str2、str_AD_带入skuid即可。

这里的self.getParam稍后再说,我们可以简单测试一下,看看是否可行

 JS逆向解析

前文说到的self.getParam是url中的一个参数,这个参数是callback,是动态加载的,在测试时你就会发现同一个数据包,每次请求时callback是不一样的,而同一个callback用久了就会无法访问,我们看callback的数据,前面是一个固定的jquery,后面是一串数字,那么这个请求既然是请求动态数据,那么就要到js中去寻找。

点击数据包,在堆栈跟踪中即可看到,它的数据由那些代码处理过,逐个查看。在js界面中使用ctrl+f搜索,内容为callback

可以看到,这里的callbackback是一个“jquery”字符和随机数拼接到一起的,这里我们可以用python代码来代替。

    def getParam(self):
        return str(math.floor(10000000*np.random.rand(1)))

同理,ext参数也可以这样查找,不过它是一个固定值。到此我们的准备工作基本完成。

 

抓包过程

爬虫分两个步骤进行,第一个步骤获取skuid及简略信息,第二步骤爬取详细信息

第一个步骤使用了scrapy框架和redis插件,scrapy和redis的使用这里不介绍,scrapy内置了并发爬取,在setting中设置即可,并发数量也与测试机器机器的cpu核数相关。scrapy-redis插件用来用来去重,访问过的url不会再次访问,第二个功能是进行持久化储存skuid,redis支持原子操作,在后续的爬取过程可以添加其他机器来加快速度,只不过不会再一个excel文件进行储存

init函数,r是要连接的redis,Workbook是用来操作excel的类

class JingDongDaoJia_2():

    def __init__(self):
        self.r = redis.Redis(host='127.0.0.1', port=6379,db=2)
        self.outwb = Workbook()
        self.wo = self.outwb.active

这个两个函数用来储存excel,careerSheet是excel中的sheet分页,每次appen就会添加一行数据

    def getCareerSheet(self):
        careerSheet = self.outwb.create_sheet('all', 0)
        careerSheet.append(['skuid', '名称','品类', '售卖价', '会员价', '好评数', '中评数', '差评数', '好评%','特色标语'])
        return careerSheet
    def SaveExcel(self):
        self.outwb.save("E:\DataAnalysis\\tools\python3\project\DistributedCrawler\JDDJ.xlsx")

获取价格函数,直接获取响应文件,使用正则表达式去除无用的地方,最后转为json,有时会因为某种原因导致获取失败,比如比分商品没有会员价,所以使用try except,如果请求频繁,就会返回无效数据,导致json置换过程报错,这时应休息一会,然后递归调用。

    def getPrice(self,url):
        print(url)
        list = []
        try:
            pattern = re.compile('\[.*]')
            response = urllib.request.urlopen(url)
            m = pattern.search(str(response.read()))
            value = json.loads(m.group())
            data1 = json.dumps(value,ensure_ascii=False)
            data2 = json.loads(data1)

            for item in data2:
                dict = {}
                price = item['p']
                dict['price'] = price
                try:
                    plus_p = item['tpp']
                    dict['plus_p'] = plus_p
                except:
                    dict['plus_p'] = '无'
                list.append(dict)
        except:
            print('请求频繁,再次尝试中')
            time.sleep(1)
            list = self.getPrice(url)
        return list

同理,获得好评和其他信息的代码如下

    def getChatCount(self,url):
        print(url)
        list = []
        try:
            pattern = re.compile('\[.*]')
            response = urllib.request.urlopen(url)
            m = pattern.search(str(response.read().decode('GBK')))
            value = json.loads(m.group())
            data1 = json.dumps(value)
            data2 = json.loads(data1)
            for item in data2:
                dict = {}
                try:
                    dict['GoodCount'] = item['GoodCount'] #好评
                except:
                    dict['GoodCount'] = '无'  # 好评
                try:
                    dict['GeneralCount'] = item['GeneralCount']  # 中评
                except:
                    dict['GeneralCount'] = '无'
                try:
                    dict['PoorCount'] = item['PoorCount']  # 差评
                except:
                    dict['PoorCount'] = '无'
                try:
                    dict['GoodRateShow'] = item['GoodRateShow']  # 分数
                except:
                    dict['GoodRateShow'] = '无'
                list.append(dict)
        except:
            print('请求频繁,再次尝试中')
            time.sleep(1)
            list = self.getChatCount(url)
        return list

    def getAD(self,url):
        print(url)
        list = []
        try:
            pattern = re.compile('\[.*]')
            response = urllib.request.urlopen(url)
            m = pattern.search(str(response.read().decode('utf-8')))
            value = json.loads(m.group())
            data1 = json.dumps(value,ensure_ascii=False)
            data2 = json.loads(data1)

            for item in data2:
                dict = {}
                dict['ad'] = item['ad']
                list.append(dict)
        except:
            print('请求频繁,再次尝试中')
            time.sleep(1)
            list = self.getAD(url)
        return list

最后上述方法同一再start_request中调用,每次爬取60个商品,60是每页最大数量,每次从redis中获取60个skuid,然后拼接到url字符串中,然后逐个获取对应的数据包,然后储存在careerSheet中。

    def start_requests(self):
        i = 0
        size = 60
        TF = True
        careerSheet = self.getCareerSheet()
        while TF:  #有数据时标记为true
            urlList = self.r.lrange("JDDJ_url:items", i*size, (i+1)*60)
            str_J_ = ''
            str2 = ''
            str_AD_ = ''
            if len(urlList) > 0:  #取到数据不跳出,取不到数据跳出。
                i = i + 1
                for item in urlList:
                    #name = eval(item)['name']
                    #Class = eval(item)['Class']
                    skuid = eval(item)['skuid']
                    str_J_ = str_J_+'J_'+skuid+'%2C'
                    str2 = str2 + skuid + '%2C'
                    str_AD_ = str_AD_ + 'AD_' + skuid + '%2C'
                time.sleep(1)
                urlPrice = 'https://p.3.cn/prices/mgets?callback=jQuery' + self.getParam() + '&ext=11101000&pin=&type=1&area=1_72_2799_0&skuIds='+str_J_
                urlChatCount = 'https://club.jd.com/comment/productCommentSummaries.action?my=pinglun&referenceIds='+str2+'&callback=jQuery'+self.getParam()+'&_=1548229360349'
                url_AD= 'https://ad.3.cn/ads/mgets?&callback=jQuery'+self.getParam()+'&my=list_adWords&source=JDList&skuids='+str_AD_

                listPrice = self.getPrice(urlPrice)
                listChatCount = self.getChatCount(urlChatCount)
                list_AD = self.getAD(url_AD)

                for index in range(len(urlList)):
                    try:
                        careerSheet.append([
                                            eval(urlList[index])['skuid'],
                                            eval(urlList[index])['name'],
                                            eval(urlList[index])['Class'],
                                            listPrice[index]['price'],
                                            listPrice[index]['plus_p'],
                                            listChatCount[index]['GoodCount'],
                                            listChatCount[index]['GeneralCount'],
                                            listChatCount[index]['PoorCount'],
                                            str(listChatCount[index]['GoodRateShow'])+'%',
                                            list_AD[index]['ad']
                        ])
                    except:
                        print('数据不足')
                print('第' + str(i * 60) + '条爬取成功')
            else:
                print('超出数据库范围')
                TF = False

关于并发和分布式

前文提到爬虫分为两个阶段,第一阶段使用了scrapy框架,开启并发在setting文件中做如下设置即可。博主是4核cpu,所以理论应该有8个并发程序。第一阶段由于在代码中生成的url,所以没办法使用分布式,如果想使用的话,要提前生成url,然后储存在redis中即可,然后再从redis中获取。

# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 配置Scrapy执行的最大并发请求(默认值:16)
CONCURRENT_REQUESTS = 2

而二阶段,如果想分布式爬取只需要在另一台机器执行py脚本即可,需要修改redis的位置,最后的储存是在自己的excel中。

如果想并发爬取,只需要创建线程队列,将start_requests函数放在线程中执行即可,不过这样就不好使用excel储存了。

京东并没有反爬虫,珍惜自己的IP,和平爬取。

代码连接

https://github.com/GuoHongYuan/DistributedCrawler

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海人001

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值