目录
目标
爬取思路
网页加载流程
数据包获取
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