scrapy-redis源码分析之发送POST请求详解


Posted in Python onMay 15, 2019

1 引言

这段时间在研究美团爬虫,用的是scrapy-redis分布式爬虫框架,奈何scrapy-redis与scrapy框架不同,默认只发送GET请求,换句话说,不能直接发送POST请求,而美团的数据请求方式是POST,网上找了一圈,发现关于scrapy-redis发送POST的资料寥寥无几,只能自己刚源码了。

2 美团POST需求说明

先来说一说需求,也就是说美团POST请求形式。我们以获取某个地理坐标下,所有店铺类别列表请求为例。获取所有店铺类别列表时,我们需要构造一个包含位置坐标经纬度等信息的表单数据,以及为了向下一层parse方法传递的一些必要数据,即meta,然后发起一个POST请求。

url:

请求地址,即url是固定的,如下所示:

url = 'http://i.waimai.meituan.com/openh5/poi/filterconditions?_=1557367197922'

url最后面的13位数字是时间戳,实际应用时用time模块生成一下就好了。

表单数据:

form_data = {
 'initialLat': '25.618626',
 'initialLng': '105.644569',
 'actualLat': '25.618626',
 'actualLng': '105.644569',
 'geoType': '2',
 'wm_latitude': '25618626',
 'wm_longitude': '105644569',
 'wm_actual_latitude': '25618626',
 'wm_actual_longitude': '105644569'
}

meta数据:

meta数据不是必须的,但是,如果你在发送请求时,有一些数据需要向下一层parse方法(解析爬虫返回的response的方法)中传递的话,就可以构造这一数据,然后作为参数传递进request中。

meta = {
 'lat': form_data.get('initialLat'),
 'lng': form_data.get('initialLng'),
 'lat2': form_data.get('wm_latitude'),
 'lng2': form_data.get('wm_longitude'),
 'province': '**省',
 'city': '**市',
 'area': '**区'
}

3 源码分析

采集店铺类别列表时需要发送怎样一个POST请求在上面已经说明了,那么,在scrapy-redis框架中,这个POST该如何来发送呢?我相信,打开我这篇博文的读者都是用过scrapy的,用scrapy发送POST肯定没问题(重写start_requests方法即可),但scrapy-redis不同,scrapy-redis框架只会从配置好的redis数据库中读取起始url,所以,在scrapy-redis中,就算重写start_requests方法也没用。怎么办呢?我们看看源码。

我们知道,scrapy-redis与scrapy的一个很大区别就是,scrapy-redis不再继承Spider类,而是继承RedisSpider类的,所以,RedisSpider类源码将是我们分析的重点,我们打开RedisSpider类,看看有没有类似于scrapy框架中的start_requests、make_requests_from_url这样的方法。RedisSpider源码如下:

class RedisSpider(RedisMixin, Spider):
 @classmethod
 def from_crawler(self, crawler, *args, **kwargs):
 obj = super(RedisSpider, self).from_crawler(crawler, *args, **kwargs)
 obj.setup_redis(crawler)
 return obj

很遗憾,在RedisSpider类中没有找到类似start_requests、make_requests_from_url这样的方法,而且,RedisSpider的源码也太少了吧,不过,从第一行我们可以发现RedisSpider继承了RedisMinxin这个类,所以我猜RedisSpider的很多功能是从父类继承而来的(拼爹的RedisSpider)。继续查看RedisMinxin类源码。RedisMinxin类源码太多,这里就不将所有源码贴出来了,不过,惊喜的是,在RedisMinxin中,真找到了类似于start_requests、make_requests_from_url这样的方法,如:start_requests、next_requests、make_request_from_data等。有过scrapy使用经验的童鞋应该都知道,start_requests方法可以说是构造一切请求的起源,没分析scrapy-redis源码之前,谁也不知道scrapy-redis是不是和scrapy一样(后面打断点的方式验证过,确实一样,话说这个验证有点多余,因为源码注释就是这么说的),不过,还是从start_requests开始分析吧。start_requests源码如下:

def start_requests(self):
 return self.next_requests()

呵,真简洁,直接把所有任务丢给next_requests方法,继续:

def next_requests(self):
 """Returns a request to be scheduled or none."""
 use_set = self.settings.getbool('REDIS_START_URLS_AS_SET',    defaults.START_URLS_AS_SET)
 fetch_one = self.server.spop if use_set else self.server.lpop
 # XXX: Do we need to use a timeout here?
 found = 0
 # TODO: Use redis pipeline execution.
 while found < self.redis_batch_size: # 每次读取的量
  data = fetch_one(self.redis_key) # 从redis中读取一条记录
  if not data:
   # Queue empty.
   break
  req = self.make_request_from_data(data) # 根据从redis中读取的记录,实例化一个request
  if req:
   yield req
  found += 1
  else:
   self.logger.debug("Request not made from data: %r", data)
 
 if found:
  self.logger.debug("Read %s requests from '%s'", found, self.redis_key)

上面next_requests方法中,关键的就是那个while循环,每一次循环都调用了一个make_request_from_data方法,从函数名可以函数,这个方法就是根据从redis中读取从来的数据,实例化一个request,那不就是我们要找的方法吗?进入make_request_from_data方法一探究竟:

def make_request_from_data(self, data):
 url = bytes_to_str(data, self.redis_encoding)
 return self.make_requests_from_url(url) # 这是重点,圈起来,要考

因为scrapy-redis默认值发送GET请求,所以,在这个make_request_from_data方法中认为data只包含一个url,但如果我们要发送POST请求,这个data包含的东西可就多了,我们上面美团POST请求说明中就说到,至少要包含url、form_data。所以,如果我们要发送POST请求,这里必须改,make_request_from_data方法最后调用的make_requests_from_url是scrapy中的Spider中的方法,不过,我们也不需要继续往下看下去了,我想诸位都也清楚了,要发送POST请求,重写这个make_request_from_data方法,根据传入的data,实例化一个request返回就好了。

 4 代码实例

明白上面这些东西后,就可以开始写代码了。修改源码吗?不,不存在的,改源码可不是好习惯。我们直接在我们自己的Spider类中重写make_request_from_data方法就好了:

from scrapy import FormRequest
from scrapy_redis.spiders import RedisSpider
 
 
class MeituanSpider(RedisSpider):
 """
 此处省略若干行
 """
 
 def make_request_from_data(self, data):
  """
  重写make_request_from_data方法,data是scrapy-redis读取redis中的[url,form_data,meta],然后发送post请求
  :param data: redis中都去的请求数据,是一个list
  :return: 一个FormRequest对象
 """
  data = json.loads(data)
  url = data.get('url')
  form_data = data.get('form_data')
  meta = data.get('meta')
  return FormRequest(url=url, formdata=form_data, meta=meta, callback=self.parse)

 def parse(self, response):
  pass

搞清楚原理之后,就是这么简单。万事俱备,只欠东风——将url,form_data,meta存储到redis中。另外新建一个模块实现这一部分功能:

def push_start_url_data(request_data):
 """
 将一个完整的request_data推送到redis的start_url列表中
 :param request_data: {'url':url, 'form_data':form_data, 'meta':meta}
 :return:
 """
 r.lpush('meituan:start_urls', request_data)
 
 
if __name__ == '__main__':
 url = 'http://i.waimai.meituan.com/openh5/poi/filterconditions?_=1557367197922'
 form_data = {
  'initialLat': '25.618626',
  'initialLng': '105.644569',
  'actualLat': '25.618626',
  'actualLng': '105.644569',
  'geoType': '2',
  'wm_latitude': '25618626',
  'wm_longitude': '105644569',
  'wm_actual_latitude': '25618626',
  'wm_actual_longitude': '105644569'
 }
 meta = {
  'lat': form_data.get('initialLat'),
  'lng': form_data.get('initialLng'),
  'lat2': form_data.get('wm_latitude'),
  'lng2': form_data.get('wm_longitude'),
  'province': '**省',
  'city': '*市',
  'area': '**区'
 }
 request_data = {
  'url': url,
  'form_data': form_data,
  'meta': meta
 }
 push_start_url_data(json.dumps(request_data))

在启动scrapy-redis之前,运行一下这一模块即可。如果有很多POI(地理位置兴趣点),循环遍历每一个POI,生成request_data,push到redis中。这一循环功能就你自己写吧。

5 总结

没有什么是撸一遍源码解决不了的,如果有,就再撸一遍!

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Python 相关文章推荐
Python读写Excel文件的实例
Nov 01 Python
Python中的lstrip()方法使用简介
May 19 Python
Python实现文件信息进行合并实例代码
Jan 17 Python
Python 使用 Pillow 模块给图片添加文字水印的方法
Aug 30 Python
python实现简单银行管理系统
Oct 25 Python
Python figure参数及subplot子图绘制代码
Apr 18 Python
Python中实现输入一个整数的案例
May 03 Python
Pytorch学习之torch用法----比较操作(Comparison Ops)
Jun 28 Python
利用python进行文件操作
Dec 04 Python
Pytorch 使用tensor特定条件判断索引
Apr 08 Python
浅谈哪个Python库才最适合做数据可视化
Jun 28 Python
Python爬虫中urllib3与urllib的区别是什么
Jul 21 Python
windows系统中Python多版本与jupyter notebook使用虚拟环境的过程
May 15 #Python
使用Python做定时任务及时了解互联网动态
May 15 #Python
Python使用统计函数绘制简单图形实例代码
May 15 #Python
详解Python3 对象组合zip()和回退方式*zip
May 15 #Python
python语言元素知识点详解
May 15 #Python
django admin后台添加导出excel功能示例代码
May 15 #Python
Python中单线程、多线程和多进程的效率对比实验实例
May 14 #Python
You might like
《雄兵连》系列首部大电影《烈阳天道》:可能是因为期望值太高了
2020/08/18 国漫
php win下Socket方式发邮件类
2009/08/21 PHP
PHP数据分析引擎计算余弦相似度算法示例
2017/08/08 PHP
jQuery Ajax调用WCF服务详细教程
2015/03/31 Javascript
一些实用性较高的js方法
2016/04/19 Javascript
Angularjs过滤器使用详解
2016/05/25 Javascript
javascript鼠标滑过显示二级菜单特效
2020/11/18 Javascript
基于js对象,操作属性、方法详解
2016/08/11 Javascript
Vue2实现组件props双向绑定
2016/12/02 Javascript
原生的强大DOM选择器querySelector介绍
2016/12/21 Javascript
javascript 网页进度条简单实例
2017/02/22 Javascript
Bootstrap modal 多弹窗之叠加关闭阴影遮罩问题的解决方法
2017/02/27 Javascript
JS全角与半角转化实例(分享)
2017/07/04 Javascript
封装运动框架实战左右与上下滑动的焦点轮播图(实例)
2017/10/17 Javascript
jQuery实现点击DIV同时点击CheckBox,并为DIV上背景色的实例
2017/12/18 jQuery
vue组件实现弹出框点击显示隐藏效果
2020/10/26 Javascript
JavaScript 中的12种循环遍历方法【总结】
2018/05/31 Javascript
Nodejs异步流程框架async的方法
2019/06/07 NodeJs
[01:10:16]DOTA2上海特级锦标赛B组资格赛#2 Fnatic VS Spirit第一局
2016/02/27 DOTA
Python实现二分查找算法实例
2015/05/26 Python
Python数组遍历的简单实现方法小结
2016/04/27 Python
使用TensorFlow-Slim进行图像分类的实现
2019/12/31 Python
如何使用python自带IDLE的几种方法
2020/10/10 Python
python tkinter实现连连看游戏
2020/11/16 Python
python 用opencv实现图像修复和图像金字塔
2020/11/27 Python
使用css3 属性如何丰富图片样式(圆角 阴影 渐变)
2012/11/22 HTML / CSS
解释一下ArrayList Vector和LinkedList的实现和区别
2013/04/26 面试题
快递业务员岗位职责
2014/01/06 职场文书
运动会广播稿400字
2014/01/25 职场文书
高中教师考核方案
2014/05/18 职场文书
2014年财政所工作总结
2014/11/22 职场文书
小学生通知书评语
2014/12/31 职场文书
初三英语教学计划
2015/01/23 职场文书
暂住证明怎么写
2015/06/19 职场文书
入党宣誓仪式主持词
2015/06/29 职场文书
基于Redis位图实现用户签到功能
2021/05/08 Redis