在掌握了基础的爬虫工具后,我们会发现当项目变得复杂时,手动管理请求、调度、数据处理和存储会变得非常繁琐。这时,我们需要一个工程化的解决方案——Scrapy 框架。
Scrapy框架简介和安装
Scrapy 是一个为了爬取网站数据、提取结构性数据而编写的应用框架。它是一个“全家桶”式的解决方案,内置了异步处理、请求调度、数据管道、中间件等一系列强大功能。
核心思维:从“写一个爬虫脚本”转变为“构建一个爬虫项目”。Scrapy 提供了骨架,我们只需填充其中的“血肉”(即爬取逻辑和数据处理逻辑)。
核心架构与数据流
理解 Scrapy 的工作流程至关重要。它就像一个精密的自动化工厂:
数据流转过程:
- 引擎 (Engine) 从 爬虫 (Spiders) 获取初始请求。
- 引擎将请求交给 调度器 (Scheduler) 去“排队”。
- 引擎向调度器请求下一个要爬取的 URL。
- 调度器返回请求,引擎将其通过 下载中间件 (Downloader Middleware) 发送给 下载器 (Downloader)。
- 下载器完成下载,将响应通过 下载中间件 返回给引擎。
- 引擎将响应发送给 爬虫 进行解析。
- 爬虫解析响应,提取数据 (Items) 和新的请求 (Requests)。
- 引擎将提取的数据发送给 项目管道 (Item Pipelines) 进行处理和存储。
- 引擎将新的请求发送给 调度器,重复第 2 步,直到没有更多请求。
数据解析和并发设置
数据解析
Scrapy 的 response 对象自带 .css()
和 .xpath()
方法。调用它们时,实际上 Scrapy 已经在后台使用了 lxml
来解析 HTML
response.css()
: 语法更简洁,对于前端开发者更友好。适合大多数层级清晰的提取场景。response.xpath()
: 功能更强大,可以实现更复杂的轴查询(如查找父节点、兄弟节点),在处理结构混乱的页面时更有优势。
Twisted简介
Scrapy 的底层网络引擎是 Twisted
,这是一个成熟的、事件驱动的网络编程框架,其工作原理与 asyncio 非常相似
- 在 Spider 中写的 parse 等方法可以看作是 Twisted 事件循环中的回调函数。
- 当 yield 一个 Request 时,你并不是在阻塞等待,而是在向事件循环注册一个新任务。引擎会继续处理其他任务,当你的请求获得响应后,Twisted 会调用你指定的回调函数(如 parse)来处理 response。
- 从 Scrapy 2.0 开始,可以在回调函数中直接使用
async def
和await
语法,这使得编写异步逻辑更加直观(需要正确配置 Twisted Reactor)。
并发设置
控制爬取行为对于避免被封禁和保护目标网站至关重要。Scrapy 提供了多种配置项,可以精细地控制爬虫。这些配置都在 settings.py
中设置
-
CONCURRENT_REQUESTS
: Scrapy 下载器全局最大并发请求数。默认是16
。python# 设置全局最大并发请求数为32 CONCURRENT_REQUESTS = 32
-
CONCURRENT_REQUESTS_PER_DOMAIN
: 针对单个域名的最大并发请求数。这个设置比全局设置更常用,也更重要。默认是8
。python# 设置单个域名的最大并发请求数为16 CONCURRENT_REQUESTS_PER_DOMAIN = 16
-
DOWNLOAD_DELAY
: 最常用、最重要的设置。为同一个网站的连续请求之间增加一个随机延迟python# 每次请求前等待 0.5 到 1.5 秒 DOWNLOAD_DELAY = 1 # 基础延迟为1秒 # 开启随机延迟,延迟时间为 [0.5 * DOWNLOAD_DELAY, 1.5 * DOWNLOAD_DELAY] RANDOMIZE_DOWNLOAD_DELAY = True
-
AUTOTHROTTLE_ENABLED
: 自动限速扩展。Scrapy 会根据服务器的响应延迟动态调整下载延迟,使其更加智能python# 启用自动限速扩展 AUTOTHROTTLE_ENABLED = True # 设置初始下载延迟为0.5秒 AUTOTHROTTLE_START_DELAY = 0.5 # 设置最大下载延迟为5秒 AUTOTHROTTLE_MAX_DELAY = 5 # 设置下载延迟的平均值为1秒 AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
-
CONCURRENT_REQUESTS_PER_IP
: 针对单个 IP 的最大并发请求数。默认是0
,表示不限制。如果使用代理,这个设置会很有用。
安装和项目创建
安装 Scrapy
pip install scrapy
执行命令后,pip 不仅安装了 scrapy 库,还会在系统路径下创建了一个可执行脚本(scrapy.exe 或 scrapy)。这样可以在任何地方直接运行 scrapy startproject
等命令。这样的确很方便,但会导致版本冲突问题:如果项目 A 依赖 Scrapy 2.0,项目 B 依赖 Scrapy 3.0,全局安装就无法满足。
推荐使用现代包管理如
uv
来管理项目,最好不要全局安装scrapy
这类命令行工具
创建项目
如果使用了全局安装scrapy的方式,则使用下面的方式创建项目:
scrapy startproject my_project
如果想要使用uv管理项目,则需要使用下面的方式创建项目:
uvx scrapy startproject my_project
# uvx 会临时下载 scrapy 到一个全局缓存中,
# 用它执行 startproject 命令来创建 my_project 目录
cd my_project
uv init
uv add scrapy
# 将 scrapy 作为这个项目的正式依赖添加进来,方便后续执行scrapy相关命令
这会自动生成一个标准的 Scrapy 项目目录结构:
my_project/
scrapy.cfg # 项目的配置文件
my_project/ # 项目的 Python 模块
__init__.py
items.py # 定义数据容器 (Item)
middlewares.py # 自定义中间件
pipelines.py # 自定义数据管道
settings.py # 项目的设置文件
spiders/ # 存放爬虫代码的目录
__init__.py
创建爬虫
进入项目目录,使用 genspider
命令创建一个新的爬虫。
cd my_project
scrapy genspider example example.com
这会在
spiders/
目录下创建一个example.py
文件,包含一个基本的爬虫模板。
如果是 uv 管理的项目,则需要使用下面的方式创建爬虫:
uv run scrapy genspider example example.com
核心组件详解
Items (数据容器)
items.py
文件用于定义要抓取的数据结构。它类似于一个字典,但提供了额外的保护机制,防止因字段名拼写错误而导致数据丢失。
定义 Item:
# my_project/items.py
import scrapy
class MyProjectItem(scrapy.Item):
# define the fields for your item here like:
name = scrapy.Field()
link = scrapy.Field()
description = scrapy.Field()
Spiders (爬虫)
Spider 是你编写爬取逻辑的核心类。它负责定义从哪里开始爬取、如何跟进链接以及如何从页面中提取数据。
一个基本的 Spider:
# my_project/spiders/example.py
import scrapy
from my_project.items import MyProjectItem
class ExampleSpider(scrapy.Spider):
name = 'example' # 爬虫的唯一标识名
allowed_domains = ['example.com'] # (可选) 允许爬取的域名
start_urls = ['http://example.com/'] # 起始 URL 列表
def parse(self, response):
"""
Scrapy 下载完页面后会自动调用此方法。
response 对象包含了页面源码和各种元数据。
"""
# 使用 CSS 或 XPath 选择器解析数据
title = response.css('h1::text').get()
# 填充 Item
item = MyProjectItem()
item['name'] = title
# ... 填充其他字段 ...
# 使用 yield 将数据返回给 Scrapy 引擎
yield item
yield item
: 将提取到的数据交给引擎,引擎会再把它送入 Item Pipeline。yield scrapy.Request(...)
: 生成一个新的请求,交给引擎去调度和下载。
Item Pipelines (项目管道)
当 Spider 提取出 Item 后,Item 会被发送到 Item Pipeline 进行后续处理,如数据清洗、验证、去重和持久化存储(存入数据库或文件)。
Pipeline 是一个普通的 Python 类,需要实现 process_item
方法。
一个简单的 Pipeline:
# my_project/pipelines.py
from itemadapter import ItemAdapter
class MyProjectPipeline:
def process_item(self, item, spider):
# 可以在这里进行数据清洗
adapter = ItemAdapter(item)
if adapter.get('price'):
adapter['price'] = float(adapter['price'])
# 必须返回 item 对象,否则后续的 Pipeline 将无法处理
return item
启用 Pipeline:你必须在 settings.py
文件中启用它。
# my_project/settings.py
ITEM_PIPELINES = {
'my_project.pipelines.MyProjectPipeline': 300, # 300是处理顺序,数字越小越先执行
}
Settings (设置)
settings.py
是 Scrapy 项目的“大脑”,你可以在这里配置各种功能。
常用设置:
# 遵守 robots.txt 协议 (君子协议)
ROBOTSTXT_OBEY = True
# 设置默认请求头,如 User-Agent
DEFAULT_REQUEST_HEADERS = {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en',
'User-Agent': 'Mozilla/5.0 ...', # 强烈建议设置
}
# 下载延迟,单位为秒,防止对服务器造成太大压力
DOWNLOAD_DELAY = 1
# 启用上面定义的 Pipeline 和设置其优先级
ITEM_PIPELINES = {
'my_project.pipelines.MyProjectPipeline': 300,
}
实战 构建 toscrape 爬虫
quotes.toscrape.com
是一个专为爬虫练习设计的网站。我们的目标是爬取所有页面的名言、作者和标签。
新建项目和爬虫
uvx scrapy startproject quotes_scraper
# 如果要使用uv管理项目,则执行下面的命令
cd quotes_scraper
uv init
uv add scrapy
新建一个爬虫文件
uv run scrapy genspider quotes quotes.toscrape.com
定义 Item
编辑 quotes_scraper/items.py
:
import scrapy
class QuoteItem(scrapy.Item):
text = scrapy.Field()
author = scrapy.Field()
tags = scrapy.Field()
编写 Spider
编辑 quotes_scraper/spiders/quotes.py
: 解析当前页数据,并找到“下一页”的链接进行跟进
import scrapy
from quotes_scraper.items import QuoteItem
class QuotesSpider(scrapy.Spider):
name = 'quotes'
start_urls = ['http://quotes.toscrape.com/']
def parse(self, response):
# 遍历页面上所有的 quote 容器
for quote in response.css('div.quote'):
item = QuoteItem()
item['text'] = quote.css('span.text::text').get()
item['author'] = quote.css('small.author::text').get()
item['tags'] = quote.css('div.tags a.tag::text').getall()
yield item
# 查找“下一页”的链接
next_page = response.css('li.next a::attr(href)').get()
if next_page is not None:
# response.follow 是 scrapy.Request 的快捷方式,会自动拼接 URL
yield response.follow(next_page, callback=self.parse)
编写 Pipeline (PostgreSQL)
安装 数据库相关依赖: pip install psycopg2
,如果使用uv管理项目,则执行下面的命令
uv add psycopg2
编辑 quotes_scraper/pipelines.py
:
import psycopg2
from itemadapter import ItemAdapter
class PostgresPipeline:
def __init__(self, postgres_host, postgres_db, postgres_user, postgres_password):
# 从 settings.py 获取数据库连接信息
self.postgres_host = postgres_host
self.postgres_db = postgres_db
self.postgres_user = postgres_user
self.postgres_password = postgres_password
self.connection = None
self.cursor = None
@classmethod
def from_crawler(cls, crawler):
# Scrapy 框架的标准方法,用于从 settings.py 创建 Pipeline 实例
return cls(
postgres_host=crawler.settings.get('POSTGRES_HOST'),
postgres_db=crawler.settings.get('POSTGRES_DB'),
postgres_user=crawler.settings.get('POSTGRES_USER'),
postgres_password=crawler.settings.get('POSTGRES_PASSWORD'),
)
def open_spider(self, spider):
"""
当爬虫启动时,此方法被调用,用于连接数据库并创建表。
"""
try:
self.connection = psycopg2.connect(
host=self.postgres_host,
dbname=self.postgres_db,
user=self.postgres_user,
password=self.postgres_password
)
self.cursor = self.connection.cursor()
# 创建表(如果不存在)
# 注意:tags 字段使用 TEXT[] 类型来存储字符串数组
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS quotes (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
author VARCHAR(255),
tags TEXT[]
)
''')
self.connection.commit()
spider.log("成功连接到 PostgreSQL 数据库。")
except psycopg2.OperationalError as e:
spider.log(f"无法连接到 PostgreSQL 数据库: {e}")
raise
def close_spider(self, spider):
"""
当爬虫关闭时,此方法被调用,用于关闭数据库连接。
"""
if self.cursor:
self.cursor.close()
if self.connection:
self.connection.close()
spider.log("已断开与 PostgreSQL 数据库的连接。")
def process_item(self, item, spider):
"""
每个 item pipeline 组件都需要调用该方法,
这个方法必须返回一个 Item (或任何继承类)对象,
或是抛出 DropItem 异常,被丢弃的 item 将不会被之后的 pipeline 组件所处理。
"""
adapter = ItemAdapter(item)
try:
# 定义插入数据的 SQL 语句
sql = "INSERT INTO quotes (text, author, tags) VALUES (%s, %s, %s)"
# 执行 SQL 语句
self.cursor.execute(sql, (
adapter.get('text'),
adapter.get('author'),
adapter.get('tags') # tags 是一个列表,psycopg2 会自动处理
))
# 提交事务
self.connection.commit()
except Exception as e:
# 如果发生错误,回滚事务
self.connection.rollback()
spider.log(f"插入数据到 PostgreSQL 时发生错误: {e}")
return item
配置 Settings
编辑 quotes_scraper/settings.py
,启用 Pipeline 并添加 数据库 配置
BOT_NAME = "quotes_scraper"
SPIDER_MODULES = ["quotes_scraper.spiders"]
NEWSPIDER_MODULE = "quotes_scraper.spiders"
# Obey robots.txt rules
ROBOTSTXT_OBEY = True
# Set settings whose default value is deprecated to a future-proof value
REQUEST_FINGERPRINTER_IMPLEMENTATION = "2.7"
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
FEED_EXPORT_ENCODING = "utf-8"
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
# 启用你的 PostgreSQL Pipeline,并设置其优先级
ITEM_PIPELINES = {
'quotes_scraper.pipelines.PostgresPipeline': 300,
}
# --- PostgreSQL 数据库配置 ---
POSTGRES_HOST = '192.168.43.236' # 你的数据库主机地址
POSTGRES_DB = 'demo' # 你的数据库名称
POSTGRES_USER = 'postgres' # 你的数据库用户名
POSTGRES_PASSWORD = '123456' # 你的数据库密码
# 其他推荐设置
DOWNLOAD_DELAY = 1 # 添加1秒的下载延迟
# DEFAULT_REQUEST_HEADERS = { ... } # 可以在这里设置 User-Agent
运行爬虫
在项目根目录下,执行scrapy crawl quotes
, 如果是uv管理的项目则执行下面的命令:
uv run scrapy crawl quotes
Scrapy 将会自动运行爬虫,一页一页地爬取,并将所有名言数据存入数据库中。