目录

 

目标

爬取思路

网页加载流程

数据包获取

 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'] = nameself.item['skuid'] = skuidself.item['Class'] = Classyield 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 careerSheetdef 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'] = pricetry:plus_p = item['tpp']dict['plus_p'] = plus_pexcept: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 listdef 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 = 0size = 60TF = TruecareerSheet = self.getCareerSheet()while TF:  #有数据时标记为trueurlList = self.r.lrange("JDDJ_url:items", i*size, (i+1)*60)str_J_ = ''str2 = ''str_AD_ = ''if len(urlList) > 0:  #取到数据不跳出,取不到数据跳出。i = i + 1for 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