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代理抓取并验证使用多线程实现
May 03 Python
python实现分析apache和nginx日志文件并输出访客ip列表的方法
Apr 04 Python
python实现数独算法实例
Jun 09 Python
Python通过RabbitMQ服务器实现交换机功能的实例教程
Jun 29 Python
python中文乱码不着急,先看懂字节和字符
Dec 20 Python
Pycharm连接远程服务器并实现远程调试的实现
Aug 02 Python
python redis连接 有序集合去重的代码
Aug 04 Python
Python generator生成器和yield表达式详解
Aug 08 Python
Numpy 中的矩阵求逆实例
Aug 26 Python
Python的条件锁与事件共享详解
Sep 12 Python
python 实现 hive中类似 lateral view explode的功能示例
May 18 Python
聊一聊python常用的编程模块
May 14 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
多数据表共用一个页的新闻发布
2006/10/09 PHP
PHP使用new StdClass()创建空对象的方法分析
2017/06/06 PHP
Document 对象的常用方法
2009/07/31 Javascript
在vs2010中调试javascript代码方法
2011/02/11 Javascript
关于全局变量和局部变量的那些事
2013/01/11 Javascript
jquery 按键盘上的enter事件
2014/05/11 Javascript
fckeditor粘贴Word时弹出窗口取消的方法
2014/10/30 Javascript
jquery实现增加删除行的方法
2015/02/03 Javascript
javascript创建函数的20种方式汇总
2015/06/23 Javascript
纯javascript响应式树形菜单效果
2015/11/10 Javascript
JavaScript原生xmlHttp与jquery的ajax方法json数据格式实例
2015/12/04 Javascript
jQuery操作属性和样式详解
2016/04/13 Javascript
Vue.js 表单校验插件
2016/08/14 Javascript
jQuery如何封装输入框插件
2016/08/19 Javascript
js上传图片预览的实现方法
2017/05/09 Javascript
vue单页缓存方案分析及实现
2018/09/25 Javascript
基于JavaScript实现单例模式
2019/10/30 Javascript
[01:13]2015国际邀请赛线下观战现场
2015/08/08 DOTA
[03:36]DOTA2完美大师赛coL战队趣味视频——我演你猜
2017/11/23 DOTA
从零学Python之入门(二)基本数据类型
2014/05/25 Python
零基础写python爬虫之urllib2中的两个重要概念:Openers和Handlers
2014/11/05 Python
python3序列化与反序列化用法实例
2015/05/26 Python
python解决方案:WindowsError: [Error 2]
2016/08/28 Python
基于Python中单例模式的几种实现方式及优化详解
2018/01/09 Python
对Python 两大环境管理神器 pyenv 和 virtualenv详解
2018/12/31 Python
python,Django实现的淘宝客登录功能示例
2019/06/12 Python
使用Python实现正态分布、正态分布采样
2019/11/20 Python
浅析Python requests 模块
2020/10/09 Python
德国婴儿推车和儿童安全座椅商店:BABYSHOP
2016/09/01 全球购物
印度尼西亚电子产品购物网站:Kliknklik
2018/06/05 全球购物
有针对性的求职自荐信
2013/11/14 职场文书
股份合作协议书
2014/04/12 职场文书
《红军不怕远征难》教学反思
2014/04/14 职场文书
教师听课学习心得体会
2016/01/15 职场文书
MySQL中int (10) 和 int (11) 的区别
2022/01/22 MySQL
python 使用tkinter与messagebox写界面和弹窗
2022/03/20 Python