1.1 Scrapy 简介:一台为请求而生的机器 本节摘要:Scrapy 是基于 Twisted 异步引擎的 Python 爬虫框架,本质是一台"请求搬运机器":你只负责告诉它去哪、抓什么,排队、下载、限速、重试、清洗、入库由组件流水线托管。本节从与手写脚本的差异切入,给出双程动线全景,帮你在"该不该上框架"这件事上做出判断。 车票还没印,先看一眼整条铁路。本节是全册的"地图页":先回答"Scrapy 和我的脚本差在哪",再给出请求在框架里的完整动线,最后帮你判断什么场景值得上框架。 从一个熟悉的困境说起 用 requests 写抓取,通常是这样开始的: 这段代码能跑,但它把至少六件事焊死在一个循环里:限速、重试、去重、解析、清洗、存储。
本节摘要:Scrapy 是基于 Twisted 异步引擎的 Python 爬虫框架,本质是一台"请求搬运机器":你只负责告诉它去哪、抓什么,排队、下载、限速、重试、清洗、入库由组件流水线托管。本节从与手写脚本的差异切入,给出双程动线全景,帮你在"该不该上框架"这件事上做出判断。
车票还没印,先看一眼整条铁路。本节是全册的"地图页":先回答"Scrapy 和我的脚本差在哪",再给出请求在框架里的完整动线,最后帮你判断什么场景值得上框架。
用 requests 写抓取,通常是这样开始的:
# 典型的脚本式爬虫:一个 while 循环扛下所有职责 import requests from bs4 import BeautifulSoup def crawl_all_pages(): results = [] page = 1 while page <= 100: url = f"https://books.example.com/list?page={page}" # 目标站点示意 resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10) if resp.status_code != 200: continue # 失败了?死循环重试,没有退避 soup = BeautifulSoup(resp.text, "html.parser") for row in soup.select(".book"): results.append({"title": row.select_one("h3").text, "price": row.select_one(".price").text}) page += 1 time.sleep(1) # 全站一刀切限速:快的页面陪慢的页面干等 save_to_db(results) # 抓完最后一次性入库,中途崩了全白跑
这段代码能跑,但它把至少六件事焊死在一个循环里:限速、重试、去重、解析、清洗、存储。任何一个环节要改——比如某个分类页限速更严、某种价格要剔除非数字字符——都得动这坨逻辑。更麻烦的是它是串行阻塞的:网络等待的时间里,CPU 什么都没干。
Scrapy 的解法不是"帮你写得更快",而是把这些关切拆给固定组件,再用一个引擎按统一路线搬运数据流。你的代码只出现在两个位置:Spider 里"去哪、抓什么",管道里"数据怎么清洗入库"。
Scrapy 官方把架构图画成一张组件关系图,初学者往往背不下来。换个记法:把它当成一次请求的双程旅程,只有去程三站、回程两站。
去程:Spider 造出 Request(车票出生)→ 引擎把车票交给调度器排队(候车)→ 引擎从调度器取出下一张票,经下载器中间件过滤加工后交给下载器(下行)→ 下载器连上远端站点,拿到 Response。
回程:Response 原路送回,先过下载器中间件的回程关卡,再经引擎、蜘蛛中间件,交到 Spider 的解析回调手里(到站)→ 回调产出两个东西:Item(货物)走管道清洗入库,新 Request(续票)回到候车大厅重新排队——旅程循环,直到队列清空。
记住这个循环,你就记住了框架的全部主干。所谓"引擎",就是那个不亲自干任何脏活、只负责按路线搬东西并触发事件的调度中枢;所谓"中间件",就是插在去程和回程路径上的检查站。

不急着逐行学(那是第 2 章的事),先感受一下"你写的部分"有多薄:
import scrapy class QuoteSpider(scrapy.Spider): name = "quote" # 爬虫的唯一标识,运行时按名字调它 start_urls = ["https://quotes.example.com/"] # 起始地址,示意站点 def parse(self, response): # response 已经下载好,这里只管拆包 for q in response.css("div.quote"): yield { "text": q.css("span.text::text").get(), "author": q.css("small.author::text").get(), } # 发现下一页:续一张票,交回引擎 next_page = response.css("li.next a::attr(href)").get() if next_page: yield response.follow(next_page, callback=self.parse)
十几行里,你声明了去哪(start_urls)、怎么拆(css 选择器)、怎么续程(follow)。运行 scrapy crawl quote 后,控制台会打出类似这样的统计:
2026-08-29 10:12:01 [scrapy.statslog] INFO: Spider closed (finished) 'downloader/response_status_count/200': 12, 'item_scraped_count': 100, 'finish_reason': 'finished'
item_scraped_count 是 100 而不是 12——因为续票机制让引擎自动翻了 12 页,每页拆出若干条。你一行并发、重试、去重的代码都没写,这些全发生在你看不见的站点里。这就是"框架"二字的分量。
对比表里反复出现"异步"二字,值得单独说透,因为它是"Scrapy 快"的全部秘密。手写脚本用一个请求等一个响应,网络往返的几十到几百毫秒里线程在干等;要并发就开多线程,线程一多,切换开销与内存占用线性上涨,几百线程就到舒适区边缘。
Twisted 走的是另一条路:单线程事件循环。所有下载请求挂在事件循环上,哪个响应先回来就先处理哪个——等待不占线程,只占一个回调登记。于是一台机器轻挂几十路并发,内存稳定,CPU 几乎不空转:
# 同一台机器的两种并发姿态(示意统计) 多线程脚本:50 线程,内存 1.2 GB,CPU 切换开销显著,再往上加就抖 Scrapy: 16 并发,内存 180 MB,CPU 空闲等待网络,调参空间余裕充足
这也解释了框架的一个"怪脾气":你的解析回调里不该写长耗时同步操作——整个事件循环只有一根线程,你卡住一秒,全部在飞的请求陪你等一秒。回调要轻,重活交给管道或外部进程。初学者把图片压缩、大文件读写塞进 parse 函数,然后抱怨"并发怎么没效果",病根都在这。
工程判断比技术热情重要。我按实际经验给出取舍:
| 场景 | 建议 | 理由 |
|---|---|---|
| 抓几十个页面的临时任务 | 脚本即可 | 框架的固定成本不划算 |
| 分页列表加详情页的持续采集 | 上框架 | 去重、限速、续票机制直接复用 |
| 需要登录、代理、动态渲染的站点 | 上框架 | 中间件生态把这些做成可插拔关卡 |
| 高频调用第三方开放接口 | 通常脚本更合适 | 对方有正式 API 时,框架的浏览器拟态能力是浪费 |
| 大规模采集、需要增量与断点 | 框架加调度平台 | 第 6 章展开 |
💡 关键直觉:判断标准不是"数据量大小",而是"旅程里有没有变数"。只要存在失败重试、去重、限速、清洗这四类关切中的两类以上,框架就开始回本。
下一站,候车之前还差一步:把工厂的设备通电——环境安装与配置,见下一节。