Scrapy爬虫模块

学习Scrapy框架的核心组件——爬虫模块,掌握网页解析、数据提取和爬虫逻辑设计的核心技术。

爬虫模块介绍

什么是Scrapy爬虫?

Scrapy爬虫是框架的核心组件,负责定义爬取逻辑、解析网页内容、提取数据以及生成新的请求。 每个爬虫都是一个独立的Python类,继承自scrapy.Spider基类。

# 爬虫模块的主要职责
- 定义起始URL和爬取范围
- 解析网页内容,提取结构化数据
- 生成新的请求,实现深度爬取
- 处理页面间的链接关系
- 实现数据清洗和验证逻辑
- 与管道模块协同处理数据

故事化案例:侦探调查案件

想象一下,你是一名经验丰富的侦探,正在调查一个复杂的案件:

  • 确定调查的起始地点和线索(定义起始URL)
  • 收集现场的证据和相关信息(解析网页内容)
  • 分析证据之间的联系,发现新的线索(提取数据)
  • 根据线索扩大调查范围(生成新的请求)
  • 整理证据,形成调查报告(数据清洗和存储)
  • 与其他部门协作完成调查(与管道模块协同)

在这个类比中,Scrapy爬虫就是侦探,它需要:

# 爬虫与侦探调查的类比
爬虫定义起始URL → 侦探确定调查起点
爬虫解析网页 → 侦探收集现场证据
爬虫提取数据 → 侦探分析证据信息
爬虫生成新请求 → 侦探扩大调查范围
爬虫数据清洗 → 侦探整理调查报告
爬虫与管道协同 → 侦探与部门协作

爬虫的生命周期

一个Scrapy爬虫从启动到结束会经历完整的生命周期:

爬虫生命周期阶段

  1. 初始化阶段:爬虫类被实例化,设置初始参数
  2. 启动阶段:调用start_requests()方法生成初始请求
  3. 爬取阶段:处理响应,解析数据,生成新请求
  4. 数据处理阶段:提取的数据通过管道进行处理
  5. 结束阶段:爬虫完成所有任务,调用closed()方法

爬虫类型与模式

常见爬虫类型

Scrapy支持多种爬虫类型,可以根据不同的爬取需求选择合适的模式:

通用爬虫 (Spider)

  • • 最基本的爬虫类型
  • • 适合简单的页面爬取
  • • 需要手动处理分页和链接
  • • 灵活性最高,控制力强

爬取规则爬虫 (CrawlSpider)

  • • 基于规则的自动爬取
  • • 使用LinkExtractor提取链接
  • • 适合结构化网站爬取
  • • 减少重复代码编写

XML内容爬虫 (XMLFeedSpider)

  • • 专门处理XML格式数据
  • • 支持RSS、Atom等订阅源
  • • 自动解析XML节点
  • • 适合新闻、博客等站点

站点地图爬虫 (SitemapSpider)

  • • 基于网站地图爬取
  • • 自动解析sitemap.xml
  • • 适合搜索引擎优化站点
  • • 爬取效率高,覆盖全面

CSV数据爬虫 (CSVFeedSpider)

  • • 处理CSV格式数据
  • • 自动解析表格数据
  • • 适合数据导出站点
  • • 支持自定义分隔符

自定义爬虫

  • • 继承基类自定义功能
  • • 支持复杂业务逻辑
  • • 适合特殊需求场景
  • • 灵活性最高

爬虫模式选择指南

根据不同的爬取场景,选择合适的爬虫类型:

# 爬虫类型选择指南

# 场景1:简单页面爬取
# 适用:通用爬虫 (Spider)
# 特点:手动控制,灵活性高

# 场景2:结构化网站爬取
# 适用:爬取规则爬虫 (CrawlSpider)
# 特点:自动链接提取,减少重复代码

# 场景3:新闻博客站点
# 适用:XML内容爬虫 (XMLFeedSpider)
# 特点:自动解析订阅源,适合动态内容

# 场景4:SEO优化站点
# 适用:站点地图爬虫 (SitemapSpider)
# 特点:基于sitemap,爬取效率高

# 场景5:数据导出站点
# 适用:CSV数据爬虫 (CSVFeedSpider)
# 特点:处理表格数据,适合批量处理

# 场景6:特殊需求场景
# 适用:自定义爬虫
# 特点:完全自定义,适合复杂业务

代码示例

基础爬虫示例

以下是一个简单的Scrapy爬虫示例,演示基本的爬取逻辑:

# spiders/example_spider.py
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule

class ExampleSpider(scrapy.Spider):
    name = 'example'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/']
    
    def parse(self, response):
        """解析起始页面的回调函数"""
        # 提取页面标题
        title = response.css('title::text').get()
        
        # 提取所有链接
        links = response.css('a::attr(href)').getall()
        
        # 生成数据项
        yield {
            'url': response.url,
            'title': title,
            'links_count': len(links),
            'links': links[:10]  # 只保留前10个链接
        }
        
        # 生成新的请求(深度爬取)
        for link in links[:5]:  # 只爬取前5个链接
            if link.startswith('http'):
                yield scrapy.Request(
                    url=link,
                    callback=self.parse_detail
                )
    
    def parse_detail(self, response):
        """解析详情页面的回调函数"""
        # 提取详细信息
        yield {
            'url': response.url,
            'title': response.css('title::text').get(),
            'content_length': len(response.text),
            'status_code': response.status
        }

# 使用CrawlSpider的示例
class ExampleCrawlSpider(CrawlSpider):
    name = 'example_crawl'
    allowed_domains = ['example.com']
    start_urls = ['http://www.example.com/']
    
    # 定义爬取规则
    rules = (
        # 提取所有链接,并调用parse_items方法
        Rule(LinkExtractor(), callback='parse_items', follow=True),
    )
    
    def parse_items(self, response):
        """解析每个页面的回调函数"""
        item = {}
        item['url'] = response.url
        item['title'] = response.css('title::text').get()
        
        # 提取正文内容
        item['content'] = response.css('p::text').getall()[:3]  # 前3段
        
        # 提取图片链接
        item['images'] = response.css('img::attr(src)').getall()
        
        yield item

高级爬虫示例

以下是一个更复杂的爬虫示例,包含错误处理、数据验证等功能:

# spiders/advanced_spider.py
import scrapy
import json
from urllib.parse import urljoin
from scrapy.exceptions import CloseSpider
from items import NewsItem

class AdvancedSpider(scrapy.Spider):
    name = 'advanced_news'
    
    # 配置参数
    custom_settings = {
        'CONCURRENT_REQUESTS': 8,
        'DOWNLOAD_DELAY': 1,
        'AUTOTHROTTLE_ENABLED': True,
        'ITEM_PIPELINES': {
            'pipelines.NewsPipeline': 300,
        }
    }
    
    def __init__(self, category=None, *args, **kwargs):
        super(AdvancedSpider, self).__init__(*args, **kwargs)
        self.category = category or 'technology'
        self.page_count = 0
        self.max_pages = 10  # 最大爬取页数
    
    def start_requests(self):
        """生成起始请求"""
        base_url = f'https://news.example.com/{self.category}'
        yield scrapy.Request(
            url=base_url,
            callback=self.parse_list,
            meta={'page': 1}
        )
    
    def parse_list(self, response):
        """解析新闻列表页"""
        # 检查页面是否有效
        if response.status != 200:
            self.logger.warning(f'页面访问失败: {response.url}')
            return
        
        # 提取新闻链接
        news_links = response.css('.news-item a::attr(href)').getall()
        
        if not news_links:
            self.logger.info('没有找到新闻链接,可能已到达最后一页')
            return
        
        # 生成详情页请求
        for link in news_links:
            absolute_url = urljoin(response.url, link)
            yield scrapy.Request(
                url=absolute_url,
                callback=self.parse_detail,
                errback=self.handle_error
            )
        
        # 生成下一页请求
        self.page_count += 1
        if self.page_count < self.max_pages:
            next_page = response.css('.next-page::attr(href)').get()
            if next_page:
                next_url = urljoin(response.url, next_page)
                yield scrapy.Request(
                    url=next_url,
                    callback=self.parse_list,
                    meta={'page': self.page_count + 1}
                )
    
    def parse_detail(self, response):
        """解析新闻详情页"""
        # 数据验证
        if not self._validate_response(response):
            return
        
        # 创建数据项
        item = NewsItem()
        
        # 提取字段
        item['title'] = response.css('h1.news-title::text').get()
        item['content'] = ' '.join(response.css('.news-content p::text').getall())
        item['author'] = response.css('.author::text').get()
        item['publish_time'] = response.css('.publish-time::text').get()
        item['source_url'] = response.url
        
        # 数据清洗
        item = self._clean_item(item)
        
        # 数据验证
        if self._validate_item(item):
            yield item
        else:
            self.logger.warning(f'数据验证失败: {response.url}')
    
    def handle_error(self, failure):
        """错误处理"""
        self.logger.error(f'请求失败: {failure.value}')
        
        # 可以根据错误类型进行不同处理
        if failure.check(scrapy.exceptions.IgnoreRequest):
            self.logger.warning('请求被忽略')
        elif failure.check(scrapy.exceptions.DropItem):
            self.logger.warning('数据项被丢弃')
    
    def _validate_response(self, response):
        """验证响应有效性"""
        if response.status != 200:
            return False
        
        # 检查页面内容是否有效
        title = response.css('title::text').get()
        if not title or len(title.strip()) < 5:
            return False
        
        return True
    
    def _clean_item(self, item):
        """数据清洗"""
        # 清理标题
        if item['title']:
            item['title'] = item['title'].strip()
        
        # 清理内容
        if item['content']:
            item['content'] = ' '.join(item['content'].split())
        
        return item
    
    def _validate_item(self, item):
        """数据验证"""
        # 检查必填字段
        required_fields = ['title', 'content']
        for field in required_fields:
            if not item.get(field):
                return False
        
        # 检查字段长度
        if len(item['title']) < 5 or len(item['content']) < 50:
            return False
        
        return True
    
    def closed(self, reason):
        """爬虫结束时的处理"""
        self.logger.info(f'爬虫结束,原因: {reason}')
        self.logger.info(f'总共爬取了 {self.page_count} 页')

数据项定义示例

定义爬虫使用的数据项模型:

# items.py - 数据项定义
import scrapy
from scrapy.loader import ItemLoader
from scrapy.loader.processors import TakeFirst, MapCompose, Join

class NewsItem(scrapy.Item):
    # 定义字段
    title = scrapy.Field()
    content = scrapy.Field()
    author = scrapy.Field()
    publish_time = scrapy.Field()
    source_url = scrapy.Field()
    category = scrapy.Field()
    tags = scrapy.Field()
    
    # 字段处理处理器
    title_out = TakeFirst()
    content_out = Join()
    author_out = TakeFirst()

class NewsItemLoader(ItemLoader):
    """自定义ItemLoader"""
    default_item_class = NewsItem
    
    # 字段处理
    title_in = MapCompose(str.strip)
    content_in = MapCompose(str.strip)
    author_in = MapCompose(str.strip)
    
    # 输出处理
    title_out = TakeFirst()
    content_out = Join(' ')
    author_out = TakeFirst()

# 使用ItemLoader的示例
def parse_with_loader(self, response):
    loader = NewsItemLoader(item=NewsItem(), response=response)
    
    # 使用CSS选择器提取
    loader.add_css('title', 'h1.news-title::text')
    loader.add_css('content', '.news-content p::text')
    loader.add_css('author', '.author::text')
    loader.add_value('source_url', response.url)
    
    # 返回处理后的数据项
    yield loader.load_item()

练习题

基础练习题

  1. 描述Scrapy爬虫的主要功能和作用。
  2. 解释爬虫的生命周期各个阶段。
  3. 如何定义起始URL和爬取范围?
  4. 爬虫如何与下载器、管道模块协同工作?
  5. 什么是回调函数?在爬虫中如何使用?

进阶练习题

  1. 设计一个支持深度优先和广度优先爬取的爬虫。
  2. 如何实现爬虫的错误处理和重试机制?
  3. 解释ItemLoader的作用和使用方法。
  4. 设计一个支持动态参数配置的爬虫。
  5. 如何优化爬虫的性能和内存使用?

实践练习题

  1. 创建一个新闻网站爬虫,提取标题、内容和发布时间。
  2. 编写一个电商网站爬虫,支持分页和商品详情爬取。
  3. 实现一个图片爬虫,自动下载网页中的图片。
  4. 设计一个API数据爬虫,处理JSON格式的响应。
  5. 创建一个多语言网站爬虫,支持内容翻译。

思考题

  1. 在大规模爬虫项目中,如何管理多个爬虫?
  2. 爬虫如何应对网站的反爬虫机制?
  3. 设计一个支持增量爬取的爬虫架构。
  4. 在分布式环境中,爬虫应该如何设计?
  5. 未来爬虫技术的发展趋势是什么?