FEATURED · 精选文章

在线教育大数据分析:Django+Spark+爬虫+Vue实战指南

发布时间 / 2026/9/11 15:23:02
来源 / 创域科博编辑部
栏目 / 资讯中心
在线教育大数据分析:Django+Spark+爬虫+Vue实战指南 简介这是一套基于Django的线上教育平台大数据分析毕设项目以Python 3.8为基础整合Spark、爬虫与Vue面向毕业设计、课程设计、大作业或工程实训场景既适合入门小白也适合需要完整工程参考的进阶学习者。压缩包共376个文件约20.24MB包含Python源码、Vue组件、SVG图标、SQL数据库脚本、安装运行脚本、项目文档等目录结构清晰便于快速部署与二次开发。目前已有704人浏览学习。项目内置管理员端支持系统首页、个人中心、调剂信息管理、访问信息管理、访问类型管理并围绕爬取数据完成阅读量统计、发布时间统计、地区访问量条形图、访问类型饼图和阅读量TOP10等可视化分析可直接导入MySQL5.7运行。附带的说明文档和SQL文件能帮助理解从爬虫采集、数据清洗到图表展示的完整流程是一个综合性较强的实战资源。1. 线上教育平台大数据分析为什么选 Django Spark Spider Vue一个典型的在线学习平台每天产生的课程点击、视频播放、章节完成、答题记录能有上百万条。直接在 Django 的 MySQL 表上做GROUP BY和COUNT数据库很快会吃不消而 Spark 适合做这类离线的全量统计爬虫Spider负责把分散在页面或第三方的数据抓下来Vue 则把统计结果变成运营能看懂的图表。这条链路最吸引人的地方是每个环节都有独立的替换空间数据量小的时候用 Pandas Django ORM 也能跑数据量涨上来之后才把 Spark 插进去不需要推翻重写。这套组合尤其适合正在做毕业设计或内部工具开发的场景单台电脑就能跑通local[*]模式不需要搭集群Django 负责权限和 APIVue 负责展示Spark 只做计算职责边界清晰。下文按“抓数 - 建模 - 分析 - 展示 - 调优”的顺序把每一步的代码和坑位都摊开讲。2. 数据采集与存储用 Spider 抓数用 Django 建模落库2.1 爬虫框架选型Scrapy 和 Playwright 的分工线上教育平台的数据通常分成两部分一部分是自有业务库里的行为日志直接导出即可另一部分是课程信息、评论、教师介绍这类半公开页面需要爬虫去采集。常见的爬虫工具有三类按适用场景区分并不复杂。工具适合场景典型问题Scrapy静态 HTML、列表页翻页、高并发抓取对动态渲染页面无能为力需要额外对接 SplashPlaywright / Selenium登录后才能看到的数据、异步加载图表、反爬严格的站点资源占用高速度慢容易被检测Requests BeautifulSoup单页解析、接口直接返回 JSON需要自己处理重试和并发我一般会先用浏览器开发者工具看数据来源如果列表数据是接口直接返回的 JSON就用requests简单搞定如果是服务端渲染的 HTML才上 Scrapy只有必须模拟点击、滚动加载的页面才用 Playwright。毕设项目里常见做法是“接口为主页面兜底”因为教育平台的课程列表和统计数字大多来自内部接口直接请求接口比解析 HTML 稳定得多。import requests def fetch_course_list(page): url https://example.edu/api/courses params {page: page, size: 50} resp requests.get(url, paramsparams, timeout10) resp.raise_for_status() return resp.json()[items]这段爬虫代码只做了三件事构造分页参数、发送 GET 请求、解析 JSON。注意这里没有处理 cookie 和 header如果一个平台要求登录态需要在requests.get里带上headers或cookies参数。raise_for_status()会在返回 4xx/5xx 时直接抛异常避免把错误页面当正常数据入库。2.2 定义 Django 数据模型把半结构化数据变成分析表爬虫拿到的是字典列表不能直接用于分析。先建两张表一张存课程维度信息一张存学习行为日志。行为日志表是后续 Spark 分析的主表字段命名尽量和业务一致避免在分析层反复转换。# dashboard/models.py from django.db import models class Course(models.Model): course_id models.CharField(max_length32, uniqueTrue) # 业务编号 title models.CharField(max_length255) category models.CharField(max_length64, blankTrue) teacher models.CharField(max_length32, blankTrue) updated_at models.DateTimeField(auto_nowTrue) class Meta: db_table edu_course class StudyLog(models.Model): user_id models.IntegerField(db_indexTrue) course_id models.CharField(max_length32, db_indexTrue) action models.CharField(max_length16) # click / play / submit duration models.IntegerField(default0) # 学习时长单位秒 created_at models.DateTimeField(db_indexTrue) class Meta: db_table study_logStudyLog的course_id设计成普通索引db_indexTrue而不是外键是为了让 Spark 直接读表时减少关联压力。action字段只存三个固定枚举值省去单独建动作字典表。duration用秒做单位统计时按需聚合即可避免在数据库里存“1小时30分”这种文本。2.3 通过 Django ORM 批量写入避免逐条 insert爬虫抓完数据后最忌讳的做法是一条一条Model.objects.create()。一个五万条的 CSV逐条插入要跑几分钟还会产生大量 SQL 日志。正确做法是先把数据构造成对象列表再用bulk_create批量写入。# management/commands/load_courses.py from django.core.management.base import BaseCommand from dashboard.models import Course class Command(BaseCommand): help 从爬虫结果文件导入课程数据 def handle(self, *args, **options): courses [] for item in read_from_spider(): # 可以换成读 CSV 或 JSON courses.append(Course( course_iditem[id], titleitem[title], categoryitem.get(category, ), )) Course.objects.bulk_create(courses, batch_size1000, ignore_conflictsTrue) self.stdout.write(f导入完成共 {len(courses)} 条)batch_size1000表示每批插入 1000 条这个值在 MySQL 和 PostgreSQL 下都比较稳妥。ignore_conflictsTrue依赖数据库的唯一键约束重复的course_id会被自动跳过适合爬虫重复执行多次的场景。如果业务需要更新已有记录可以改成先bulk_create再update或者直接用update_or_create但后者性能会差一些。3. 用 Spark 做学习行为分析从 PySpark 读 MySQL 到生成结果表3.1 本地跑通 Spark 的最小环境Spark 不是必须搭集群单机照样可以做数据分析。只需要安装 JDK 8/11、下载 Spark 发行版然后pip install pyspark。在环境变量里设置JAVA_HOME指向 JDK 目录SPARK_HOME指向 Spark 目录即可。验证安装时在pyspark交互环境里跑一句spark.range(10).sum()能返回 45 就算是通了。注意 Windows 下还需要一个winutils.exe否则local模式会报缺少 Hadoop home 的错误。3.2 用 DataFrame 统计课程热度、完课率、活跃时段PySpark 的 DataFrame API 比 RDD 更适合分析任务关键是它能直接从 MySQL 表读数据。下面的代码统计每个课程的点击量、播放量和总学习时长from pyspark.sql import SparkSession from pyspark.sql.functions import count, sum as _sum, when spark SparkSession.builder \ .appName(EduAnalysis) \ .master(local[*]) \ .config(spark.sql.shuffle.partitions, 8) \ .getOrCreate() df spark.read.format(jdbc) \ .option(url, jdbc:mysql://localhost:3306/edu?useSSLfalse) \ .option(dbtable, study_log) \ .option(user, root) \ .option(password, 123456) \ .option(driver, com.mysql.cj.jdbc.Driver) \ .load() result df.groupBy(course_id) \ .agg(count(user_id).alias(click_cnt), _sum(when(df.action play, 1).otherwise(0)).alias(play_cnt), _sum(duration).alias(total_duration))master(local[*])表示用本机所有 CPU 核心跑毕设阶段够用。spark.sql.shuffle.partitions默认是 200单机改成 8 能减少文件碎片。when(df.action play, 1).otherwise(0)是 PySpark 版的CASE WHEN专门用于对满足条件的行求和。读取 MySQL 时如果没有设置partitionColumnSpark 只用一个连接读全表数据量大时建议加这三个参数.option(partitionColumn, id) \ .option(lowerBound, 1) \ .option(upperBound, 1000000) \ .option(numPartitions, 8)这样可以按id范围把查询拆成 8 个并行任务避免单点读取拖慢整体速度。需要注意lowerBound和upperBound是参考值实际分区边界由 Spark 自动计算。3.3 把分析结果回写 MySQL并设计分区表分析结果要供 Django 查询必须写回数据库。常见做法是直接保存到一张独立的结果表result.write.format(jdbc) \ .option(url, jdbc:mysql://localhost:3306/edu?useSSLfalse) \ .option(dbtable, course_stat) \ .option(user, root) \ .option(password, 123456) \ .mode(overwrite) \ .save()mode(overwrite)会先删除原表再重建适合每次全量重算。如果只想刷新某些课程需要改成mode(append)并配合自定义主键做去重。结果表建议按course_id做分区用 MySQL 8.0 的 RANGE 分区或直接交给 Django 拆分索引。更稳妥的做法是把分析结果写成 Parquet 文件放在 HDFS 或本地再用 Django 手动导入但毕设链路里用 JDBC 直接写回是最省事的。4. Django 提供统计 APIVue 做可视化大屏4.1 DRF 读取结果表按需聚合输出 JSONSpark 算完的course_stat表已经是聚合后的数据Django 这边只需要做简单查询和序列化。用 Django REST FrameworkDRF写一个只读接口# dashboard/views.py from rest_framework.decorators import api_view from rest_framework.response import Response from .models import CourseStat api_view([GET]) def course_ranking(request): rows (CourseStat.objects .order_by(-click_cnt)[:10]) data [{ course_id: r.course_id, title: r.title, click_cnt: r.click_cnt, total_duration: r.total_duration } for r in rows] return Response(data)这里没有用 DRF 的ModelSerializer是因为查询字段少手动构造字典反而更直白。order_by(-click_cnt)是倒序排序[:10]在 Django 里会翻译成 SQL 的LIMIT 10不会把全表 load 进内存。如果查询很慢需要检查有没有在click_cnt上加索引。4.2 Vue 3 ECharts 渲染课程热度排行和学习趋势Vue 端最常见的做法是用axios请求接口再把数据交给 ECharts。下面是一个极简的柱状图组件!-- CourseRanking.vue -- script setup import { ref, onMounted } from vue import axios from axios import * as echarts from echarts const chartEl ref(null) onMounted(async () { const { data } await axios.get(/api/analyze/ranking/) const chart echarts.init(chartEl.value) chart.setOption({ xAxis: { type: category, data: data.map(item item.title) }, yAxis: { type: value }, series: [{ type: bar, data: data.map(item item.click_cnt) }] }) }) /script template div refchartEl stylewidth: 800px; height: 400px/div /templateref(chartEl)绑定真实的 DOM 节点ECharts 必须挂在已经渲染的元素上所以onMounted里执行初始化。axios.get返回的data是双重包装第一层是 axios 的响应体第二层是服务器返回的 JSON解构赋值时要写const { data } ...。如果图表数据不更新最常见原因是setOption没有配置series的name或没有清空旧数据可以在更新前调用chart.clear()。4.3 给 Vue 路由加参数从/analyze/:courseId看单一课程课程排行榜只是总览运营人员还需要点进某个课程看详细趋势。这就要用 Vue Router 的动态路由参数。路由配置// router/index.js const routes [ { path: /, component: Home }, { path: /analyze/:courseId, component: CourseDetail } ]在CourseDetail.vue里通过useRoute拿到参数script setup import { useRoute } from vue-router import { ref, watch } from vue const route useRoute() const courseId ref(route.params.courseId) watch(() route.params.courseId, (newId) { courseId.value newId // 这里重新调用接口拉详情 }) /script:courseId是路径参数访问/analyze/C001时route.params.courseId就是C001。注意useRoute只在 Vue 3 组合式 API 中可用如果项目还在用 Options API需要用this.$route. 监听路由变化是因为同一个组件会被复用不监听就只能在首次进入时拿到参数。5. 排错与调优Spark 内存、Django 查询效率、爬虫被封5.1 Spark 内存溢出和分区数调整Spark 分析大表时最常见的报错是java.lang.OutOfMemoryError。先看驱动端还是执行端local模式下二者都归driver管直接调大 Spark 配置即可。表格里这几个参数务必优先确认参数默认值调优建议spark.driver.memory1g本地分析建议至少 4gspark-submit用--driver-memory指定spark.executor.memory1g集群模式每个 executor 至少要能容纳一次 shuffle 的数据spark.sql.shuffle.partitions200单机可以调到 CPU 核数 x 2否则会生成大量小文件spark.sql.autoBroadcastJoinThreshold10m小表 Join 大表时低于该阈值会自动广播省一次 shuffle修改方式有两种在代码里写config(spark.executor.memory, 4g)或者在提交时加--conf spark.executor.memory4g。注意动态分配如果开启executor.memory会被spark.dynamicAllocation.maxExecutors限制排查时要同时看这两个参数。5.2 Django 查询效率与缓存Spark 写回的结果表如果被高频访问直接查 MySQL 还是有压力。我的做法是用 Redis 缓存查询结果设置 5 分钟过期配合一个简单的装饰器# dashboard/decorators.py from functools import wraps from django.core.cache import cache def cache_response(timeout300): def decorator(func): wraps(func) def wrapper(request, *args, **kwargs): key fapi:{request.path}:{request.GET.urlencode()} data cache.get(key) if data is None: data func(request, *args, **kwargs) cache.set(key, data, timeouttimeout) return data return wrapper return decorator注意cache.set的data必须是可序列化的对象DRF 的Response不能直接缓存需要先取response.data。这个装饰器粒度是“同一路径 同一查询参数”看作一个 key适合排行榜、课程趋势这类周期性变化的数据。如果统计结果每天只更新一次缓存时间可以拉到 86400。5.3 验证 Spark 分析结果和 Django 口径是否一致分析完成不能只看图表“像那么回事”必须验证两个计算路径的数值是否一致。我用一个简单脚本对比 Spark 统计和 Django ORM 统计# scripts/verify.py def test_click_consistency(course_id): spark_click spark_result.filter(col(course_id) course_id) \ .select(click_cnt).collect()[0][0] orm_click StudyLog.objects.filter(course_idcourse_id).count() assert spark_click orm_click, \ fSpark{spark_click}, ORM{orm_click} 不一致filter和select是 PySpark 的按行筛选和按列拣选collect()在结果量小的时候可以用返回的是一个 Row 列表索引[0][0]取第一行第一列。这种验证脚本建议做成 CI 的一部分每次跑完 Spark 任务自动执行能尽早发现分区读取漏数或when条件写错的问题。最后分享一个调优小技巧Spark 读 MySQL 时优先给study_log表增加一个自增主键id然后用partitionColumn按主键范围拆分任务。Django 模型里每张表默认都有id所以一般不用额外改动。这样既能让 Spark 并行读库又不影响 Django 端的写入性能。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻