简介:这是一套面向计算机相关专业学生与项目实战学习者的共享单车数据分析完整项目源码,采用Spark作为核心计算引擎,配套前端与后端代码,可直接用于毕业设计、课程设计或期末大作业。项目经导师指导并认可,评分达98分,代码经过严格调试,确保可运行。压缩包共265个文件,约9.15MB,涵盖185个xml配置、21个vue前端组件、9个js脚本、6个java与4个scala源码、6个html页面及csv数据集等,前后端结构完整,便于按模块阅读与二次开发。目前已有384人学习下载。读者可获得一套可直接复用的毕设级方案,包括数据清洗与统计分析逻辑、前后端交互实现、可视化展示页面以及完整的工程目录组织,既能用于快速搭建答辩项目,也能作为Spark数据分析与Web开发的学习范例,帮助理解从数据接入到结果呈现的完整链路。
1. 从一份共享单车数据分析项目说起:Spark 到底在毕业设计里扮演什么角色
共享单车每天产生千万级骑行记录,字段无非是借还时间、站点编号、车辆 ID、骑行时长。数据本身不复杂,但量级一上来,单机 pandas 就开始卡。很多同学做毕业设计时第一反应是「我数据才几十万行,用得着 Spark 吗」,这个疑问很真实。答案是:如果只做一次静态统计,确实用不着;但毕业设计要展示的是一套能跑通的数据管道——从原始 CSV 进 HDFS,到 Spark 清洗聚合,再到后端把结果吐给前端可视化。这条链路才是评分点。
这个标题里的「前端后端完整代码」,本质是一个前后端分离项目实战:Spark 负责算,后端负责查,前端负责画。适合正在找 spark 数据分析案例的本科生,也适合想补一个完整项目经验的后端方向同学。下面我按自己搭过的一套方案,把选型、代码、参数和翻车点讲清楚。
2. 数据层怎么落地:从 CSV 到 Spark 可查的表
2.1 为什么选 Spark SQL 而不是 RDD
毕业设计里最常见的误区是上来就写 RDD 的 map/reduce。RDD 灵活,但代码量大、调试痛苦,而且答辩老师看不出你的业务逻辑。共享单车的分析需求无非是:按小时统计骑行量、按站点统计热度、算平均骑行时长、找高峰时段。这些全是标准 SQL 能表达的聚合。
Spark SQL 的好处是:你写的 DataFrame 操作会被 Catalyst 优化器重写,执行计划可查,而且代码短。我一般会先把原始数据注册成临时视图,后面所有指标都用 SQL 写,这样业务逻辑一眼能看懂,改需求也快。
2.2 环境搭建与依赖版本对齐
Spark 对版本很敏感,Scala 版本、JDK 版本、Hadoop 版本三者错一个就报NoSuchMethodError。我踩过的坑是 JDK 17 配 Spark 3.2,直接起不来。稳妥组合是 JDK 8 或 11,Spark 3.3 以上,Scala 2.12。
# 本地伪分布式跑通的最小配置,生产集群另说 export JAVA_HOME=/usr/lib/jvm/java-11-openjdk export SPARK_HOME=/opt/spark-3.5.0 export PATH=$SPARK_HOME/bin:$PATH # 启动 spark-shell 验证 spark-shell --master local[*] --conf spark.sql.shuffle.partitions=8spark.sql.shuffle.partitions默认是 200,本地跑小数据会生成 200 个空分区,任务调度开销比计算还大。本地开发设成 8 或 16 就够,集群上再按数据量调。这个参数是新手最容易忽略的性能开关。
2.3 读取 CSV 并做第一轮清洗
原始数据常见问题是:字段缺失、时间格式不统一、骑行时长为负。清洗要在聚合之前做完,否则脏数据会污染所有指标。
from pyspark.sql import SparkSession from pyspark.sql.functions import col, to_timestamp, when spark = SparkSession.builder \ .appName("bike_analysis") \ .config("spark.sql.shuffle.partitions", "8") \ .getOrCreate() # 读取原始 CSV,inferSchema 让 Spark 自动推断类型 raw = spark.read.csv( "hdfs:///data/bike/trip.csv", header=True, inferSchema=True ) cleaned = raw \ .withColumn("start_time", to_timestamp(col("start_time"), "yyyy-MM-dd HH:mm:ss")) \ .withColumn("end_time", to_timestamp(col("end_time"), "yyyy-MM-dd HH:mm:ss")) \ .filter(col("start_time").isNotNull()) \ .filter(col("duration") > 0) \ .filter(col("duration") < 86400) \ .dropDuplicates(["trip_id"]) cleaned.createOrReplaceTempView("trips") cleaned.cache()逻辑说明:to_timestamp显式指定格式,比让 Spark 猜更可靠;duration上限设 86400 秒是排除异常长骑(比如车没还上);dropDuplicates按行程 ID 去重。cache()是因为后面多个 SQL 都要扫这张表,缓存到内存能省重复 IO。注意inferSchema=True会多扫一遍数据,数据量大时建议手动指定 schema,省一次全表扫描。
2.4 四个核心指标的 SQL 写法
-- 指标一:按小时统计骑行量,用于前端折线图 SELECT hour(start_time) AS hour_of_day, count(*) AS trip_count FROM trips GROUP BY hour(start_time) ORDER BY hour_of_day; -- 指标二:站点热度 Top20,用于前端地图热力 SELECT start_station_id, count(*) AS depart_count FROM trips GROUP BY start_station_id ORDER BY depart_count DESC LIMIT 20; -- 指标三:平均骑行时长(分钟) SELECT round(avg(duration) / 60, 2) AS avg_minutes FROM trips; -- 指标四:工作日 vs 周末骑行对比 SELECT CASE WHEN dayofweek(start_time) IN (1, 7) THEN 'weekend' ELSE 'workday' END AS day_type, count(*) AS trip_count, round(avg(duration) / 60, 2) AS avg_minutes FROM trips GROUP BY 1;这四个查询覆盖了毕业设计里最常被问的「你分析了什么」。每个结果都可以直接落库给后端查。注意dayofweek在 Spark 里 1 是周日、7 是周六,这个和 MySQL 不一样,写错会导致工作日周末颠倒,答辩时被问就尴尬了。
2.5 结果写回存储的两种方式
算完的结果要能被后端读到。常见做法是写 MySQL,因为后端查起来方便;也可以写 Parquet 文件,后端直接读文件。我一般写 MySQL,因为前端要分页、要按条件筛,数据库更合适。
result_df.write \ .format("jdbc") \ .option("url", "jdbc:mysql://localhost:3306/bike") \ .option("dbtable", "hourly_stats") \ .option("user", "root") \ .option("password", "your_password") \ .option("batchsize", "1000") \ .mode("overwrite") \ .save()batchsize控制每次插入的行数,太小会频繁建连接,太大占内存,1000 到 5000 之间比较稳。mode("overwrite")会先删表再建,适合每天全量重算的场景;如果是增量追加,改成append。这里要提醒:JDBC 写入是单线程的,数据量大时慢得让人怀疑人生,可以考虑先repartition再写,或者干脆写 Parquet 让后端读。
3. 后端怎么把 Spark 结果变成接口
3.1 后端框架选型:为什么用 FastAPI 而不是 Spring Boot
标题里没指定后端语言,但热搜里 python fastapi 典型的后端框架出现频率很高。我的判断是:毕业设计场景下,FastAPI 比 Spring Boot 更省事。原因有三:一是 Python 和 Spark 同栈,不用维护两套环境;二是 FastAPI 自带 Swagger 文档,答辩演示接口时直接打开/docs就能点,很加分;三是代码量少,一个接口十行以内。
如果学校要求 Java 技术栈,那就用 Spring Boot,思路一样,只是把查询换成 JPA 或 MyBatis。下面以 FastAPI 为例。
3.2 数据库连接与查询封装
from fastapi import FastAPI, Query from sqlalchemy import create_engine, text from typing import List app = FastAPI(title="Bike Analysis API") engine = create_engine( "mysql+pymysql://root:your_password@localhost:3306/bike", pool_size=5, pool_recycle=3600 ) @app.get("/api/hourly") def get_hourly(): with engine.connect() as conn: rows = conn.execute(text( "SELECT hour_of_day, trip_count FROM hourly_stats ORDER BY hour_of_day" )).fetchall() return [{"hour": r[0], "count": r[1]} for r in rows]pool_size=5是连接池大小,毕业设计并发低,5 够用。pool_recycle=3600防止 MySQL 八小时断连导致的Lost connection报错,这个坑在演示前一晚特别容易炸。查询用text()包原生 SQL,比 ORM 直观,也方便你直接复制到数据库客户端调试。
3.3 跨域配置:前端调不通的第一嫌疑人
前后端分离项目实战里,跨域是最高频的翻车点。前端跑在 5173 端口,后端跑在 8000,浏览器直接拦。FastAPI 加中间件解决:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )allow_origins不要图省事写["*"]又同时开allow_credentials=True,浏览器会拒绝。开发阶段写死前端地址,上线再改成正式域名。如果部署到宝塔,Nginx 反代时也要加Access-Control-Allow-Origin头,否则本地通了线上又挂。
3.4 接口分页与参数校验
站点热度这种可能上百条的数据,前端要分页。FastAPI 的Query能做参数校验:
@app.get("/api/stations") def get_stations( page: int = Query(1, ge=1), size: int = Query(10, ge=1, le=100) ): offset = (page - 1) * size with engine.connect() as conn: rows = conn.execute(text( "SELECT start_station_id, depart_count FROM station_stats " "ORDER BY depart_count DESC LIMIT :limit OFFSET :offset" ), {"limit": size, "offset": offset}).fetchall() return {"page": page, "data": [{"station": r[0], "count": r[1]} for r in rows]}ge=1和le=100是边界校验,防止前端传size=100000把数据库拖垮。LIMIT/OFFSET在数据量大时深分页会慢,毕业设计数据量下无所谓,但如果要优化,可以改成基于游标的分页。
4. 前端怎么把数据画出来
4.1 技术栈选择:Vue3 加 ECharts 的组合
前端开发最新框架里 Vue3 是主流,配合 ECharts 画图,是毕业设计最稳的组合。React 也行,但 Vue 的模板语法对不常写前端的同学更友好。组件库用 Element Plus,表格和分页组件开箱即用。
npm create vite@latest bike-frontend -- --template vue cd bike-frontend npm install axios echarts element-plusVite 启动快,热更新秒级,改样式不用等。axios负责请求后端,echarts画图,element-plus提供 UI 组件。
4.2 封装请求与处理跨域
// src/api/request.js import axios from 'axios' const request = axios.create({ baseURL: 'http://localhost:8000/api', timeout: 5000 }) request.interceptors.response.use( res => res.data, err => { console.error('请求失败', err.message) return Promise.reject(err) } ) export default requestbaseURL指向后端地址,timeout设 5 秒,避免请求卡死页面一直转圈。拦截器统一处理错误,不用每个组件都写 try/catch。如果后端和前端部署在同一域名下,baseURL改成/api,由 Nginx 转发,就不存在跨域了。
4.3 折线图与柱状图的 ECharts 配置
import * as echarts from 'echarts' import { onMounted, ref } from 'vue' import request from '@/api/request' const chartRef = ref(null) onMounted(async () => { const data = await request.get('/hourly') const chart = echarts.init(chartRef.value) chart.setOption({ xAxis: { type: 'category', data: data.map(d => d.hour + '时') }, yAxis: { type: 'value', name: '骑行量' }, series: [{ type: 'line', data: data.map(d => d.count), smooth: true, areaStyle: { opacity: 0.3 } }], tooltip: { trigger: 'axis' } }) })echarts.init要等 DOM 挂载完再调,放在onMounted里。smooth: true让折线平滑,areaStyle加面积填充,视觉上更像「数据分析」而不是「随便画的线」。tooltip的trigger: 'axis'让鼠标悬停显示整列数据,比单点触发好用。
4.4 地图热力的降级方案
站点热度用地图最直观,但 ECharts 地图需要城市 GeoJSON 数据,找数据、注册地图对新手是个门槛。我的建议是:如果时间紧,用横向柱状图代替,效果不差,还省去地图注册的麻烦。真要做地图,去 ECharts 官方示例里找对应城市的 GeoJSON,用echarts.registerMap注册后再画散点图。
提示:地图 GeoJSON 文件较大,不要打包进前端 bundle,放 public 目录用 fetch 动态加载,否则首屏会明显变慢。
5. 避坑与排查:那些让我重跑一整天的坑
5.1 现象:Spark 任务卡在最后一个 stage 不动
原因:数据倾斜。某个站点或某个小时的记录数远超其他,导致一个 task 处理了 80% 的数据。解决:先df.groupBy("start_station_id").count().orderBy(desc("count")).show(10)确认倾斜键,然后对热点键加随机前缀打散,聚合两次。或者直接调大spark.sql.shuffle.partitions让每个分区小一点,缓解但不根治。
5.2 现象:后端接口返回中文乱码
原因:MySQL 建表时字符集用了latin1,或者连接串没指定编码。解决:建表用utf8mb4,连接串加?charset=utf8mb4。FastAPI 返回 JSON 默认就是 UTF-8,问题基本都出在数据库这一层。
5.3 现象:前端图表数据对但显示空白
原因:ECharts 容器没有高度。div默认高度是 0,图表画在 0 高度的容器里自然看不见。解决:给容器设style="height: 400px; width: 100%",或者用 CSS 类指定高度。这个坑我见过至少五个人踩。
5.4 现象:Spark 写 MySQL 报Communications link failure
原因:MySQL 连接超时或驱动版本不匹配。解决:确认mysql-connector-java版本和 MySQL 服务端匹配,连接串加autoReconnect=true,写入前先df.count()触发一次实际计算,避免在写入时才暴露上游问题。
5.5 现象:答辩演示时前端请求全部 404
原因:后端服务没启动,或者 Nginx 配置的转发路径多了或少了一层/api。解决:演示前用curl http://localhost:8000/api/hourly确认后端活着,再检查 Nginx 的location /api/是否正确proxy_pass到后端地址。养成先 curl 再开浏览器的习惯,能省很多瞎猜的时间。
6. 让这个项目从「能跑」到「能打」的几个技巧
最后一个部分讲点进阶的。毕业设计评分不只是看你能不能跑通,还看有没有工程意识。第一个技巧是给 Spark 任务加执行计划检查。在关键 SQL 前加explain(True),能看到 Catalyst 优化后的物理计划,如果出现CartesianProduct说明有笛卡尔积,必须改。这个习惯能帮你在数据量涨十倍时不至于突然崩掉。
第二个技巧是后端加一层缓存。小时统计这种数据一天才变一次,没必要每次请求都查库。用functools.lru_cache或者 Redis 缓存五分钟,接口响应能从几百毫秒降到几毫秒。演示时点页面秒开,观感完全不同。
第三个技巧是前端加 loading 状态和错误提示。请求发出到返回有延迟,用户看到白屏会以为坏了。用 Element Plus 的v-loading指令,请求失败弹ElMessage.error,这些细节在答辩时是加分项。
| 优化点 | 改动位置 | 预期收益 |
|---|---|---|
| 缓存聚合结果 | 后端接口层 | 响应时间降 80% |
| 预聚合到中间表 | Spark 任务 | 避免每次全表扫描 |
| 前端路由懒加载 | Vue Router | 首屏加载更快 |
| 接口合并 | 后端 | 减少请求数 |
第四个技巧是准备一份数据字典。答辩老师常问「你这个字段什么意思」,提前把每个表的字段、含义、来源写成一页文档,被问到直接翻,比现场编靠谱。我一般会把数据字典放在项目根目录的docs/下,和代码一起提交。
最后一个习惯:每次改完代码,先本地spark-submit跑一遍完整流程,再提交。我吃过亏,改了一个 SQL 没测就打包,结果答辩当天发现字段名拼错,当场改代码重跑,那几分钟的尴尬至今记得。宁可多跑一次,不留后悔药。
希望帮到你。
本文还有配套的精品资源,点击获取