Spider实战指南 - Request、Response、yield深度解析与爬虫逻辑实现

📂 所属阶段:第一阶段 — 初出茅庐(框架核心篇)
🔗 相关章节:创建你的首个工程 · Selector 选择器

Spider是Scrapy框架的灵魂。它决定了爬虫去哪里、怎么解析、提取什么数据。如果你把Scrapy比作一辆车,Spider就是驾驶员——控制着方向盘、油门和刹车。本文将深入解析Spider的几个核心要素,帮你写出更高效、更健壮的爬虫。

目录

Spider基础结构

每个Spider都是scrapy.Spider的子类,自带一套完整的生命周期。下面是一个最精简的模板,包含了所有必需品。

基础Spider模板

import scrapy

class ExampleSpider(scrapy.Spider):
    name = 'example'                     # 爬虫名称,启动时要用
    allowed_domains = ['example.com']    # 允许爬取的域名
    start_urls = ['http://example.com']  # 起始URL列表

    def parse(self, response):
        # 第一步:提取我们想要的数据
        for item in response.css('div.item'):
            yield {
                'title': item.css('h2::text').get(),
                'price': item.css('span.price::text').get(),
                'url': response.url
            }

        # 第二步:找到“下一页”链接,继续爬取
        next_page = response.css('a.next::attr(href)').get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

划重点

  • name:爬虫的唯一身份证,启动时通过 scrapy crawl name 来告诉Scrapy要用哪一个。
  • allowed_domains:设置了以后,Scrapy会自动过滤掉域名外的请求,防止爬虫跑偏。
  • start_urls:启动时第一个被请求的页面,Scrapy会把它封装成Request发送出去,响应默认交给parse方法处理。
  • custom_settings:如果你想为某个Spider单独设置并发量、延迟等参数,可以给它配一个专属的配置字典,优先级高于全局设置。

Request详解

Request对象代表一次HTTP请求。我们不仅在启动时需要它,在运行过程中发现新链接时也要用它来“下订单”。

Request基本用法

import scrapy

class RequestSpider(scrapy.Spider):
    name = 'request_example'

    def start_requests(self):
        # 1. 普通GET请求
        yield scrapy.Request(
            url='http://example.com/api/data',
            callback=self.parse_get_data,
            headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Authorization': 'Bearer token123'
            },
            meta={'request_type': 'api_call'}   # meta像是随身携带的“便签”,可以往后传递自定义信息
        )

        # 2. POST请求
        yield scrapy.Request(
            url='http://example.com/login',
            callback=self.parse_login,
            method='POST',
            headers={'Content-Type': 'application/json'},
            body='{"username": "user", "password": "pass"}'
        )

    def parse_get_data(self, response):
        # 根据Content-Type判断响应格式
        content_type = response.headers.get('Content-Type', b'').decode()
        data = response.json() if 'application/json' in content_type else response.text
        yield {'api_data': data}

核心参数速查

  • url:要请求的页面地址(必填)。
  • callback:下载完成后把响应交给哪个函数处理;不指定则默认调用parse
  • method'GET'(默认)或'POST',甚至可以设'PUT''DELETE'等。
  • headers:携带自定义请求头,比如UA、Cookie、Token。
  • body:POST请求时用来传递数据的,字符串或字节都可以。
  • meta:一个字典,数据会从请求流向响应,非常适合跨函数传递页码、分类名等上下文。
  • dont_filter=True:可以强制Scrapy不检查这个URL是否已经被抓过,常用于需要重复请求的接口。

Response详解

当请求被成功下载后,Scrapy会构造一个Response对象交给我们。这个对象就是解析数据的主战场。

Response常用属性与方法

def parse_response_attributes(self, response):
    # ---- 基本属性 ----
    url = response.url                # 当前响应的最终URL(可能发生过重定向)
    status = response.status          # HTTP状态码,200表示成功
    headers = response.headers        # 响应头,类似一个字典
    text = response.text              # 响应正文(字符串)
    meta = response.meta              # 从对应Request继承来的meta数据

    # ---- 数据提取 ----
    titles = response.css('h1::text').getall()        # CSS选择器
    links = response.xpath('//a/@href').getall()      # XPath选择器

    # ---- 链接处理 ----
    next_page = response.follow('next.html', callback=self.parse_next)  # 省去拼接URL的麻烦
    absolute_url = response.urljoin('/relative/path')  # 手动把相对路径变绝对路径

    # ---- JSON响应 ----
    content_type = response.headers.get('Content-Type', b'').decode()
    if 'application/json' in content_type:
        json_data = response.json()

小窍门

  • 选择器返回的都是选择器列表,一定要调用.get()(取第一个)或.getall()(取全部)才能拿到真正的文本。
  • 处理需要登录或API返回的JSON时,先用json()把响应转成Python字典,解析起来非常顺手。
  • response.urljoin()response.follow() 都会基于当前页面地址解析相对路径,后者更简洁,推荐优先使用。

yield的使用技巧

在Scrapy中,yield是一个超级重要的关键字。它把函数变成了生成器(generator),让我们可以一个接一个地输出数据或者请求,而不是一次性返回所有结果。这样做的好处是内存友好,而且便于异步处理。

yield的三大用途

def parse_with_yield_examples(self, response):
    # ① 产出数据(dict或Item对象)
    yield {
        'title': response.css('h1::text').get(),
        'url': response.url
    }

    # ② 产出新的Request(手动构造)
    next_page = response.css('a.next::attr(href)').get()
    if next_page:
        yield scrapy.Request(
            url=response.urljoin(next_page),
            callback=self.parse,
            meta={'page': response.meta.get('page', 1) + 1}
        )

    # ③ 产出response.follow的结果(更简洁的Request)
    product_links = response.css('a.product-link::attr(href)').getall()
    for link in product_links:
        yield response.follow(link, callback=self.parse_product)

为什么不用return?
如果函数里有很多个商品,用return [dict1, dict2, ...]会一下子创建一大串列表,占满内存;而yield一个就处理一个,数据像流水一样被送进后续的Pipeline或文件,这正是Scrapy高性能的奥秘之一。

爬虫解析逻辑

真实网站经常有层级结构:分类页 → 列表页 → 详情页。Scrapy的多层级解析通过回调函数链来实现,中间用meta传递上下文。

多层级解析示例:分类→产品列表→产品详情

class MultiLevelParsingSpider(scrapy.Spider):
    name = 'multi_level_parsing'

    def start_requests(self):
        yield scrapy.Request('http://example.com/categories',
                             callback=self.parse_categories)

    def parse_categories(self, response):
        """第一层:解析分类页面,获取所有分类链接"""
        for category_link in response.css('a.category-link::attr(href)').getall():
            yield response.follow(
                category_link,
                callback=self.parse_products,
                meta={'category': response.css('h1::text').get()}
            )

    def parse_products(self, response):
        """第二层:解析产品列表页,抓取产品链接并处理翻页"""
        category = response.meta['category']

        # 产品链接
        for product_link in response.css('a.product-link::attr(href)').getall():
            yield response.follow(
                product_link,
                callback=self.parse_product_detail,
                meta={'category': category}
            )

        # 翻页 — 注意meta要继续往下传
        next_page = response.css('a.next::attr(href)').get()
        if next_page:
            yield response.follow(next_page,
                                  callback=self.parse_products,
                                  meta={'category': category})

    def parse_product_detail(self, response):
        """第三层:详情页,提取完整信息并产出最终数据"""
        yield {
            'category': response.meta['category'],
            'url': response.url,
            'name': response.css('h1.product-title::text').get(),
            'price': response.css('.price::text').get(),
            'description': response.css('.description::text').get()
        }

要点提醒

  • meta 字典在回调链中默认会一路传递,只要你在生成Request时设置了它,响应里就能拿到。
  • 处理分页时,一定要把当前的meta原样传给下一页的请求,否则上下文丢失,数据就“断了线”。

链接跟随策略

Scrapy提供了多种方式帮我们处理新链接,选对方法能让代码干净不少。

response.follow() vs scrapy.Request

def comparison_of_link_following(self, response):
    # ✅ 方法1:response.follow() — 自动处理相对URL,推荐
    next_page = response.css('a.next::attr(href)').get()
    if next_page:
        yield response.follow(next_page, callback=self.parse)

    # ✅ 方法2:scrapy.Request — 需要手动调用urljoin,更灵活
    next_page = response.css('a.next::attr(href)').get()
    if next_page:
        yield scrapy.Request(url=response.urljoin(next_page),
                             callback=self.parse)
  • response.follow()内部会自动做URL拼接,代码更清爽,是你“默认的选择”。
  • scrapy.Request适合非常规场景,比如需要自定义errback、设置特殊meta或者强制dont_filter时。

智能链接提取:LinkExtractor

当页面比较复杂,你想根据规律批量提取链接时,LinkExtractor就是一把利器。

from scrapy.linkextractors import LinkExtractor

class AdvancedLinkExtractionSpider(scrapy.Spider):
    name = 'advanced_links'

    def parse(self, response):
        # 定义一个提取器:只抓分类页链接,排除后台管理页面
        link_extractor = LinkExtractor(
            allow=r'/category/\w+',        # 允许的URL正则模式
            deny=r'/admin/',               # 需要忽略的模式
            restrict_css='.main-content'   # 只在主内容区查找链接
        )

        links = link_extractor.extract_links(response)
        for link in links:
            yield response.follow(link.url, callback=self.parse_category)

数据提取技术

提取出来的原始数据往往带有杂质——空格、乱码、无意义的默认值……在产出之前把它们整理干净,能让后续的数据处理事半功倍。

数据清洗与验证

import re

class DataCleaningSpider(scrapy.Spider):
    name = 'data_cleaning'

    def parse(self, response):
        for product in response.css('div.product'):
            raw_title = product.css('.title::text').get()
            raw_price = product.css('.price::text').get()

            # 清洁数据
            cleaned_title = self.clean_text(raw_title)
            cleaned_price = self.clean_price(raw_price)

            # 只产出合格的数据
            if self.validate_data(cleaned_title, cleaned_price):
                yield {
                    'title': cleaned_title,
                    'price': cleaned_price,
                    'url': response.url
                }

    def clean_text(self, text):
        """去空白、合并空格"""
        if not text:
            return ''
        return re.sub(r'\s+', ' ', text.strip())

    def clean_price(self, price_str):
        """从 '¥99.00元' 这样的字符串里提取数字"""
        if not price_str:
            return None
        numbers = re.findall(r'\d+\.?\d*', price_str.replace(',', ''))
        return float(numbers[0]) if numbers else None

    def validate_data(self, title, price):
        if not title or len(title) < 2:
            return False
        if price is None or price <= 0:
            return False
        return True

最佳实践

  • 将清洗逻辑封装到独立的方法里,保持parse函数简洁清晰。
  • 养成先清洗后验证的习惯,只有通过校验的数据才产出,脏数据直接丢弃或记录日志。
  • 使用正则表达式时建议配合re.subre.findall,它们在处理各种奇形怪状的网页数据时非常靠谱。

错误处理与异常捕获

网络请求不可能百分百成功,404、500、连接超时随时可能发生。Scrapy提供了errback让我们优雅地处理失败请求。

给请求装上“安全气囊”

class ErrorHandlingSpider(scrapy.Spider):
    name = 'error_handling'

    def start_requests(self):
        urls = [
            'http://example.com/valid-page',
            'http://example.com/404-page',
        ]
        for url in urls:
            yield scrapy.Request(
                url=url,
                callback=self.parse,
                errback=self.handle_error      # 出错时走这里
            )

    def parse(self, response):
        if response.status == 200:
            yield {
                'url': response.url,
                'status': response.status,
                'title': response.css('title::text').get(),
                'success': True
            }

    def handle_error(self, failure):
        """错误处理函数会收到一个Failure对象"""
        request = failure.request
        # 记录日志,方便追踪问题
        self.logger.error(f"请求失败:{request.url},错误:{failure.value}")

        # 也可以产出失败记录,用于事后排查
        yield {
            'url': request.url,
            'error': str(failure.value),
            'success': False
        }

还要做什么?

  • 对部分失败,可以在errback里重新生成一个相同的Request(注意设置dont_filter=True),实现简单重试。
  • 配合中间件(如RetryMiddleware)可以更系统地处理重试策略。

💡 核心要点:Spider是Scrapy的核心,掌握好Request、Response、yield就抓住了爬虫的命脉。再叠加数据清洗、错误处理和多层级解析,你就能写出稳定、可维护的爬虫系统。记住:代码写得好,老板下班早!

🔗 相关教程推荐