把 Scrapy 接入脚本、应用和 Notebook:事件循环、多爬虫与运行边界

原文:Common Practices;作者/维护方:Scrapy developers。中文翻译与技术整理:未完纪。核验日期:2026-10-05。

除了在命令行运行 scrapy crawl,Scrapy 也能作为库嵌入脚本、Web 应用或 Notebook。决定接入方式之前,先弄清楚当前进程有没有运行事件循环,以及谁负责启动、停止和日志配置。

本文对应 Scrapy 2.19.0 的 Common Practices。本页使用的 Async API 与无 Twisted 模式不应直接套到旧版本;后者在原文中仍标为实验性。代码里的 ... 表示原文省略的爬虫定义,不能把这些骨架当成完整可运行项目。

独立脚本:让 Process 管理生命周期

Scrapy 需要 Twisted reactor,或者在 TWISTED_REACTOR_ENABLED=False 时使用 asyncio 事件循环。AsyncCrawlerProcess 与 CrawlerProcess 会协助启动 reactor、配置日志并设置关闭处理器;Scrapy 命令也使用这类工具。

两者的主要差异在异步接口风格:Async 版本的异步方法返回协程,传统版本返回 Twisted Deferred。下面是原文的单爬虫脚本骨架,结果按 JSON 导出到 items.json;start() 会阻塞到抓取完成:

import scrapy
from scrapy.crawler import AsyncCrawlerProcess


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


process = AsyncCrawlerProcess(
    settings={
        "FEEDS": {
            "items.json": {"format": "json"},
        },
    }
)

process.crawl(MySpider)
process.start()  # the script will block here until the crawling is finished

通过构造函数的 settings 字典设置配置。若代码处于 Scrapy 项目中,可用 get_project_settings() 取得项目设置,并直接传爬虫名称。原文以下例使用 testspiders 项目中的 followall:

from scrapy.crawler import AsyncCrawlerProcess
from scrapy.utils.project import get_project_settings

process = AsyncCrawlerProcess(get_project_settings())

# 'followall' is the name of one of the spiders of the project.
process.crawl("followall", domain="scrapy.org")
process.start()  # the script will block here until the crawling is finished

编辑补充:读取项目设置也会带入项目配置的扩展、代理、输出和目标站点。运行前应确认它们指向预期资源;不要把“只调用两行 API”理解成没有外部副作用。

Scrapy集成选择示意:独立脚本使用Process,已有Twisted或asyncio应用使用Runner复用事件循环,多爬虫选择顺序await或并行join。
Process 与 Runner 的核心区别是事件循环生命周期由谁管理。(未完纪原创示意图,非截图或测试结果。)

已有 reactor:Runner 只调度,不接管循环

AsyncCrawlerRunner 与 CrawlerRunner 是较薄的调度封装,提供运行多个 crawler 的辅助方法,不主动启动或干预已有 reactor。若应用已经使用 Twisted,通常选择 Runner 复用它。

调度爬虫后,需要由应用显式运行 reactor。AsyncRunner 的 crawl() 对应任务完成后,或 CrawlerRunner 返回的 Deferred 触发后,再执行后续逻辑或关闭 reactor。简单脚本也可借助 twisted.internet.task.react() 管理启动和停止:

import scrapy
from scrapy.crawler import AsyncCrawlerRunner
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.log import configure_logging
from scrapy.utils.reactor import install_reactor
from twisted.internet.task import react


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


async def crawl(_):
    configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
    runner = AsyncCrawlerRunner()
    await runner.crawl(MySpider)  # completes when the spider finishes


install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
react(deferred_f_from_coro_f(crawl))

上例安装 AsyncioSelectorReactor,用 deferred_f_from_coro_f 把协程函数适配给 Twisted。下面保留原文的传统 Runner 与 EPollReactor 对照;AsyncRunner 的 Twisted 模式只支持前者:

import scrapy
from scrapy.crawler import CrawlerRunner
from scrapy.utils.log import configure_logging
from scrapy.utils.reactor import install_reactor
from twisted.internet.task import react


class MySpider(scrapy.Spider):
    custom_settings = {
        "TWISTED_REACTOR": "twisted.internet.epollreactor.EPollReactor",
    }
    # Your spider definition
    ...


def crawl(_):
    configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
    runner = CrawlerRunner()
    d = runner.crawl(MySpider)
    return d  # this Deferred fires when the spider finishes


install_reactor("twisted.internet.epollreactor.EPollReactor")
react(crawl)

reactor 的选择具有进程级影响,不能在已经安装了不同 reactor 后随意切换。EPollReactor 还具有平台适用范围,不是 Windows 通用选择。已有循环的应用应遵循其初始化顺序。

不启用 Twisted 的三种脚本形式

将 TWISTED_REACTOR_ENABLED 设为 False,可以让 AsyncCrawlerProcess 使用 asyncio:

import scrapy
from scrapy.crawler import AsyncCrawlerProcess


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


process = AsyncCrawlerProcess(
    settings={
        "TWISTED_REACTOR_ENABLED": False,
    }
)

process.crawl(MySpider)
process.start()  # the script will block here until the crawling is finished

这一模式下,同一进程可以先后创建多个 AsyncCrawlerProcess,每次启动、完成并停止自己的循环。原文的例子如下:

import scrapy
from scrapy.crawler import AsyncCrawlerProcess


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


process1 = AsyncCrawlerProcess(
    settings={
        "TWISTED_REACTOR_ENABLED": False,
    }
)
process1.crawl(MySpider)
process1.start()

process2 = AsyncCrawlerProcess(
    settings={
        "TWISTED_REACTOR_ENABLED": False,
    }
)
process2.crawl(MySpider)
process2.start()

也可以让 asyncio.run() 管理最外层循环,在里面使用 AsyncCrawlerRunner:

import asyncio

import scrapy
from scrapy.crawler import AsyncCrawlerRunner
from scrapy.utils.log import configure_logging


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


async def main():
    configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
    runner = AsyncCrawlerRunner(settings={"TWISTED_REACTOR_ENABLED": False})
    await runner.crawl(MySpider)  # completes when the spider finishes


asyncio.run(main())

这仍属于实验性能力,应阅读 Scrapy asyncio 与无 Twisted 模式的限制。如果当前线程已经有运行中的 asyncio 循环,例如 Notebook 或 ASGI 应用,就不应照搬最外层 asyncio.run();应直接等待已有循环中的协程。

接入现有 Web 应用

简单应用若可以同步等待任务,或者任务队列每个任务都启动独立进程,可沿用独立脚本方案。异步应用需要进一步区分循环的归属。

已有 Twisted reactor 时使用 Runner。若应用既没有 reactor 也没有 asyncio 循环,例如原文中的 Django WSGI 部署,可用禁用 Twisted 的 AsyncCrawlerProcess,为每次调用启动和停止 asyncio 循环:

import scrapy
from django.http import HttpResponse
from scrapy.crawler import AsyncCrawlerProcess


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


def crawl_view(request):
    process = AsyncCrawlerProcess(settings={"TWISTED_REACTOR_ENABLED": False})
    process.crawl(MySpider)
    process.start()  # returns when the spider finishes
    return HttpResponse("Crawling finished")

若应用已经有 asyncio 循环,例如 Django 运行在 uvicorn 等 ASGI 服务器上,则让 AsyncCrawlerRunner 复用现有循环:

import scrapy
from django.http import HttpResponse
from scrapy.crawler import AsyncCrawlerRunner


class MySpider(scrapy.Spider):
    # Your spider definition
    ...


async def crawl_view(request):
    runner = AsyncCrawlerRunner(settings={"TWISTED_REACTOR_ENABLED": False})
    await runner.crawl(MySpider)  # completes when the spider finishes
    return HttpResponse("Crawling finished")

两段原例都让请求等待爬虫完成:同步版会占住对应执行资源,异步版虽然以等待方式调度,但用户请求仍然没结束。编辑补充:长时间抓取通常应安排在有超时、取消和状态查询能力的后台任务中;不能把面向用户的任意 URL 直接交给爬虫,否则还需处理访问内网地址等输入风险。本稿没有新增这样的接口。

在 Jupyter Notebook 中反复运行

Notebook 内核已经提供事件循环,因此使用 AsyncCrawlerRunner 并关闭 Twisted。Runner 不自动配置日志,如果希望在单元格中看到日志,应调用 configure_logging()。原文的完整例子支持作为单个或多个单元格重复运行:

from scrapy import Spider
from scrapy.crawler import AsyncCrawlerRunner
from scrapy.utils.log import configure_logging

configure_logging()


class BooksSpider(Spider):
    name = "books"
    start_urls = ["https://books.toscrape.com"]

    def parse(self, response):
        for book in response.css("h3"):
            yield {"title": book.css("a::attr(title)").get()}


runner = AsyncCrawlerRunner({"TWISTED_REACTOR_ENABLED": False})
await runner.crawl(BooksSpider)

这个例子使用公开练习站点 books.toscrape.com,读取书名并产出字段;本文没有发送抓取请求。真实项目应依据目标站许可控制范围、频率和结果保存位置,无 Twisted 的实验性限制在 Notebook 中同样存在。

同一进程运行多个爬虫

scrapy crawl 默认一个进程运行一个爬虫,但内部 API 支持多个。每次 crawl() 都创建自己的 Crawler、下载器与爬虫中间件实例,以及解析后的设置;这些实例不会因同进程而自动共享。

同时调度两个爬虫,再由 Process 一起等待完成:

import scrapy
from scrapy.crawler import AsyncCrawlerProcess
from scrapy.utils.project import get_project_settings


class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...


class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...


settings = get_project_settings()
process = AsyncCrawlerProcess(settings)
process.crawl(MySpider1)
process.crawl(MySpider2)
process.start()  # the script will block here until all crawling jobs are finished

使用 AsyncRunner 时,先调度各个爬虫,再 await runner.join() 等待全部完成:

import scrapy
from scrapy.crawler import AsyncCrawlerRunner
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.log import configure_logging
from scrapy.utils.reactor import install_reactor
from twisted.internet.task import react


class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...


class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...


async def crawl(_):
    configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
    runner = AsyncCrawlerRunner()
    runner.crawl(MySpider1)
    runner.crawl(MySpider2)
    await runner.join()  # completes when both spiders finish


install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
react(deferred_f_from_coro_f(crawl))

如果任务有先后关系,则在启动下一个之前等待前一个完成:

import scrapy
from scrapy.crawler import AsyncCrawlerRunner
from scrapy.utils.defer import deferred_f_from_coro_f
from scrapy.utils.log import configure_logging
from scrapy.utils.reactor import install_reactor
from twisted.internet.task import react


class MySpider1(scrapy.Spider):
    # Your first spider definition
    ...


class MySpider2(scrapy.Spider):
    # Your second spider definition
    ...


async def crawl(_):
    configure_logging({"LOG_FORMAT": "%(levelname)s: %(message)s"})
    runner = AsyncCrawlerRunner()
    await runner.crawl(MySpider1)
    await runner.crawl(MySpider2)


install_reactor("twisted.internet.asyncioreactor.AsyncioSelectorReactor")
react(deferred_f_from_coro_f(crawl))

同进程多个爬虫的日志和 reactor 设置不能各自矛盾,pre-crawler 设置也不能按爬虫单独定义。其他大部分设置则按 crawler 各自生效,包括 CONCURRENT_REQUESTS、CONCURRENT_REQUESTS_PER_DOMAIN、DOWNLOAD_DELAY 和 AutoThrottle。

译注修正:原文笼统建议同时抓取时把这些值除以 crawler 数量,以维持总负载。这对并发上限可作为粗略分配思路,但不能机械套到延迟:把 DOWNLOAD_DELAY 除小会加快请求,反而提高总请求速率。要降低合计速率,应让每个 crawler 更慢,或使用统一的总限速机制,并观测目标站的总负载。异步请求和随机延迟使“并发除法”本身也只是近似,不能当严格限流证明。

重复启动同一个爬虫会把独立限制叠加,并不等于自动得到协调后的抓取容量。原文建议若目标只是让单个爬虫更快,优先在允许的负载范围内调整一个 crawler 的并发,而非通过重复实例绕开约束。

跨服务器分配任务

Scrapy 没有内置的通用多服务器分布式抓取设施。多爬虫场景可以部署多个 Scrapyd 实例,把不同运行任务分配给它们。单个大爬虫则通常先划分待抓 URL,再通过参数让各实例读取不同分片。

原文示意先准备三个清单:

http://somedomain.com/urls-to-crawl/spider1/part1.list
http://somedomain.com/urls-to-crawl/spider1/part2.list
http://somedomain.com/urls-to-crawl/spider1/part3.list

然后在三台 Scrapyd 服务器上分别调度,把 part 传给爬虫:

curl http://scrapy1.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=1
curl http://scrapy2.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=2
curl http://scrapy3.mycompany.com:6800/schedule.json -d project=myproject -d spider=spider1 -d part=3

这些是原文内部主机名和未加密 HTTP 的概念示例,命令会真正启动抓取。实际部署需要对调度端点实施身份认证、访问限制和适当的传输保护,并设计分片去重、失败恢复及结果合并;本文不把它们当作可直接对公网开放的配置,也未执行这些命令。

大型项目减少启动加载

scrapy crawl 会加载 SPIDER_MODULES 列出的全部模块来寻找目标爬虫。大量爬虫会增加启动耗时和内存占用。可以把设置收窄到目标模块:

scrapy crawl myspider -s SPIDER_MODULES=myproject.spiders.myspider

SPIDER_MODULES 是列表设置,需要多个模块时用逗号分隔。被导入的模块仍会执行 Python 顶层代码,因此项目也应避免在导入阶段启动抓取或访问外部资源。

访问策略、静态分析与在线排错

网站会依据请求头、速度和来源等特征识别抓取流量。原文先建议:在允许抓取的网站上,用 USER_AGENT 表明身份并提供联系方式,让站点可以联系你调整行为。

原文随后列出影响流量特征的做法:轮换常见浏览器 User-Agent;关闭 cookie 以避免会话标识关联请求;用 DOWNLOAD_DELAY 把请求间隔拉到 2 秒或更长;尽量读取 Common Crawl 已有数据;把请求分配到 IP 池(例举 Tor 与 ProxyMesh);通过 DOWNLOAD_TLS_MIN_VERSION/DOWNLOAD_TLS_MAX_VERSION 调整 TLS 行为;或使用 Zyte API 及其 Scrapy 插件。

这些是源文中的选项,不构成所有站点的通行授权。编辑提示:先协商许可与速率,不应把轮换身份或来源当成规避明确拒绝的默认方案,也不要为模仿浏览器而降低 TLS 安全底线。使用第三方服务还会改变数据流向、费用和隐私边界。持续被阻断时,原文建议联系商业支持。

代码层面可以考虑 scrapy-lint,用于发现 Scrapy 项目的常见错误和反模式。它属于静态检查,不能证明网站访问、输出或部署行为正确。

对于长期运行的爬虫,原文介绍两个在线观察入口:Telnet 控制台可连接进程并执行 Python;Scrapy MCP server 可让编码助手连接爬虫并执行进程内 Python。它们都是有代码执行能力的管理入口,应限制访问与授权,不能误认为只读监控接口。本文只解释机制,没有开启控制台、连接 MCP 或运行任何爬虫。

来源、版本与使用说明

来源:Scrapy 2.19.0 Common Practices,© Scrapy developers,BSD-3-Clause 完整许可条件及免责声明保留在本文下方。本文逐节翻译整理,原代码骨架保留;延迟分配、服务暴露、输入与管理权限的说明为编辑标注。示例未执行,未验证实验性模式的依赖兼容性或抓取结果。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

LICENSE-Scrapy.txt

Copyright (c) Scrapy developers.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    1. Redistributions of source code must retain the above copyright notice,
       this list of conditions, and the following disclaimer.
    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions, and the following disclaimer in the
       documentation and/or other materials provided with the distribution.

    3. Neither the name of Scrapy nor the names of its contributors may be used
       to endorse or promote products derived from this software without
       specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容