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 相关文章推荐
centos系统升级python 2.7.3
Jul 03 Python
Python网络爬虫实例讲解
Apr 28 Python
Pytorch入门之mnist分类实例
Apr 14 Python
python匹配两个短语之间的字符实例
Dec 25 Python
Python之lambda匿名函数及map和filter的用法
Mar 05 Python
使用Django开发简单接口实现文章增删改查
May 09 Python
利用Python实现Shp格式向GeoJSON的转换方法
Jul 09 Python
Python字符串大小写转换拼接删除空白
Sep 19 Python
完美解决pycharm导入自己写的py文件爆红问题
Feb 12 Python
用什么库写 Python 命令行程序(示例代码详解)
Feb 20 Python
django ORM之values和annotate使用详解
May 19 Python
解决Alexnet训练模型在每个epoch中准确率和loss都会一升一降问题
Jun 17 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
php 验证码制作(网树注释思想)
2009/07/20 PHP
Php 构造函数construct的前下划线是双的_
2009/12/08 PHP
打造超酷的PHP数据饼图效果实现代码
2011/11/23 PHP
php中++i 与 i++ 的区别
2012/08/08 PHP
YII框架中搜索分页jQuery写法详解
2016/12/19 PHP
thinkPHP5.0框架引入Traits功能实例分析
2017/03/18 PHP
PHP框架自动加载类文件原理详解
2017/06/06 PHP
js同时按下两个方向键
2007/12/01 Javascript
jquery ui dialog里调用datepicker的问题
2009/08/06 Javascript
JS实现倒计时和文字滚动的效果实例
2014/10/29 Javascript
jQuery实现右侧显示可向左滑动展示的深色QQ客服效果代码
2015/10/23 Javascript
JS实现超简单的鼠标拖动效果
2015/11/02 Javascript
Bootstrap树形控件使用方法详解
2016/01/27 Javascript
Bootstrap自定义文件上传下载样式
2016/05/26 Javascript
Vue.js每天必学之数据双向绑定
2016/09/05 Javascript
JavaScript BASE64算法实现(完美解决中文乱码)
2017/01/10 Javascript
jQuery实现的浮动层div浏览器居中显示效果
2017/02/03 Javascript
在vue.js中抽出公共代码的方法示例
2017/06/08 Javascript
ionic环境配置及问题详解
2017/06/27 Javascript
使用JavaScript实现链表的数据结构的代码
2017/08/02 Javascript
利用Node.js了解与测量HTTP所花费的时间详解
2017/09/22 Javascript
vue vuex vue-rouert后台项目——权限路由(适合初学)
2017/12/29 Javascript
原生JS实现动态加载js文件并在加载成功后执行回调函数的方法
2020/12/30 Javascript
Vue请求JSON Server服务器数据的实现方法
2018/11/02 Javascript
Vue触发式全局组件构建的方法
2018/11/28 Javascript
解决layui表格内文本超出隐藏的问题
2019/09/12 Javascript
基于JavaScript实现单例模式
2019/10/30 Javascript
python进阶教程之函数参数的多种传递方法
2014/08/30 Python
Windows下实现Python2和Python3两个版共存的方法
2015/06/12 Python
Java编程迭代地删除文件夹及其下的所有文件实例
2018/02/10 Python
利用PyCharm Profile分析异步爬虫效率详解
2019/05/08 Python
Python3 列表,数组,矩阵的相互转换的方法示例
2019/08/05 Python
使用Python代码实现Linux中的ls遍历目录命令的实例代码
2019/09/07 Python
移动端rem布局的两种实现方法
2018/01/03 HTML / CSS
extern在函数声明中是什么意思
2014/01/19 面试题
军训个人总结
2015/03/03 职场文书