scrapy-redis发送POST请求源码分析与实现
本文深入分析scrapy-redis框架源码,指出其默认仅支持GET请求的限制。通过重写RedisSpider中的make_request_from_data方法,结合FormRequest构造包含url、form_data和meta的POST请求,并演示如何将数据存入Redis以实现分布式POST抓取。
引言
1 引言
近期在研究美团数据抓取项目,采用了scrapy-redis分布式爬虫框架。然而,scrapy-redis与原生scrapy存在差异,其默认机制仅支持发送GET请求,无法直接发起POST请求。由于美团的数据接口主要采用POST方式,网络上关于scrapy-redis发送POST请求的资料极少,因此只能通过分析源码自行解决。
美团POST需求说明
2 美团POST需求说明
首先明确需求,即美团POST请求的具体形式。以获取特定地理坐标下的所有店铺类别列表为例,我们需要构造一个包含经纬度等位置信息的表单数据(form_data),同时为了向下一层解析方法(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方法传递额外数据,可构造该数据并作为参数传入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中重写start_requests方法即可发送POST,但scrapy-redis不同,它仅从配置的Redis数据库读取起始url,因此即使重写start_requests也无效。我们需要查看源码寻找解决方案。
scrapy-redis与scrapy的主要区别在于,它不再继承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继承了RedisMixin类,推测其功能多由父类提供(即“拼爹”)。继续查看RedisMixin类源码。由于源码较长,此处不全部列出,但惊喜地发现RedisMixin中确实存在类似start_requests、next_requests、make_request_from_data等方法。有scrapy经验者应知start_requests是构造请求的起点。经源码注释及后续断点验证,scrapy-redis确实沿用了这一机制。从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 总结
没有通读源码解决不了的问题,如果有,就再读一遍!
以上为本文全部内容,希望本文对大家的scrapy-redis学习或工作具有参考价值,感谢支持。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









