Scrapy 是一个用 Python 编写的开源且功能强大的 Web 爬虫框架,它被设计用于快速、高效地从网站上提取结构化数据。Scrapy 不仅提供了完整的爬虫生命周期管理,包括请求调度、并发控制、数据解析和持久化,还通过其高度模块化的架构,允许开发者轻松扩展和定制爬虫行为。

核心思想:将 Web 爬取视为一个事件驱动的流程,通过异步 I/O (基于 Twisted) 实现高并发,并提供一套可插拔的组件,以便开发者专注于数据提取逻辑。


一、为什么需要 Scrapy?

在数据驱动的时代,从 Web 获取大量结构化信息的需求日益增长。虽然我们可以使用 requests 库发送 HTTP 请求并结合 BeautifulSouplxml 等库解析 HTML,但当面临以下挑战时,手动编写爬虫会变得复杂且低效:

  1. 并发与效率:需要同时发送大量请求以提高爬取速度,手动管理并发、线程或协程将非常繁琐。
  2. 请求调度与去重:爬虫需要跟踪哪些 URL 已访问、哪些待访问,并避免重复请求,这需要复杂的调度逻辑。
  3. 中间件处理:处理 User-Agent 轮换、代理 IP、Cookie 管理、重试机制、请求限速等功能时,需要一个统一的机制。
  4. 数据清洗与持久化:将提取到的数据进行清洗、验证,并存储到数据库、文件或其他存储介质时,需要结构化的处理流程。
  5. 可维护性与扩展性:随着爬虫规则的增加或网站结构的变化,代码需要易于修改和扩展。

Scrapy 旨在解决这些问题,提供一个端到端的爬虫解决方案,让开发者能够专注于如何提取数据,而不是如何实现爬虫底层机制

二、Scrapy 架构与核心组件

Scrapy 的架构是高度模块化的,基于一个事件驱动的异步 I/O 引擎 (Scrapy Engine),其核心组件协同工作,构成一个完整的爬虫生命周期。

2.1 架构概览

2.2 核心组件定义

  1. Scrapy Engine (Scrapy 引擎)

    • 定义:Scrapy 的核心,负责控制所有组件之间的数据流,并根据事件驱动的机制触发组件的动作。它就像爬虫的“大脑”,协调各个部分的工作。
    • 职责:处理请求与响应,将任务分发给调度器、下载器和爬虫,以及将 Item 传递给 Item Pipeline。
  2. Scheduler (调度器)

    • 定义:负责接收 Scrapy Engine 发送的请求,并将其放入队列,等待 Downloader 获取。它也负责请求的去重。
    • 职责:存储和管理待抓取的 URL 队列,确保请求按优先级顺序发送,并过滤掉重复的请求。
  3. Downloader (下载器)

    • 定义:负责执行所有网络请求,获取网页内容并返回给 Scrapy Engine。
    • 职责:通过 HTTP(S) 协议下载页面,处理重定向、代理、User-Agent 等。
  4. Spiders (爬虫)

    • 定义:开发者自定义的类,负责定义如何抓取特定网站,包括初始请求、如何跟随链接以及如何从页面中提取结构化数据 (Items)。
    • 职责:生成初始请求,接收 Downloader 返回的响应,并解析响应以提取数据或生成新的请求。
  5. Item (数据模型)

    • 定义:用于存储从网页中提取的数据的简单容器。它类似于 Python 字典,但提供了额外的基于声明的字段定义。
    • 职责:定义数据的结构,使得数据在爬虫、中间件和管道之间以统一的格式传递。
  6. Item Pipelines (项目管道)

    • 定义:用于处理 Spider 提取的 Item。它们是一系列独立的组件,按顺序处理 Item。
    • 职责:清洗、验证、持久化 (存储到数据库、文件等) Item 数据。
  7. Downloader Middleware (下载器中间件)

    • 定义:介于 Scrapy Engine 和 Downloader 之间的钩子框架。
    • 职责:在请求发送给 Downloader 之前或响应返回给 Scrapy Engine 之前,修改请求或响应。例如,设置代理、User-Agent 轮换、处理 Cookie、实现重试机制等。
  8. Spider Middleware (爬虫中间件)

    • 定义:介于 Scrapy Engine 和 Spiders 之间的钩子框架。
    • 职责:在响应发送给 Spider 之前或 Spider 处理请求返回结果之后,修改请求或 Item。例如,处理 Spider 的输入/输出、过滤重复数据等。

三、Scrapy 项目结构与核心概念实战

3.1 创建 Scrapy 项目

首先,安装 Scrapy:

1
pip install scrapy

然后,创建一个新的 Scrapy 项目:

1
scrapy startproject myproject

这会生成以下目录结构:

1
2
3
4
5
6
7
8
9
10
myproject/
├── scrapy.cfg # 项目配置文件
├── myproject/ # 项目的 Python 模块
│ ├── __init__.py
│ ├── items.py # Item 定义文件
│ ├── middlewares.py # Spider 和 Downloader 中间件定义文件
│ ├── pipelines.py # Item Pipeline 定义文件
│ ├── settings.py # 项目设置文件
│ └── spiders/ # 存放爬虫的目录
│ └── __init__.py

3.2 Item (数据模型)

myproject/items.py 中定义你想要提取的数据结构。

1
2
3
4
5
6
7
8
9
10
11
# myproject/items.py
import scrapy

class Product(scrapy.Item):
# 定义字段名称
name = scrapy.Field()
price = scrapy.Field()
category = scrapy.Field()
description = scrapy.Field()
# 可以定义更多的字段,例如 URL、图片等
url = scrapy.Field()

3.3 Spider (爬虫)

myproject/spiders/ 目录下创建一个新的爬虫文件,例如 quotes_spider.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# myproject/spiders/quotes_spider.py
import scrapy
from myproject.items import Product # 导入之前定义的 Item

class QuotesSpider(scrapy.Spider):
name = "quotes" # 爬虫的唯一名称
start_urls = [ # 爬虫开始抓取的 URL 列表
"https://quotes.toscrape.com/page/1/",
"https://quotes.toscrape.com/page/2/",
]

def parse(self, response):
# 这是一个默认的回调函数,用于处理从 start_urls 下载的响应
# response 对象包含了下载的页面内容
# 使用 CSS 选择器或 XPath 提取数据

# 示例:提取每条引言的文本、作者和标签
for quote in response.css('div.quote'):
item = Product() # 实例化 Item
item['name'] = quote.css('span.text::text').get() # 用 name 字段来存储引言文本
item['price'] = quote.css('small.author::text').get() # 用 price 字段来存储作者名
item['category'] = quote.css('div.tags a.tag::text').getall() # 用 category 字段存储所有标签
item['url'] = response.url # 存储当前页面的 URL
yield item # 将提取到的 Item 传递给 Item Pipeline

# 查找下一页的链接,并生成新的请求
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
# 使用 response.follow() 自动生成完整 URL
yield response.follow(next_page, callback=self.parse)

3.4 Item Pipeline (项目管道)

myproject/pipelines.py 中定义 Item Pipeline。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# myproject/pipelines.py
import json

class MyprojectPipeline:
def open_spider(self, spider):
# 在爬虫启动时打开文件
self.file = open('quotes.json', 'w', encoding='utf-8')
self.file.write("[\n") # 写入 JSON 数组的开始

def close_spider(self, spider):
# 在爬虫关闭时关闭文件
self.file.write("\n]") # 写入 JSON 数组的结束
self.file.close()

def process_item(self, item, spider):
# 处理每个 Item
line = json.dumps(dict(item), ensure_ascii=False) + ",\n"
self.file.write(line)
return item # 确保 Item 被返回,以便其他管道继续处理 (如果有的话)

注意:为了使 Item Pipeline 生效,需要在 myproject/settings.py 中启用它。

3.5 Settings (项目设置)

myproject/settings.py 中配置爬虫的各种参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# myproject/settings.py
# Scrapy settings for myproject project

BOT_NAME = 'myproject'
SPIDER_MODULES = ['myproject.spiders']
NEWSPIDER_MODULE = 'myproject.spiders'

ROBOTSTXT_OBEY = True # 是否遵守 robots.txt 协议,生产环境通常设置为 True

# 配置并发请求数
CONCURRENT_REQUESTS = 16 # 同时处理的最大请求数 (默认16)

# 配置下载延迟,防止被网站封禁
DOWNLOAD_DELAY = 1 # 每次请求的最小下载延迟秒数

# User-Agent 配置 (可以轮换)
USER_AGENT = 'myproject (+http://www.yourdomain.com)'

# 启用 Item Pipeline
ITEM_PIPELINES = {
'myproject.pipelines.MyprojectPipeline': 300, # 数字代表优先级,越小优先级越高
}

# 启用下载器中间件 (例如,如果启用了代理或自定义 User-Agent)
DOWNLOADER_MIDDLEWARES = {
# 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 禁用内置的 User-Agent
# 'myproject.middlewares.RandomUserAgentMiddleware': 400, # 启用自定义的 User-Agent 中间件
# 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 1, # 启用代理中间件
}

# LOG_LEVEL = 'INFO' # 设置日志级别,可选 DEBUG, INFO, WARNING, ERROR, CRITICAL
# FEED_FORMAT = 'json' # 也可以直接通过 FEED_EXPORT_ITEM_FIELDS 控制输出字段
# FEED_URI = 'quotes.json' # 更简单的输出方式,但控制力不如 Item Pipeline

3.6 运行爬虫

在项目根目录下 (与 scrapy.cfg 同级),运行:

1
scrapy crawl quotes

这将启动 quotes 爬虫,并在完成后生成 quotes.json 文件。

四、高级特性与最佳实践

4.1 中间件 (Middleware)

中间件是 Scrapy 强大的扩展点,用于在请求/响应处理流程中插入自定义逻辑。

自定义 Downloader Middleware 示例 (User-Agent 轮换)
myproject/middlewares.py 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# myproject/middlewares.py
import random
from scrapy import signals

class RandomUserAgentMiddleware:
def __init__(self, user_agents):
self.user_agents = user_agents

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.getlist('USER_AGENTS')) # 从 settings 获取 User-Agent 列表

def process_request(self, request, spider):
user_agent = random.choice(self.user_agents)
request.headers['User-Agent'] = user_agent

# 在 settings.py 中配置 USER_AGENTS 和启用此中间件
# USER_AGENTS = [
# 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
# 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36',
# # ... 更多 User-Agent
# ]
# DOWNLOADER_MIDDLEWARES = {
# 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 禁用 Scrapy 内置 User-Agent
# 'myproject.middlewares.RandomUserAgentMiddleware': 400,
# }

4.2 避免被封禁的策略

  • DOWNLOAD_DELAY:设置请求之间的延迟。
  • AUTOTHROTTLE_ENABLED = True:自动调节下载延迟,根据网站响应速度智能调整。
  • USER_AGENTS 轮换:使用 Downloader Middleware 实现,模拟不同浏览器。
  • 代理 IP 池:使用 Downloader Middleware 实现,通过代理发送请求。
  • CONCURRENT_REQUESTS_PER_DOMAIN:限制单个域名的并发请求数。
  • CONCURRENT_REQUESTS_PER_IP:限制单个 IP 的并发请求数。
  • 处理 Cookie:Downloader 会自动处理 Cookie,但有时需要手动清理或管理。

4.3 错误处理与日志

  • LOG_LEVEL:在 settings.py 中设置日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)。
  • LOG_FILE:将日志输出到文件。
  • Request 的 errback:在请求失败时(例如 HTTP 错误、DNS 错误),可以指定一个错误回调函数来处理。
    1
    2
    3
    4
    5
    6
    # 在 Spider 中
    yield scrapy.Request(url, callback=self.parse, errback=self.handle_error)

    def handle_error(self, failure):
    # 记录错误或进行重试
    self.logger.error(f"Request failed for {failure.request.url}: {failure.value}")

4.4 其他实用命令

  • scrapy genspider example example.com:快速生成一个爬虫模板。
  • scrapy shell <url>:启动交互式 Scrapy Shell,用于测试选择器和调试爬虫。
  • scrapy crawl <spider_name> -o output.json:将爬取结果直接输出到文件,无需 Item Pipeline。

五、Scrapy 的优缺点

5.1 优点

  1. 高并发与效率:基于 Twisted 异步网络库,实现高效的并发爬取,吞吐量大。
  2. 模块化与可扩展性:清晰的架构和丰富的扩展点 (中间件、管道、自定义调度器等),方便定制和维护。
  3. 功能丰富:内置请求调度、去重、Cookie 处理、会话管理、DNS 缓存、自动限速等功能。
  4. 易于使用:提供了强大的选择器 (CSS/XPath) 进行数据提取,且有完善的文档和社区支持。
  5. 适用性广:适合爬取大型网站、复杂数据结构和需要长期运行的爬虫项目。
  6. Shell 调试scrapy shell 提供了强大的交互式调试环境。

5.2 缺点

  1. 学习曲线:对于初学者,其架构和异步机制可能比 requests + BeautifulSoup 更复杂,学习成本略高。
  2. Twisted 依赖:基于 Twisted,可能在某些环境中安装和调试相对复杂。
  3. 不适合简单任务:对于只需爬取少数几个页面的简单任务,引入 Scrapy 框架可能显得过于“重”。
  4. JavaScript 渲染:Scrapy 自身不直接支持 JavaScript 渲染。对于大量依赖 JS 动态加载内容的网站,需要集成其他工具 (如 Splash 或 Selenium/Playwright)。

六、总结

Scrapy 是 Python 社区中最成熟、最受欢迎的 Web 爬虫框架之一。它通过其强大的异步架构和高度可插拔的组件,为开发者提供了一套全面的解决方案,以应对现代 Web 爬取所面临的各种挑战。无论您是需要构建一个小型的数据采集脚本,还是一个大规模的分布式爬虫系统,Scrapy 都能提供强大的支持。理解其核心架构、组件职责和灵活的扩展机制,将使您能够高效地构建出稳定、可扩展的 Web 爬虫。