Scrapy 框架:架构与 Spider

Scrapy 把抓取拆成一组职责明确的组件:调度器统一去重排队,下载器统一超时与重试,中间件统一限速与请求头。当规则多变、页数上万、需要一致的重试与限速时,自写脚本的维护成本会超过框架的学习成本。合规提醒:Scrapy 默认 ROBOTSTXT_OBEY = True,不要为了「提速」把它关掉;控制并发与延迟,不采集个人隐私与受版权保护的付费内容,不绕过登录、付费墙、验证码等访问控制。

架构:谁负责什么

组件职责关键点
Engine驱动整个数据流,串联各组件不写业务逻辑,也不该被绕开
Scheduler请求去重与排队默认按 URL 指纹去重,重复请求直接丢弃
Downloader真正发 HTTP 请求并发、超时、重试都在这层生效
Spider写解析规则,产出 RequestItem业务代码集中在这里
Item Pipeline清洗、校验、落盘按数字优先级从低到高依次执行
Downloader Middleware改请求与响应(UA、代理、限速、重试)全局生效,改动影响所有 Spider
Spider Middleware改 Spider 的输入输出用得少,但排查「Item 去哪了」时很有用
Engine → Scheduler:请求入队(按 URL 指纹去重,重复的直接丢弃)
Scheduler → Downloader:取出待抓请求,途中经过 Downloader Middleware
Downloader → Spider:响应交给 parse 回调,途中经过 Spider Middleware
Spider → Item Pipeline:yield Item,清洗校验后落盘
Spider → Scheduler:yield Request,回到队列,直到队列为空

装好并起一个项目

pip install scrapy
scrapy startproject demo
cd demo && scrapy genspider example example.com
scrapy crawl example -O out.jsonl     # 覆盖写入;小写 -o 是追加写入
scrapy shell "https://example.com"    # 交互式试选择器,不用整站跑

Spider:parse 里的两种 yield

parse 是入口回调,yield 只有两种去向:yield scrapy.Request(...)(更省事的写法是 response.follow(...))把新请求交回调度器继续翻页,yield Item 把数据交给 Pipeline。

from dataclasses import dataclass

import scrapy

@dataclass
class Article:
    """字段少时用标准库 dataclass,Scrapy 与 Pipeline 同样接受。"""
    url: str
    title: str
    published: str

class ExampleSpider(scrapy.Spider):
    name = "example"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com/list?page=1"]
    custom_settings = {
        "DOWNLOAD_DELAY": 1.0,                 # 同域名请求间隔,礼貌优先
        "CONCURRENT_REQUESTS_PER_DOMAIN": 4,   # 第三方站点别超过这个量级
    }

    def parse(self, response):
        """列表页解析:产出 Item,并把下一页请求交回调度器。"""
        for li in response.css("li.article"):
            yield Article(                                    # 第二种 yield:数据
                url=response.urljoin(li.css("a::attr(href)").get()),
                title=li.css("a::text").get(default="").strip(),
                published=li.css("time::attr(datetime)").get(default=""),
            )
        next_page = response.css("a.next::attr(href)").get()   # 第一种 yield:请求
        if next_page:
            yield response.follow(next_page, callback=self.parse)

Item 与 Pipeline

scrapy.Itemscrapy.Field() 声明字段(字段多、要复用时更清晰),或者像上面那样直接用 dataclass。同一批字段也可以用 response.xpath('//li[@class="article"]//a/@href') 这类写法取,与 response.css 等价,选顺手的即可。落盘逻辑写进 Pipeline 的 process_item,文件句柄在 open_spider/close_spider 里开与关,然后在 settings.py 注册:ITEM_PIPELINES = {"demo.pipelines.JsonLinesPipeline": 300},数字是优先级,越小越先执行。

import json

from itemadapter import ItemAdapter

class JsonLinesPipeline:
    """按行落盘:追加写,崩溃最多丢最后一行,也便于 grep 与重跑。"""

    def open_spider(self, spider):
        self.file = open("articles.jsonl", "a", encoding="utf-8")
        spider.logger.info("开始写入 articles.jsonl")

    def process_item(self, item, spider):
        row = dict(ItemAdapter(item))        # dataclass 与 Item 都能适配成字典
        missing = [k for k, v in row.items() if v in (None, "")]
        if missing:                          # 关键字段为空就别写进结果集
            spider.logger.warning("字段缺失 %s:%s", missing, row.get("url"))
            return item
        self.file.write(json.dumps(row, ensure_ascii=False) + "\n")
        return item                          # 必须返回,否则后面的 Pipeline 收不到

    def close_spider(self, spider):
        self.file.close()
        spider.logger.info("写入结束,可以开始校验与去重")

settings 关键项

配置作用建议
ROBOTSTXT_OBEY = True遵守 robots.txt默认开启,不要为了提速关掉
USER_AGENT声明抓取者身份写清用途与联系方式,别伪装成浏览器
CONCURRENT_REQUESTS全局并发请求数起步 8~16,按对方承受力调
CONCURRENT_REQUESTS_PER_DOMAIN单域名并发对第三方站点 2~4 就够了
DOWNLOAD_DELAY同域名请求间隔0.5~1 秒起步,比并发更能保护对方
RANDOMIZE_DOWNLOAD_DELAY给间隔加随机抖动保持 True,别让请求整齐得像机器
AUTOTHROTTLE_ENABLED自动降速True,比手动试并发省心
AUTOTHROTTLE_TARGET_CONCURRENCY自动节流的目标并发1~2,被限流时框架会自己慢下来
RETRY_TIMES重试次数2~3,与自写脚本的退避策略保持一致
DOWNLOAD_TIMEOUT下载超时15~30 秒,避免慢连接拖住整批
HTTPCACHE_ENABLED本地缓存响应调试选择器时开,正式跑关掉
FEEDS导出格式与路径简单场景用 scrapy crawl -O 就够

调优的顺序是先设 AUTOTHROTTLE_ENABLEDDOWNLOAD_DELAY,再用 scrapy shell 验证选择器,最后才考虑加大并发。看到 403 与 429 就降并发、加延迟,而不是关掉 robots.txt 或换代理。

自写脚本还是 Scrapy

维度requests/httpx 脚本Scrapy
上手成本十分钟,一个文件跑通半天,先理解组件分工
适合规模几十到几千页、一次性任务上万页、长期运行、多站点
调度与去重自己写队列与集合内置,含 URL 指纹去重
重试与限速自己写退避与令牌桶settings 与中间件统一配置
规则多变改函数最快,但容易写乱拆成 Spider 与 Pipeline,结构清晰
调试print 与断点scrapy shell 直接试选择器

规则稳定、页数不多、只跑一次,自写脚本更划算;规则频繁变动、需要统一重试限速、要长期增量跑,Scrapy 的调度器与中间件能省掉大量重复代码。

常见坑

  • parse 里写同步阻塞代码(如 time.sleep),会拖住整个下载器,限速交给 DOWNLOAD_DELAY
  • Pipeline 的 process_item 忘记 return item,后续 Pipeline 与 FEEDS 都拿不到数据。
  • scrapy shell 里验证通过的选择器在整站跑时为空,通常是页面有多个模板或懒加载,需要按 URL 类型分流回调。

小结:Scrapy 的价值在于调度去重、统一重试限速与可插拔的中间件;写 Spider 只需记住 parseyield Request 继续翻页、yield Item 交给 Pipeline,落盘放 Pipeline,限速与礼貌放 settings,并始终保留默认开启的 ROBOTSTXT_OBEY

笔记加载中…