零基础21天搞定Python分布式爬虫_分布式网络爬虫入门进阶视频教程

86 阅读5分钟

为什么需要分布式?单机的天花板

在构建分布式系统之前,我们必须理解单机爬虫的局限:

  1. 性能瓶颈:单台机器的 CPU、内存、网络带宽有限,爬取速度无法线性提升。
  2. IP 封禁风险:高频请求从同一 IP 发出,极易被目标网站识别并封禁。
  3. 单点故障:一旦爬虫程序崩溃或机器宕机,整个采集任务中断。

分布式爬虫通过将任务拆分,部署在多台机器上协同工作,有效解决了上述问题。而 Scrapy-Redis 正是实现这一目标的经典组合:Scrapy 提供强大的爬虫框架,Scrapy-Redis 则利用 Redis 作为中央调度器,实现请求的共享和去重。

核心架构:Scrapy-Redis 的工作原理

在分布式架构中,Redis 扮演着“大脑”的角色

  • 共享队列 (Queue) :所有爬虫节点从同一个 Redis 队列中获取待爬取的 URL(start_urls 和后续解析出的链接)。
  • 去重集合 (Dupefilter) :每个请求的指纹(由 URL 生成的哈希值)存储在 Redis 的集合中,确保同一 URL 不会被重复爬取。
  • 数据存储 (Item) :爬取到的数据(Item)可以统一写入 Redis,再由其他服务消费,或直接存入数据库。

这样,多个 Scrapy 爬虫实例(可以是同一台机器的多个进程,也可以是不同机器)就能并行工作,互不干扰,极大地提升了效率和稳定性。

从零开始:构建一个分布式豆瓣电影爬虫

让我们通过一个完整的例子,用代码实现一个分布式爬取豆瓣电影 Top250 的系统。

1. 环境准备与依赖

首先,确保安装了必要的库:

pip install scrapy scrapy-redis redis

2. 项目结构

distributed_spider/
├── scrapy.cfg
├── douban_spider/
│   ├── __init__.py
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders/
│       ├── __init__.py
│       └── douban_top250.py

3. 定义数据结构 (items.py)

# items.py
import scrapy

class MovieItem(scrapy.Item):
    # 电影排名
    ranking = scrapy.Field()
    # 电影名称
    title = scrapy.Field()
    # 评分
    score = scrapy.Field()
    # 评价人数
    votes = scrapy.Field()
    # 引用语(经典台词)
    quote = scrapy.Field()
    # 电影链接
    url = scrapy.Field()

4. 配置文件 (settings.py) - 分布式的核心

# settings.py
import os

# --- Scrapy 基础配置 ---
BOT_NAME = 'douban_spider'

SPIDER_MODULES = ['douban_spider.spiders']
NEWSPIDER_MODULE = 'douban_spider.spiders'

# 避免法律风险,遵守 robots.txt
ROBOTSTXT_OBEY = True

# 下载延迟,避免对目标网站造成过大压力
DOWNLOAD_DELAY = 2

# 并发请求数量
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8

# --- Scrapy-Redis 分布式配置 ---
# 指定调度器,使用 Scrapy-Redis 提供的调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 指定去重类
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 指定 Redis 连接信息
REDIS_HOST = '127.0.0.1'  # 如果 Redis 在远程服务器,改为服务器 IP
REDIS_PORT = 6379
# REDIS_PASSWORD = 'your_password'  # 如果设置了密码

# 是否在爬虫结束后,保持 Redis 中的请求队列和去重集合
# 开发调试时设为 True,方便复用;生产环境可设为 False
SCHEDULER_PERSIST = True

# 请求队列的优先级排序
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderPriorityQueue' # 优先级队列
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderQueue' # 队列(FIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderStack' # 栈(LIFO)

# --- 数据管道 (Pipeline) ---
ITEM_PIPELINES = {
    # 可以将 Item 写入 Redis,供其他服务处理
    'scrapy_redis.pipelines.RedisPipeline': 300,
    # 或者自定义管道存入数据库
    # 'douban_spider.pipelines.DoubanPipeline': 300,
}

# 日志级别
LOG_LEVEL = 'INFO'

# 用户代理,可以设置为随机 User-Agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'

关键配置解析:

  • SCHEDULERDUPEFILTER_CLASS:这两项是启用 Scrapy-Redis 分布式能力的核心,它们将默认的内存调度和去重替换为基于 Redis 的实现。
  • SCHEDULER_PERSIST:设为 True 时,即使爬虫停止,队列中的 URL 也不会丢失,下次启动可继续爬取。
  • SCHEDULER_QUEUE_CLASS:决定了 URL 的处理顺序。SpiderQueue 是先进先出,适合广度优先;SpiderStack 是后进先出,适合深度优先。

5. 编写爬虫 (douban_top250.py)

# spiders/douban_top250.py
import scrapy
from scrapy_redis.spiders import RedisSpider  # 使用 RedisSpider
from douban_spider.items import MovieItem

class DoubanTop250Spider(RedisSpider):
    name = 'douban_top250'
    
    # 注意:这里不再需要 start_urls!
    # 分布式爬虫的起始 URL 由 Redis 提供
    # start_urls = ['https://movie.douban.com/top250']
    
    # Redis 中存放起始 URL 的键名
    redis_key = 'douban:start_urls'

    def parse(self, response):
        """
        解析电影列表页
        """
        # 提取电影列表
        movie_list = response.css('ol.grid_view li')
        
        for movie in movie_list:
            item = MovieItem()
            # 排名
            item['ranking'] = movie.css('div.pic em::text').get()
            # 名称
            item['title'] = movie.css('span.title::text').get()
            # 评分
            item['score'] = movie.css('span.rating_num::text').get()
            # 评价人数
            item['votes'] = movie.css('div.star span::text').re_first(r'(\d+)人评价')
            # 引用语
            item['quote'] = movie.css('span.inq::text').get()
            # 电影链接
            item['url'] = movie.css('div.pic a::attr(href)').get()
            
            yield item

        # 提取下一页链接并生成请求
        next_page = response.css('span.next a::attr(href)').get()
        if next_page:
            next_page_url = response.urljoin(next_page)
            yield scrapy.Request(next_page_url, callback=self.parse)

    def make_requests_from_url(self, url):
        """
        可选:用于处理从 Redis 读取的 URL
        可以在这里添加额外的参数,如 headers、cookies
        """
        return scrapy.Request(url=url, dont_filter=True)

关键点:

  • 继承 RedisSpider 而非 scrapy.Spider
  • 使用 redis_key 指定 Redis 中存放起始 URL 的键。
  • 爬虫启动后,会自动从 Redis 的 douban:start_urls 队列中获取 URL 并开始爬取。

6. 启动分布式爬虫

  1. 启动 Redis 服务

    redis-server
    
  2. 向 Redis 中添加起始 URL

    # 使用 redis-cli
    redis-cli
    > LPUSH douban:start_urls https://movie.douban.com/top250
    
  3. 启动多个爬虫实例: 在不同的终端或不同的机器上,运行:

    scrapy crawl douban_top250
    

    你可以同时启动多个实例,它们都会从同一个 Redis 队列中取任务,实现分布式爬取。

  4. 监控: 使用 redis-cli 监控队列:

    redis-cli
    > LLEN douban:start_urls  # 查看队列长度
    > SCARD douban:requests:dupefilter  # 查看去重集合大小
    

总结:分布式爬虫的实战心得

通过这个项目,我深刻体会到:

  • Redis 是灵魂:它简单、高效、稳定,完美适合作为分布式爬虫的调度中心。
  • 去重是关键:Scrapy-Redis 的 RFPDupeFilter 基于 Redis 的 set,能有效避免重复爬取,节省资源。
  • 可扩展性:增加爬虫节点非常简单,只需部署代码并启动实例,系统会自动负载均衡。
  • 容错性:单个爬虫节点崩溃不影响整体任务,其他节点会继续处理队列中的请求。

当然,真正的生产环境还需考虑更多:使用代理 IP 池应对反爬、将数据存入 MySQL/MongoDB、使用 Docker 容器化部署、结合 Celery 进行任务调度等。但 Scrapy-Redis 为我们提供了一个坚实而优雅的起点。掌握它,你就真正迈入了高效、稳定的数据采集世界。