Scrapy 框架:架构与 Spider
Scrapy 把抓取拆成一组职责明确的组件:调度器统一去重排队,下载器统一超时与重试,中间件统一限速与请求头。当规则多变、页数上万、需要一致的重试与限速时,自写脚本的维护成本会超过框架的学习成本。合规提醒:Scrapy 默认 ROBOTSTXT_OBEY = True,不要为了「提速」把它关掉;控制并发与延迟,不采集个人隐私与受版权保护的付费内容,不绕过登录、付费墙、验证码等访问控制。
架构:谁负责什么
| 组件 | 职责 | 关键点 |
|---|---|---|
| Engine | 驱动整个数据流,串联各组件 | 不写业务逻辑,也不该被绕开 |
| Scheduler | 请求去重与排队 | 默认按 URL 指纹去重,重复请求直接丢弃 |
| Downloader | 真正发 HTTP 请求 | 并发、超时、重试都在这层生效 |
| Spider | 写解析规则,产出 Request 与 Item | 业务代码集中在这里 |
| 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.Item 加 scrapy.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_ENABLED 与 DOWNLOAD_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 只需记住 parse 里 yield Request 继续翻页、yield Item 交给 Pipeline,落盘放 Pipeline,限速与礼貌放 settings,并始终保留默认开启的 ROBOTSTXT_OBEY。