5. Scrapy
这是什么:Scrapy 是 Python 爬虫界事实上的标准框架。前四章手写的每一样东西——请求队列、URL 去重、并发控制、失败重试、限速、数据导出——Scrapy 全部内置,你只需要写两块业务代码:从哪开始爬(Spider)和抓到后解析什么(parse)。
为什么用框架:手写爬虫到全站规模,调度和健壮性代码会膨胀到比业务逻辑还多;框架把这些抽成配置,一套代码从单页爬到百万页。反过来,小脚本级的需求用 requests 更轻——框架不是万能替代,是规模化工具。
如何用:建项目 → 写 Spider → 配置限速 → 一条命令爬完全站导出 JSON。
装包与建项目
pip install scrapy
scrapy startproject scrapy_demo # 创建项目
cd scrapy_demo
生成的目录结构(每样东西的职责先有个印象):
| 文件/目录 | 职责 | 类比手写爬虫 |
|---|---|---|
scrapy_demo/spiders/ |
放爬虫,每站一个文件 | 你的 parse_quotes.py |
items.py |
定义一条数据的字段结构 | 你存数据的字典长什么样 |
pipelines.py |
数据清洗、去重、入库 | 第 4 章的存储代码 |
settings.py |
限速、并发、UA、robots 开关 | 第 4 章的礼貌爬取 |
middlewares.py |
请求发出前/响应回来后的加工车间 | 换代理、换 UA 的钩子(进阶) |
写第一个 Spider
scrapy genspider quotes quotes.toscrape.com
打开 spiders/ 下生成的模板文件,改成下面这样(去掉多余注释):
import scrapy
class QuotesSpider(scrapy.Spider):
name = "quotes" # 爬虫名字,运行时用
start_urls = ["https://quotes.toscrape.com/"] # 起点自动进队列
def parse(self, response): # 每个下载好的页面都会进来
for quote in response.css("div.quote"):
yield {
"text": quote.css("span.text::text").get(),
"author": quote.css("small.author::text").get(),
"tags": quote.css("a.tag::text").getall(),
}
# 翻页:发现「下一页」就把它加入队列,Scrapy 自动去重、自动调度
next_page = response.css("li.next a::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)
注意三个和手写版的差别:
| 手写版 | Scrapy 版 | 框架帮你干了什么 |
|---|---|---|
requests.get() + 自己管理 |
start_urls + response |
请求、重试、限速、并发全自动 |
a["href"] 取属性 |
a::attr(href) 直接取属性值 |
选择器语法统一,返回就是值 |
get_text() |
::text |
同上 |
| 自己写队列 + set 去重 | yield response.follow(...) |
入队、去重、调度、URL 拼接全内置 |
response.css 返回的是选择器列表:.get() 取第一个值,.getall() 取全部值——和第 3 章的 CSS 选择器是同一套语法。
运行:一条命令爬完导出
scrapy crawl quotes -O quotes.json
你会看到:终端刷一阵请求日志(绿色 200),项目目录出现 quotes.json——全部 10 页 100 条名言,你一行调度代码都没写。-O 是覆盖导出(.csv/.jl 后缀同样支持);只想看数据不想要日志,加 -s LOG_ENABLED=False。
settings.py:把礼貌爬取变成配置
打开 scrapy_demo/settings.py,这几行和第 4 章的规矩一一对应:
ROBOTSTXT_OBEY = True # 遵守 robots.txt(新项目模板默认开启)
DOWNLOAD_DELAY = 1.0 # 每次请求间隔 1 秒,相当于内置 time.sleep
CONCURRENT_REQUESTS = 8 # 总并发请求数,别开大
CONCURRENT_REQUESTS_PER_DOMAIN = 2 # 单个网站的并发,礼貌红线
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126.0"
RETRY_TIMES = 3 # 失败自动重试 3 次
配置即代码——限速、并发、重试这些「手写要几百行」的东西,Scrapy 里就是几个常量。
Item 与 Pipeline:数据加工车间
yield 字典 已经能用,但正式项目建议定义 Item(字段有名字、有约束),并在 Pipeline 里做清洗和入库:
# items.py
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
# pipelines.py —— 数据流经的加工车间
class CleanPipeline:
def process_item(self, item, spider):
item["text"] = item["text"].strip()
if not item["author"]: # 也能在这里丢弃脏数据
from scrapy.exceptions import DropItem
raise DropItem("作者为空,丢弃")
return item
# settings.py 里启用管道(数字越小越先执行)
ITEM_PIPELINES = {"scrapy_demo.pipelines.CleanPipeline": 300}
之后 Spider 里 yield QuoteItem(...),数据就自动流过管道再落盘。记住分工:Spider 管提取,Pipeline 管清洗入库,settings 管行为。
常见翻车现场
| 症状 | 原因 | 修复 |
|---|---|---|
scrapy 命令找不到 |
装在了别的环境/没激活 venv | 激活装包的那个环境 |
运行报 Spider not found |
crawl 后面没写对 name |
scrapy list 查看可用爬虫名 |
| 抓到的都是空值 | 选择器和页面结构对不上 | F12 重新核对,注意数据是否 JS 渲染 |
| 全是 403 | settings 里没配 USER_AGENT | 加上 UA(模板默认可能没开) |
| 输出 JSON 中文乱码 | 用了 -o 且没设置编码 |
导出用 -O,或导出 .jsonl 后自己转 |
动手改
- 给 Spider 加一个
start_urls起点:https://books.toscrape.com/,抓书名 + 价格 - 把
DOWNLOAD_DELAY改成 0 再跑,对比抓完 10 页的耗时(也体会一下网站的压力差) - 写一个 Pipeline:把 author 去空格、tags 合并成逗号字符串后再导出