scrapy-redis发送POST请求源码分析与实现

小鹿同学
小鹿同学
发布时间:2026-10-13| 2人浏览过
原创

本文深入分析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学习或工作具有参考价值,感谢支持。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。