在数据采集领域,Scrapy 是 Python 生态中无可争议的 “爬虫利器”—— 它自带高效的异步下载、灵活的解析规则和完善的中间件机制,能轻松应对各类网站的数据抓取需求。但 Scrapy 的短板也同样明显:它默认依赖命令行启动,缺乏直观的管理界面,爬取进度、任务状态和结果数据都难以可视化监控;而 Django 作为 “全能型 Web 框架”,凭借自带的 Admin 后台、ORM 数据模型和灵活的视图系统,恰好能弥补这些不足。

当 Scrapy 的 “数据采集能力” 与 Django 的 “Web 管理能力” 相结合,我们就能打造一个集 “爬虫配置 - 任务调度 - 数据存储 - 可视化监控” 于一体的完整爬虫平台。本文将从环境搭建到功能落地,手把手教你实现这一方案。

一、技术栈与核心思路

在动手前,我们先明确技术栈选型和整体架构,避免后续开发 “无章可循”。

1. 核心技术栈

工具 / 框架作用
Python 3.8+基础开发语言(确保兼容 Scrapy 和 Django 的最新稳定版)
Scrapy 2.8+负责核心数据爬取(异步下载、页面解析、数据提取)
Django 4.2+提供 Web 管理界面(Admin 后台)、数据模型(ORM)和视图接口
Celery 5.3+处理异步任务(避免爬虫阻塞 Web 请求,支持定时调度)
Redis 6.0+作为 Celery 的消息队列(存储爬虫任务)和缓存(临时存储爬取进度)
ECharts 5.4+实现数据可视化(任务状态饼图、爬取量柱状图等)
MySQL 8.0+持久化存储爬虫配置、任务记录和爬取结果(替代 Django 默认的 SQLite)

2. 整体架构思路

我们将整个平台拆分为 3 个核心模块,各司其职又相互联动:

  1. Django 层:负责 “管理” 和 “展示”—— 通过 Admin 后台配置爬虫规则、发起爬取任务;通过视图和模板展示任务状态、爬取结果的可视化图表。
  2. Scrapy 层:负责 “采集”—— 接收 Django 传递的任务参数(如目标 URL、解析规则),执行爬取逻辑,将结果回传给 Django 的数据库。
  3. Celery 层:负责 “调度”—— 作为中间件,接收 Django 发起的爬虫任务,异步触发 Scrapy 爬取,避免 Web 请求阻塞;同时支持定时任务(如每天凌晨爬取数据)。

二、分步实现:从环境到功能

1. 第一步:搭建基础环境

(1)创建虚拟环境(避免依赖冲突)

bash

# 安装虚拟环境工具(若未安装)
pip install virtualenv
# 创建虚拟环境
virtualenv scrapy_django_env
# 激活虚拟环境(Windows)
scrapy_django_env\Scripts\activate
# 激活虚拟环境(Mac/Linux)
source scrapy_django_env/bin/activate
(2)安装依赖库

bash

# 安装Django、Scrapy、Celery等核心库
pip install django==4.2.7 scrapy==2.11.0 celery==5.3.6 redis==4.6.0 pymysql==1.1.0 django-celery-beat==2.5.0
(3)初始化项目结构

我们采用 “Django 为主项目,Scrapy 作为子模块” 的结构,确保 Django 能直接调用 Scrapy:

bash

# 1. 创建Django主项目
django-admin startproject scrapy_platform
cd scrapy_platform

# 2. 创建Django App(负责爬虫管理和可视化)
python manage.py startapp spider_manager

# 3. 在spider_manager目录下创建Scrapy项目(作为子模块)
cd spider_manager
scrapy startproject spider_core
cd .. && cd ..

最终项目结构如下(关键目录标注):

plaintext

scrapy_platform/          # Django主项目
├── scrapy_platform/      # Django项目配置
│   ├── settings.py       # 全局配置(数据库、Celery等)
│   └── urls.py           # 主路由
├── spider_manager/       # 核心App(爬虫管理)
│   ├── models.py         # 数据模型(任务、结果、配置)
│   ├── admin.py          # Admin后台配置
│   ├── views.py          # 视图(可视化、任务触发)
│   ├── templates/        # 模板(可视化页面)
│   └── spider_core/      # Scrapy子模块
│       ├── spiders/      # 爬虫脚本
│       ├── items.py      # 爬取数据结构
│       └── pipelines.py  # 数据管道(对接Django ORM)
└── manage.py             # Django命令行工具

2. 第二步:设计 Django 数据模型(ORM)

数据模型是平台的 “骨架”,我们需要存储 3 类核心数据:爬虫配置(爬什么、怎么爬)、爬取任务(任务状态、时间)、爬取结果(具体数据)。

打开spider_manager/models.py,定义如下模型:

python

from django.db import models
from django.utils import timezone

# 1. 爬虫配置表(存储爬虫的固定规则)
class SpiderConfig(models.Model):
    SPIDER_TYPE_CHOICES = (
        ('article', '文章爬取'),
        ('product', '商品爬取'),
        ('comment', '评论爬取'),
    )
    name = models.CharField(max_length=100, verbose_name="爬虫名称")  # 如“知乎文章爬虫”
    spider_type = models.CharField(max_length=20, choices=SPIDER_TYPE_CHOICES, verbose_name="爬虫类型")
    start_url = models.URLField(verbose_name="起始URL")  # 爬虫入口
    parse_rule = models.JSONField(verbose_name="解析规则")  # 存储CSS/XPath规则(如{"title": "h1::text"})
    is_active = models.BooleanField(default=True, verbose_name="是否启用")
    create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")

    class Meta:
        verbose_name = "爬虫配置"
        verbose_name_plural = "爬虫配置"

    def __str__(self):
        return self.name

# 2. 爬取任务表(存储每次爬取的状态和日志)
class SpiderTask(models.Model):
    TASK_STATUS_CHOICES = (
        ('pending', '等待中'),
        ('running', '运行中'),
        ('success', '成功'),
        ('failed', '失败'),
    )
    config = models.ForeignKey(SpiderConfig, on_delete=models.CASCADE, related_name="tasks", verbose_name="关联配置")
    task_id = models.CharField(max_length=50, unique=True, verbose_name="Celery任务ID")  # 关联Celery任务
    status = models.CharField(max_length=20, choices=TASK_STATUS_CHOICES, default='pending', verbose_name="任务状态")
    start_time = models.DateTimeField(null=True, blank=True, verbose_name="开始时间")
    end_time = models.DateTimeField(null=True, blank=True, verbose_name="结束时间")
    log = models.TextField(null=True, blank=True, verbose_name="爬取日志")  # 存储错误日志或进度

    class Meta:
        verbose_name = "爬取任务"
        verbose_name_plural = "爬取任务"
        ordering = ['-start_time']

    def __str__(self):
        return f"{self.config.name} - {self.status}"

# 3. 爬取结果表(存储具体的爬取数据)
class SpiderResult(models.Model):
    task = models.ForeignKey(SpiderTask, on_delete=models.CASCADE, related_name="results", verbose_name="关联任务")
    data = models.JSONField(verbose_name="爬取数据")  # 存储结构化数据(如{"title": "xxx", "content": "xxx"})
    crawl_time = models.DateTimeField(default=timezone.now, verbose_name="爬取时间")
    url = models.URLField(null=True, blank=True, verbose_name="数据来源URL")

    class Meta:
        verbose_name = "爬取结果"
        verbose_name_plural = "爬取结果"
        ordering = ['-crawl_time']

    def __str__(self):
        return f"任务{self.task.id} - {self.url[:50]}"

定义完成后,执行数据库迁移,生成表结构:

bash

# 生成迁移文件
python manage.py makemigrations
# 执行迁移(创建表)
python manage.py migrate

3. 第三步:Scrapy 与 Django 的核心整合

这是整个平台的 “关键链路”—— 需要实现两个核心目标:Django 触发 Scrapy 爬取Scrapy 将结果存入 Django 数据库

(1)配置 Scrapy 对接 Django ORM

Scrapy 默认通过pipelines.py处理爬取结果,我们需要自定义一个 Pipeline,将 Scrapy 的 Item 数据存入 Django 的SpiderResult模型。

首先,在 Scrapy 的settings.py(路径:spider_manager/spider_core/spider_core/settings.py)中添加 Django 环境变量,确保 Scrapy 能找到 Django 模型:

python

# 配置Django环境(关键!否则Scrapy无法导入Django模型)
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'scrapy_platform.settings')
django.setup()

# 启用自定义Pipeline(用于对接Django ORM)
ITEM_PIPELINES = {
    'spider_manager.spider_core.spider_core.pipelines.DjangoORM pipeline': 300,
}

然后,修改pipelines.py,实现数据写入逻辑:

python

from spider_manager.models import SpiderTask, SpiderResult

class DjangoORMPipeline:
    def __init__(self, task_id):
        self.task_id = task_id  # 接收Celery传递的任务ID,关联到具体任务

    @classmethod
    def from_crawler(cls, crawler):
        # 从Scrapy的配置中获取任务ID(后续由Celery触发时传入)
        task_id = crawler.settings.get('TASK_ID')
        return cls(task_id)

    def process_item(self, item, spider):
        """将Scrapy Item存入Django的SpiderResult表"""
        try:
            # 找到当前任务(通过task_id)
            task = SpiderTask.objects.get(task_id=self.task_id)
            # 创建爬取结果
            SpiderResult.objects.create(
                task=task,
                data=dict(item),  # 将Scrapy Item转为字典
                url=item.get('url', '')
            )
            return item
        except Exception as e:
            # 若出错,将错误日志写入任务表
            task.log += f"\n{str(e)}"
            task.status = 'failed'
            task.save()
            raise e
(2)用 Celery 实现异步任务触发

如果直接在 Django 视图中调用 Scrapy,会导致 Web 请求阻塞(爬取可能耗时几分钟甚至几小时)。因此,我们用 Celery 将爬虫任务转为异步执行。

首先,在 Django 主项目(scrapy_platform)下创建celery.py,配置 Celery:

python

import os
from celery import Celery

# 设置Django环境
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'scrapy_platform.settings')

# 初始化Celery
app = Celery('scrapy_platform')
# 从Django settings中读取Celery配置
app.config_from_object('django.conf:settings', namespace='CELERY')
# 自动发现所有Django App中的tasks.py
app.autodiscover_tasks()

然后,在spider_manager目录下创建tasks.py,定义 Celery 任务(触发 Scrapy 爬取):

python

from celery import shared_task
from scrapy.crawler import CrawlerProcess
from scrapy.utils.project import get_project_settings
from spider_manager.spider_core.spider_core.spiders.generic_spider import GenericSpider  # 自定义的通用爬虫
from spider_manager.models import SpiderTask, SpiderConfig
from datetime import datetime

@shared_task(bind=True, name="run_spider_task")
def run_spider_task(self, config_id):
    """
    异步执行爬虫任务
    :param config_id: 爬虫配置ID(从Django Admin传递)
    :return: 任务结果
    """
    try:
        # 1. 获取爬虫配置
        config = SpiderConfig.objects.get(id=config_id, is_active=True)
        # 2. 创建任务记录(状态设为“运行中”)
        task = SpiderTask.objects.create(
            config=config,
            task_id=self.request.id,  # Celery任务ID
            status='running',
            start_time=datetime.now()
        )

        # 3. 配置Scrapy,传入任务ID和爬取规则
        scrapy_settings = get_project_settings()
        scrapy_settings.set('TASK_ID', self.request.id)  # 传递任务ID给Pipeline
        scrapy_settings.set('PARSE_RULE', config.parse_rule)  # 传递解析规则给爬虫

        # 4. 启动Scrapy爬虫
        process = CrawlerProcess(scrapy_settings)
        process.crawl(GenericSpider, start_url=config.start_url)  # 传入起始URL
        process.start()  # 阻塞直到爬取完成

        # 5. 更新任务状态为“成功”
        task.status = 'success'
        task.end_time = datetime.now()
        task.log += "\n爬取完成!"
        task.save()
        return f"Spider {config.name} finished successfully"

    except Exception as e:
        # 6. 若出错,更新任务状态为“失败”并记录日志
        task.status = 'failed'
        task.end_time = datetime.now()
        task.log += f"\nError: {str(e)}"
        task.save()
        raise e
(3)编写通用 Scrapy 爬虫

为了适配不同的爬取需求(如文章、商品),我们编写一个GenericSpider(通用爬虫),通过 Django 传递的PARSE_RULE动态解析页面。

spider_manager/spider_core/spider_core/spiders/目录下创建generic_spider.py

python

import scrapy
from scrapy.settings import settings

class GenericSpider(scrapy.Spider):
    name = "generic_spider"  # 爬虫名称(固定)

    def __init__(self, start_url, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.start_urls = [start_url]  # 从Celery传递的起始URL
        self.parse_rule = settings.get('PARSE_RULE')  # 从Scrapy配置获取解析规则

    def parse(self, response):
        """动态解析页面(根据PARSE_RULE提取数据)"""
        # 示例:解析规则为{"title": "h1::text", "content": ".content::text", "url": "url"}
        item = {}
        for field, rule in self.parse_rule.items():
            if field == 'url':
                item[field] = response.url  # 直接获取当前页面URL
            else:
                # 支持CSS选择器(如"h1::text")或XPath(如"//h1/text()")
                if rule.startswith('//'):
                    item[field] = response.xpath(rule).get(default='').strip()
                else:
                    item[field] = response.css(rule).get(default='').strip()
        
        yield item  # 传递给Pipeline,存入Django数据库

        # 可选:自动跟进页面中的链接(如分页)
        next_page = response.css('.next-page::attr(href)').get()
        if next_page:
            yield response.follow(next_page, self.parse)

4. 第四步:定制 Django Admin 管理界面

Django 自带的 Admin 后台是 “开箱即用的管理工具”,我们只需注册模型并自定义界面,就能实现 “爬虫配置管理” 和 “任务触发”。

打开spider_manager/admin.py,编写如下代码:

python

from django.contrib import admin
from .models import SpiderConfig, SpiderTask, SpiderResult
from .tasks import run_spider_task

# 1. 注册SpiderConfig(爬虫配置)
@admin.register(SpiderConfig)
class SpiderConfigAdmin(admin.ModelAdmin):
    list_display = ('name', 'spider_type', 'start_url', 'is_active', 'create_time')  # 列表页显示字段
    search_fields = ('name', 'start_url')  # 搜索字段
    list_filter = ('spider_type', 'is_active')  # 过滤字段
    fieldsets = (
        ('基本信息', {
            'fields': ('name', 'spider_type', 'is_active')
        }),
        ('爬取规则', {
            'fields': ('start_url', 'parse_rule'),
            'description': '解析规则格式:{"字段名": "CSS/XPath规则"},如{"title": "h1::text"}'
        }),
    )  # 编辑页字段分组

# 2. 注册SpiderTask(爬取任务),并添加“触发爬虫”的自定义动作
@admin.register(SpiderTask)
class SpiderTaskAdmin(admin.ModelAdmin):
    list_display = ('config', 'task_id', 'status', 'start_time', 'end_time')
    search_fields = ('config__name', 'task_id')
    list_filter = ('status', 'start_time')
    readonly_fields = ('task_id', 'start_time', 'end_time', 'log')  # 只读字段(不允许编辑)

    # 自定义动作:批量触发爬虫任务
    actions = ['trigger_spider']

    def trigger_spider(self, request, queryset):
        """触发选中配置的爬虫任务"""
        for config in queryset:
            # 调用Celery异步任务
            run_spider_task.delay(config_id=config.id)
        self.message_user(request, f"已触发{queryset.count()}个爬虫任务,可在任务列表查看进度!")

    trigger_spider.short_description = "触发选中配置的爬虫任务"  # 动作名称

# 3. 注册SpiderResult(爬取结果)
@admin.register(SpiderResult)
class SpiderResultAdmin(admin.ModelAdmin):
    list_display = ('task', 'url', 'crawl_time')
    search_fields = ('url', 'data__title')  # 支持JSON字段的搜索(Django 4.0+)
    list_filter = ('crawl_time',)
    readonly_fields = ('task', 'data', 'url', 'crawl_time')
    # 优化JSON字段显示(用更友好的格式展示)
    def formatted_data(self, obj):
        import json
        return json.dumps(obj.data, ensure_ascii=False, indent=2)
    formatted_data.short_description = "爬取数据(格式化)"
    fieldsets = (
        ('关联信息', {
            'fields': ('task', 'url', 'crawl_time')
        }),
        ('爬取数据', {
            'fields': (formatted_data,)
        }),
    )

此时,启动 Django 服务器,访问http://127.0.0.1:8000/admin(需先创建超级用户:python manage.py createsuperuser),就能看到完整的管理界面:

  • 在 “爬虫配置” 中添加规则(如 “知乎文章爬虫”,解析规则为{"title": "h1.Post-Title::text", "content": ".Post-Content::text", "url": "url"});
  • 选中配置,点击 “触发选中配置的爬虫任务”,即可通过 Celery 异步启动 Scrapy;
  • 在 “爬取任务” 中查看任务状态,在 “爬取结果” 中查看具体数据。

5. 第五步:实现数据可视化(ECharts)

管理界面解决了 “功能” 问题,可视化则解决 “直观性” 问题。我们用 ECharts 绘制两个核心图表:任务状态分布饼图每日爬取量柱状图

(1)编写可视化视图

打开spider_manager/views.py,定义视图函数,传递统计数据给模板:

python

from django.shortcuts import render
from django.db.models import Count, DateField
from django.db.models.functions import TruncDate
from .models import SpiderTask, SpiderResult

def dashboard(request):
    """可视化仪表盘视图"""
    # 1. 统计任务状态分布(饼图数据)
    task_status_data = SpiderTask.objects.values('status').annotate(count=Count('id'))
    status_labels = [item['status'] for item in task_status_data]
    status_counts = [item['count'] for item in task_status_data]

    # 2. 统计每日爬取量(柱状图数据)
    daily_result_data = SpiderResult.objects.annotate(
        crawl_date=TruncDate('crawl_time')  # 按日期分组
    ).values('crawl_date').annotate(count=Count('id')).order_by('crawl_date')
    date_labels = [item['crawl_date'].strftime('%Y-%m-%d') for item in daily_result_data]
    result_counts = [item['count'] for item in daily_result_data]

    return render(request, 'dashboard.html', {
        'status_labels': status_labels,
        'status_counts': status_counts,
        'date_labels': date_labels,
        'result_counts': result_counts,
    })
(2)创建可视化模板

spider_manager目录下创建templates文件夹,再创建dashboard.html

html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>爬虫平台可视化仪表盘</title>
    <!-- 引入ECharts CDN -->
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        .chart-container {
            width: 80%;
            height: 400px;
            margin: 20px auto;
            border: 1px solid #eee;
            padding: 10px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">爬虫平台可视化仪表盘</h1>

    <!-- 1. 任务状态饼图 -->
    <div class="chart-container" id="taskStatusChart"></div>

    <!-- 2. 每日爬取量柱状图 -->
    <div class="chart-container" id="dailyResultChart"></div>

    <script>
        // 初始化饼图(任务状态)
        var statusChart = echarts.init(document.getElementById('taskStatusChart'));
        statusChart.setOption({
            title: { text: '爬取任务状态分布', left: 'center' },
            tooltip: { trigger: 'item' },
            legend: { orient: 'vertical', left: 'left' },
            series: [
                {
                    name: '任务数量',
                    type: 'pie',
                    radius: ['40%', '70%'],
                    avoidLabelOverlap: false,
                    itemStyle: {
                        borderRadius: 10,
                        borderColor: '#fff',
                        borderWidth: 2
                    },
                    label: { show: false, position: 'center' },
                    emphasis: {
                        label: { show: true, fontSize: 20, fontWeight: 'bold' }
                    },
                    labelLine: { show: false },
                    data: [
                        {% for i in status_labels %}
                            { value: {{ status_counts.forloop.counter0 }}, name: '{{ i }}' },
                        {% endfor %}
                    ]
                }
            ]
        });

        // 初始化柱状图(每日爬取量)
        var dailyChart = echarts.init(document.getElementById('dailyResultChart'));
        dailyChart.setOption({
            title: { text: '每日爬取量统计', left: 'center' },
            tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
            grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
            xAxis: { type: 'category', data: {{ date_labels|safe }} },
            yAxis: { type: 'value', name: '爬取数量' },
            series: [
                {
                    name: '爬取结果数',
                    type: 'bar',
                    data: {{ result_counts|safe }},
                    itemStyle: { color: '#5470c6' }
                }
            ]
        });

        // 窗口大小变化时,自动调整图表尺寸
        window.addEventListener('resize', function() {
            statusChart.resize();
            dailyChart.resize();
        });
    </script>
</body>
</html>
(3)配置 URL 路由

打开scrapy_platform/urls.py,添加可视化仪表盘的路由:

python

from django.contrib import admin
from django.urls import path
from spider_manager.views import dashboard

urlpatterns = [
    path('admin/', admin.site.urls),
    path('dashboard/', dashboard, name='dashboard'),  # 可视化仪表盘路由
]

此时,访问http://127.0.0.1:8000/dashboard/,就能看到实时的任务状态和爬取量图表。

三、部署与扩展:从本地到生产

1. 本地测试与验证

在启动平台前,需确保各组件正常运行:

  1. 启动 Redis(作为 Celery 消息队列):redis-server(Windows 需先安装 Redis);
  2. 启动 Celery Worker(处理异步任务):在 Django 项目根目录执行celery -A scrapy_platform worker --loglevel=info
  3. 启动 Django 服务器:python manage.py runserver
  4. 流程验证:Admin 添加爬虫配置 → 触发任务 → 查看任务状态 → 查看可视化图表。

2. 生产环境部署

本地测试通过后,可按以下步骤部署到生产环境:

  1. Web 服务器:用 Gunicorn 替代 Django 内置服务器(gunicorn scrapy_platform.wsgi:application --bind 0.0.0.0:8000),并配置 Nginx 作为反向代理(处理静态文件、负载均衡);
  2. Celery 部署:用supervisord管理 Celery Worker 进程(确保进程后台运行、崩溃自动重启);
  3. 数据库:使用 MySQL(而非 SQLite),并配置定期备份;
  4. 静态文件:用 Django 的collectstatic命令收集静态文件,由 Nginx 提供访问。

3. 功能扩展方向

该平台具备良好的可扩展性,可根据需求添加以下功能:

  • 权限管理:用 Django 的django-guardian实现细粒度权限(如不同用户只能管理自己的爬虫);
  • 定时任务:用celery-beat添加定时爬取(如每天凌晨 3 点爬取数据);
  • 邮件通知:任务失败或完成时,通过 Django 的django.core.mail发送邮件通知;
  • 数据导出:在 Admin 中添加 “导出 Excel/CSV” 功能(用django-import-export库);
  • 爬虫监控:添加实时日志流(用django-channels实现 WebSocket 实时刷新日志)。

四、总结

当 Scrapy 的 “爬虫能力” 遇上 Django 的 “Web 能力”,我们不再需要在命令行中反复输入scrapy crawl,也不再需要手动分析爬取结果 —— 通过一个统一的平台,就能实现 “配置 - 触发 - 监控 - 分析” 的全流程管理。

这种 “爬虫框架 + Web 框架” 的组合,不仅提升了开发效率,更降低了爬虫的使用门槛(非技术人员也能通过 Admin 操作)。无论是企业内部的数据采集平台,还是个人的爬虫管理工具,这一方案都具备极高的实用价值。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐