Scrapy爬虫框架

在掌握了基础的爬虫工具后,我们会发现当项目变得复杂时,手动管理请求、调度、数据处理和存储会变得非常繁琐。这时,我们需要一个工程化的解决方案——Scrapy 框架。

Scrapy框架简介和安装

Scrapy 是一个为了爬取网站数据、提取结构性数据而编写的应用框架。它是一个“全家桶”式的解决方案,内置了异步处理、请求调度、数据管道、中间件等一系列强大功能。

核心思维:从“写一个爬虫脚本”转变为“构建一个爬虫项目”。Scrapy 提供了骨架,我们只需填充其中的“血肉”(即爬取逻辑和数据处理逻辑)。


核心架构与数据流

理解 Scrapy 的工作流程至关重要。它就像一个精密的自动化工厂:

数据流转过程:

Scrapy Architecture

  1. 引擎 (Engine)爬虫 (Spiders) 获取初始请求。
  2. 引擎将请求交给 调度器 (Scheduler) 去“排队”。
  3. 引擎向调度器请求下一个要爬取的 URL。
  4. 调度器返回请求,引擎将其通过 下载中间件 (Downloader Middleware) 发送给 下载器 (Downloader)
  5. 下载器完成下载,将响应通过 下载中间件 返回给引擎。
  6. 引擎将响应发送给 爬虫 进行解析。
  7. 爬虫解析响应,提取数据 (Items)新的请求 (Requests)
  8. 引擎将提取的数据发送给 项目管道 (Item Pipelines) 进行处理和存储。
  9. 引擎将新的请求发送给 调度器,重复第 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 defawait 语法,这使得编写异步逻辑更加直观(需要正确配置 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

bash
pip install scrapy

执行命令后,pip 不仅安装了 scrapy 库,还会在系统路径下创建了一个可执行脚本(scrapy.exe 或 scrapy)。这样可以在任何地方直接运行 scrapy startproject 等命令。这样的确很方便,但会导致版本冲突问题:如果项目 A 依赖 Scrapy 2.0,项目 B 依赖 Scrapy 3.0,全局安装就无法满足。

推荐使用现代包管理如 uv 来管理项目,最好不要全局安装 scrapy 这类命令行工具

创建项目

如果使用了全局安装scrapy的方式,则使用下面的方式创建项目:

bash
scrapy startproject my_project

如果想要使用uv管理项目,则需要使用下面的方式创建项目:

bash
uvx scrapy startproject my_project
# uvx 会临时下载 scrapy 到一个全局缓存中,
# 用它执行 startproject 命令来创建 my_project 目录

cd my_project
uv init
uv add scrapy
# 将 scrapy 作为这个项目的正式依赖添加进来,方便后续执行scrapy相关命令

这会自动生成一个标准的 Scrapy 项目目录结构:

text
my_project/
    scrapy.cfg            # 项目的配置文件
    my_project/           # 项目的 Python 模块
        __init__.py
        items.py          # 定义数据容器 (Item)
        middlewares.py    # 自定义中间件
        pipelines.py      # 自定义数据管道
        settings.py       # 项目的设置文件
        spiders/          # 存放爬虫代码的目录
            __init__.py

创建爬虫

进入项目目录,使用 genspider 命令创建一个新的爬虫。

bash
cd my_project
scrapy genspider example example.com

这会在 spiders/ 目录下创建一个 example.py 文件,包含一个基本的爬虫模板。

如果是 uv 管理的项目,则需要使用下面的方式创建爬虫:

bash
uv run scrapy genspider example example.com

核心组件详解

Items (数据容器)

items.py 文件用于定义要抓取的数据结构。它类似于一个字典,但提供了额外的保护机制,防止因字段名拼写错误而导致数据丢失。

定义 Item:

python
# 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:

python
# 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:

python
# 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 文件中启用它。

python
# my_project/settings.py
ITEM_PIPELINES = {
   'my_project.pipelines.MyProjectPipeline': 300, # 300是处理顺序,数字越小越先执行
}

Settings (设置)

settings.py 是 Scrapy 项目的“大脑”,你可以在这里配置各种功能。

常用设置:

python
# 遵守 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 是一个专为爬虫练习设计的网站。我们的目标是爬取所有页面的名言、作者和标签。

新建项目和爬虫

bash
uvx scrapy startproject quotes_scraper

# 如果要使用uv管理项目,则执行下面的命令
cd quotes_scraper
uv init
uv add scrapy

新建一个爬虫文件

bash
uv run scrapy genspider quotes quotes.toscrape.com

定义 Item

编辑 quotes_scraper/items.py:

python
import scrapy

class QuoteItem(scrapy.Item):
    text = scrapy.Field()
    author = scrapy.Field()
    tags = scrapy.Field()

编写 Spider

编辑 quotes_scraper/spiders/quotes.py: 解析当前页数据,并找到“下一页”的链接进行跟进

python
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管理项目,则执行下面的命令

bash
uv add psycopg2

编辑 quotes_scraper/pipelines.py:

python
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 并添加 数据库 配置

python
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管理的项目则执行下面的命令:

bash
uv run scrapy crawl quotes

Scrapy 将会自动运行爬虫,一页一页地爬取,并将所有名言数据存入数据库中。