IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    一日一技:Scrapy如何发起假请求?

    青南发表于 2025-05-26 13:22:46
    love 0


    摄影:产品经理
    韩国章肥虾。

    在使用Scrapy的时候,我们可以通过在pipelines.py里面定义一些数据处理流程,让爬虫在爬到数据以后,先处理数据再储存。这本来是一个很好的功能,但容易被一些垃圾程序员拿来乱用。

    我看到过一些Scrapy爬虫项目,它的代码是这样写的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ...

    def start_requests(self):
    yield scrapy.Request('https://baidu.com')

    def parse(self, response):
    import pymongo
    handler = pymongo.MongoClient().xxdb.yycol
    rows = handler.find()
    for row in rows:
    yield row

    这种垃圾代码之所以会出现,是因为有一些垃圾程序员想偷懒,想复用Pipeline里面的代码,但又不想单独把它抽出来。于是他们没有皱褶的脑子一转,想到在Scrapy里面从数据库读取现成的数据,然后直接yield出来给Pipeline。但因为Scrapy必须在start_requests里面发起请求,不能直接yield数据,因此他们就想到先随便请求一个url,例如百度,等Scrapy的callback进入了parse方法以后,再去读取数据。

    虽然请求百度,不用担心反爬问题,响应大概率也是HTTP 200,肯定能进入parse,但这样写代码怎么看怎么蠢。

    有没有什么办法让代码看起来,即便蠢也蠢得高级一些呢?有,那就是发送假请求。让Scrapy看起来发起了HTTP请求,但实际上直接跳过。

    方法非常简单,就是把URL写成:data:,,注意末尾这个英文逗号不能省略。

    于是你的代码就会写成:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def start_requests(self):
    yield scrapy.Request('data:,')

    def parse(self, response):
    import pymongo
    handler = pymongo.MongoClient().xxdb.yycol
    rows = handler.find()
    for row in rows:
    yield row

    这样写以后,即使你没有外网访问权限也没问题,因为它不会真正发起请求,而是直接一晃而过,进入parse方法中。我把这种方法叫做发送假请求。

    这个方法还有另外一个应用场景。看下面这个代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def start_requests(self):
    while True:
    yield scrapy.Request('https://kingname.info/atom.xml', callback=self.parse, dont_filter=True)
    time.sleep(60)

    def parse(self, response):
    ...对rss接口返回的数据进行处理...
    for item in xxx['items']:
    url = row['url']
    yield scrapy.Request(url, callback=self.parse_detail)

    假如你需要让爬虫每分钟监控一个URL,你可能会像上面这样写代码。但由于Scrapy是基于Twisted实现的异步并发,因此time.sleep这种同步阻塞等待会把爬虫卡住,导致在sleep的时候,parse里面发起的子请求全都会被卡住,于是爬虫的并发数基本上等于1.

    可能有同学知道Scrapy支持asyncio,于是想这样写代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import asyncio


    async def start_requests(self):
    while True:
    yield scrapy.Request('https://kingname.info/atom.xml', callback=self.parse, dont_filter=True)
    asyncio.sleep(60)

    def parse(self, response):
    ...对rss接口返回的数据进行处理...
    for item in xxx['items']:
    url = row['url']
    yield scrapy.Request(url, callback=self.parse_detail)

    但这样写会报错,如下图所示:

    这个问题的原因就在于start_requests这个入口方法不能使用async来定义。他需要至少经过一次请求,进入任何一个callback以后,才能使用async来定义。

    这种情况下,也可以使用假请求来解决问题。我们可以把代码改为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def start_requests(self):
    yield scrapy.Request('data:,', callback=self.make_really_req)

    async def make_really_req(self, _):
    while True:
    yield scrapy.Request(url="https://kingname.com", callback=self.parse)
    await asyncio.sleep(60)

    def parse(self, response):
    print(response.text)

    这样一来,使用了asyncio.sleep,既能实现60秒请求一次,又不会阻塞子请求了。

    当然,最新版的Scrapy已经废弃了start_requests方法,改为start方法了,这个方法天生就是async方法,可以直接在里面asyncio.sleep,也就不会再有上面的问题了。不过如果你使用的还是老版本的Scrapy,上面这个假请求的方法还是有点用处。



沪ICP备19023445号-2号
友情链接