1.1 Scrapy 简介:一台为请求而生的机器


文档摘要

1.1 Scrapy 简介:一台为请求而生的机器 本节摘要:Scrapy 是基于 Twisted 异步引擎的 Python 爬虫框架,本质是一台"请求搬运机器":你只负责告诉它去哪、抓什么,排队、下载、限速、重试、清洗、入库由组件流水线托管。本节从与手写脚本的差异切入,给出双程动线全景,帮你在"该不该上框架"这件事上做出判断。 车票还没印,先看一眼整条铁路。本节是全册的"地图页":先回答"Scrapy 和我的脚本差在哪",再给出请求在框架里的完整动线,最后帮你判断什么场景值得上框架。 从一个熟悉的困境说起 用 requests 写抓取,通常是这样开始的: 这段代码能跑,但它把至少六件事焊死在一个循环里:限速、重试、去重、解析、清洗、存储。

1.1 Scrapy 简介:一台为请求而生的机器

本节摘要: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 Scrapy 双程架构总览:组件与数据流

图2 Scrapy 双程架构总览:组件与数据流

概念代码:一个最小 Spider 长什么样

不急着逐行学(那是第 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 章展开

💡 关键直觉:判断标准不是"数据量大小",而是"旅程里有没有变数"。只要存在失败重试、去重、限速、清洗这四类关切中的两类以上,框架就开始回本。

本节要点回顾

  • Scrapy 的本质是组件化流水线加异步引擎,你只写 Spider 与管道两个岗位的代码;
  • 双程动线:请求经调度、中间件、下载器去程出发,响应原路折返到解析回调,Item 回程入库,新请求续票循环;
  • 引擎只搬运:它不下载、不解析、不存储,只按路线传数据流并触发事件;
  • 选型看变数:重试、去重、限速、清洗四类关切占两类以上,就该上框架。

下一站,候车之前还差一步:把工厂的设备通电——环境安装与配置,见下一节。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U