
简介这是一份面向高校毕业设计的大数据综合实践项目覆盖网络爬虫、数据处理与可视化全流程。项目以豆瓣电影为数据源抓取影片名称、评分、导演、演员、上映日期、类型等字段借助Spark大数据计算框架完成数据清洗、转换与聚合并结合机器学习库进行评分预测或推荐建模最终以可视化图表展示分析结果。适合大数据相关专业学生作为毕设参考也可供技术爱好者研究数据分析链路。压缩包共241个文件总大小5.6兆内含后端源码、查询脚本、爬虫脚本、页面、样式与前端脚本及数据文件等同时保留编译后的类文件与校验文件目录结构清晰可直接导入工程运行。已有202人学习覆盖采集、分析、展示三大环节并附带项目配置与输出样例能有效节省环境搭建和排错时间适合快速复现与扩展。1. 从豆瓣电影到毕业设计这条数据流水线为什么值得完整跑通豆瓣电影的数据天然适合做毕业设计数据量级不大不小字段够杂、够真实既有评分、票房这类数值型数据也有短评、导演、类型这些适合做文本分析和多维交叉的文本型数据。把一个豆瓣爬虫写成 Spark 数据分析项目再落成可视化页面等于覆盖了数据采集、清洗、分布式计算、结果展示的完整链路而这恰恰是很多技术岗位面试时最看重的能力块。这套方案的常见做法是用 Python 的 requests 配合解析库爬取豆瓣电影页面或公开接口把数据落成 JSON 或 CSV再交给 Spark 做清洗、聚合和分析最后用 Flask 或 FastAPI 拉起一个 Web 服务前端用 ECharts 呈现分析结果。最省心的落地顺序是先单机爬通、再上 Spark、最后做可视化。而不是先搭集群再写爬虫因为任务的核心难点永远是数据质量而不是集群规模。全文我会按这个顺序展开第二章解决爬虫的抓取策略与免封禁设计第三章讲 Spark 的安装选型和数据清洗、分析的写法第四章给出一套完整可跑的本地分析代码第五章把结果做成可视化页面最后一章落到毕业设计文档和答辩的常见高频问题。全程不会用任何虚构的包和版本号只用当前主流稳定生态。2. 豆瓣电影爬虫从单页请求到并发与全量抓取2.1 先明确目标数据与反爬边界做爬虫的第一步不是写代码而是定字段。毕业设计里的豆瓣爬虫一般只需要抓三类数据电影基本信息片名、导演、主演、类型、年份、地区、片长、评分、评价人数、短评数据评论内容、评分、点赞数、评论时间、Top250 或某一类型下的榜单列表。豆瓣的公开页面反爬强度中等主要的限制手段集中在三点请求频率、User-Agent 检测、以及封禁逻辑。常见做法是控制请求间隔在 1 到 3 秒之间随机轮换 User-Agent并在代码里做好异常重试。不使用高并发代理池不需要多账号体系也能稳定拿下一两千部电影的数据——这对毕业设计来说已经够用了。下面是爬取豆瓣电影 Top250 的一个最小可运行版本用 requests BeautifulSoup 实现。注意这里只请求列表页不会碰任何需要登录才能访问的资源import requests import time import random from bs4 import BeautifulSoup import pandas as pd HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } def fetch_top250(): movies [] for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start} resp requests.get(url, headersHEADERS, timeout10) if resp.status_code ! 200: print(f请求失败: {resp.status_code}) time.sleep(5) continue soup BeautifulSoup(resp.text, html.parser) for item in soup.select(.item): title item.select_one(.title).text rating item.select_one(.rating_num).text quote item.select_one(.inq) quote_text quote.text if quote else movies.append({ title: title, rating: float(rating), quote: quote_text, page: start // 25 1 }) print(f完成 {start 25} 条) time.sleep(random.uniform(1, 3)) return movies df pd.DataFrame(fetch_top250()) df.to_csv(douban_top250.csv, indexFalse, encodingutf-8-sig)这段代码把翻页参数 start 从 0 循环到 225以 25 条为步长。每次请求后停顿随机时间是控制请求频次的主要手段。用 utf-8-sig 编码写 CSV是保证 Excel 打开不出现中文乱码的关键参数。2.2 分布式爬虫与并发设计到底该不该上 Scrapy有调研经验的人会注意到热词里有「分布式爬虫」和「并发设计 到底哪个好」这两类高频讨论。我的建议是如果任务在 5000 条数据以内requests 多线程就够用超过 5 万条再考虑 Scrapy 去重中间件。分布式爬虫的核心价值是解决两个问题请求队列的分发和 URL 去重的集中管理。但豆瓣的接口限制决定了单 IP 的请求瓶颈很低分布式爬虫在这个场景里反而会放大封禁风险。如果确实要在项目里体现并发能力可以用 concurrent.futures 线程池控制并发度。注意线程数建议设置在 3 到 5 之间而不是越大越好from concurrent.futures import ThreadPoolExecutor, as_completed def fetch_one(start): return fetch_page(start) with ThreadPoolExecutor(max_workers4) as executor: futures [executor.submit(fetch_one, start) for start in range(0, 250, 25)] for future in as_completed(futures): page_data future.result() # 统一汇总用 max_workers4 而不是更大是为了让每个线程的请求间隔在串行基础上再叠加随机延迟。并发度越高被识别为爬虫的概率越大。这也是并发设计的核心取舍吞吐量和反爬安全之间的平衡点比线程数的理论最大值更重要。2.3 反爬参数说明与失败恢复策略豆瓣页面在 2020 年后对未登录请求的响应体做了压缩所以爬虫代码里最好设置 Accept-Encoding 请求头。此外有两类失败场景必须有预案403 响应说明 IP 被临时限制需要退避等待 30 秒以上再重试页面结构变化选择器失效时不要硬解析先把 HTML 落盘再排查一个实用的兜底策略是把每次失败的 URL 记录到一个文本文件里全部跑完后统一重试。这样即使中途崩了也不需要从头开始跑failed_urls [] # 请求失败时记录 failed_urls.append(url) # 结束后统一重试 for url in failed_urls: resp requests.get(url, headersHEADERS, timeout10) # 再次处理提示这里只依赖 HTML 页面解析未使用任何需要登录态或验证码绕过的方案符合公开网页爬取的基本边界。3. Spark 数据分析的安装选型本地伪分布式与集群的取舍3.1 Spark 安装的核心版本匹配JDK、Scala、Hadoop 三者联动标题里写的是 Spark 数据分析但实践经验表明绝大多数毕业论文项目在本地伪分布式模式下就能完成全部开发最后把同一套代码丢到集群上跑一遍做数据演示。这样既避免了买服务器的高成本也能在论文里得到清晰的迁移路径。安装 Spark 之前要确认三个组件的版本兼容关系。常见稳定搭配是组件建议版本选择说明JDK1.8 或 11Spark 3.x 依赖 JDK 8/11太新的 JDK 有反射权限问题Hadoop3.2 或 3.3Spark 本身不带 HDFS伪分布式可以只装 Hadoop ClientSpark3.3 或 3.4当前主流的稳定发布线Scala 2.12 版本兼容性最好安装命令以 Linux 或 macOS 为基准Windows 用户建议直接用 WSL2 避免踩路径坑# 下载 Spark 后解压并配置环境变量 wget https://archive.apache.org/dist/spark/spark-3.3.4/spark-3.3.4-bin-hadoop3.tgz sudo tar -zxvf spark-3.3.4-bin-hadoop3.tgz -C /opt sudo ln -s /opt/spark-3.3.4-bin-hadoop3 /opt/spark echo export SPARK_HOME/opt/spark ~/.bashrc echo export PATH$PATH:$SPARK_HOME/bin ~/.bashrc echo export PYSPARK_PYTHONpython3 ~/.bashrc source ~/.bashrc上面的命令里PYSPARK_PYTHON是指定 Spark 运行 Python 代码时使用的解释器。不设置的话Spark 默认会去找python命令如果系统里只装了python3就会报错无法启动。3.2 Spark 内存参数配置初始内存与执行内存的边界Spark 的常见提问里有一类是「Spark 内存」相关这属于配置层面的高频点。运行分析任务前需要先给自己的机器定一个合理的分配方案。伪分布式的核心参数在$SPARK_HOME/conf/spark-defaults.conf里配置以下是我在 8GB 内存笔记本上的常用配置spark.master spark://localhost:7077 spark.driver.memory 2g spark.executor.memory 2g spark.executor.cores 2 spark.sql.shuffle.partitions 4 spark.default.parallelism 4 spark.serializer org.apache.spark.serializer.KryoSerializerspark.sql.shuffle.partitions和spark.default.parallelism是影响小数据量任务性能最明显的参数。如果不手动设置Spark 默认的 shuffle 分区数是 200在单机处理几十 MB 的数据时会产生大量空任务。把分区数压到 4 或 8能显著减少任务调度开销。3.3 用 Spark SQL 完成豆瓣数据的 ETL 清洗数据从 CSV 进入 Spark 后第一步不是分析而是清洗。豆瓣数据最常见的脏值包括空评分数值、带有「人评价」后缀的字符串、年份字段里混入的杂质以及短评里的多余空白字符。下面用 Spark SQL 的 DataFrame 接口做清洗pyspark --master local[4]进入交互式环境后执行from pyspark.sql import SparkSession from pyspark.sql.functions import col, regexp_replace, to_int, when spark SparkSession.builder.appName(douban_etl) \ .config(spark.sql.shuffle.partitions, 4) \ .getOrCreate() df spark.read.option(header, True) \ .option(encoding, utf-8) \ .csv(/path/douban_top250.csv) # 清理评分字段去掉空值和非法字符 df_clean df.withColumn(rating, col(rating).cast(double)) \ .withColumn(quote, regexp_replace(col(quote), \s, )) \ .dropna(subset[rating]) df_clean.show(5) df_clean.printSchema()这里的关键点在于withColumn(rating, col(rating).cast(double))。CSV 读进来的所有字段都是字符串类型必须先显式转换。如果源数据里存在无法转换的值cast 会得到 null所以紧接着用dropna(subset[rating])过滤掉这些行。4. Spark 数据分析实战评分分布、类型 TopN 与年度趋势挖掘4.1 核心指标的定义与 SQL 实现分析层是整个项目的「得分点」判断一个分析有没有意义就看指标能不能回答一个业务问题。针对豆瓣电影数据我选定了三个可讲清楚的分析主题评分分布高分段的电影数量占比判断豆瓣评分是否存在严重的头部效应类型分析哪类电影更容易获得高评分用平均评分而不是总数来衡量年度趋势不同年份的电影平均评分变化观察内容质量的波动周期这三个指标分别对应直方图、分组聚合、时间序列。它们的共同特征是用少量代码就可以在 Spark 上完成同时在可视化阶段能做出好看的图表。4.2 用 DataFrame 与 Spark SQL 分别实现先看 DataFrame 写法——这是 PySpark 的首选方式类型安全且便于链式调用from pyspark.sql import functions as F # 评分分布按区间分组 rating_dist df_clean.groupBy( F.when(df_clean[rating] 9.0, 9-10) .when(df_clean[rating] 8.0, 8-9) .when(df_clean[rating] 7.0, 7-8) .otherwise(below_7) .alias(rating_range) ).count().orderBy(rating_range) rating_dist.show()这段代码用F.when连续性条件构造分段字段。也可以直接groupBy(F.floor(col(rating)).alias(rating_range))按整数分桶效果类似但细分度更低。alias(rating_range)是给新生成的列命名后续 orderBy 依赖这个名称。再看 Spark SQL 等价实现适合写入论文的附录df_clean.createOrReplaceTempView(movies) result spark.sql( SELECT rating_range, COUNT(*) AS cnt FROM ( SELECT CASE WHEN rating 9.0 THEN 9-10 WHEN rating 8.0 THEN 8-9 WHEN rating 7.0 THEN 7-8 ELSE below_7 END AS rating_range FROM movies ) t GROUP BY rating_range ORDER BY rating_range )两类写法在 Spark 引擎内部会走同一个 Catalyst 优化流程性能差异可以忽略。选择哪种取决于团队习惯偏工程用 DataFrame偏传统数据分析用 SQL。论文里建议两种都呈现能体现对不同接口的掌握程度。4.3 多维度交叉分析评分与评论数的关系验证除基础分组外还需要一个「带解释力」的分析模型。这里引入一个新的维度——豆瓣短评反馈用评论数是否破万作为热度代理指标。具体分析目标是验证一个假设高热度电影是否同时保持高评分还是存在口碑与热度背离的现象。下面是完整分析代码直接读取上一阶段清洗后的数据文件from pyspark.sql.functions import avg, count, round as rnd analysis_df df_clean.groupBy(title) \ .agg( rnd(avg(rating), 2).alias(avg_rating), count(*).alias(comment_cnt) ) \ .withColumn(hot_flag, F.when(F.col(comment_cnt) 50, hot) .otherwise(normal) ) \ .groupBy(hot_flag) \ .agg( rnd(avg(avg_rating), 2).alias(hot_avg_rating), count(*).alias(movie_cnt) ) analysis_df.show()这里的withColumn(hot_flag, ...)是在聚合之后新增一列把样本分为热度和普通两组。再次聚合后得到两组各自的平均评分与样本数量。注意两次聚合的逻辑不同第一次是电影维度的均值第二次是组间均值。4.4 让分析结果落盘CSV 还是 Parquet为了把分析结果输出给可视化阶段使用最后一步是写文件。不要用 CSV 存中间结果要直接用 Parquet。Parquet 按列存储配合 Spark 读取时有天然的谓词下推优势而且保留数据类型信息不需要重复转换output_path /data/douban_analysis_result analysis_df.write.mode(overwrite).parquet(output_path) # 可视化阶段直接读取 result_df spark.read.parquet(output_path)write.mode(overwrite)适用于重跑场景不会因为目标目录已存在而自动报错。用 Parquet 后文件里会多出现_SUCCESS和.parquet后缀文件这是正常的目录结构不要手动删除内部文件。5. 可视化设计从 ECharts 图表到交互式大屏的前端实现5.1 可视化的技术选型与整体架构数据分析项目需要用图表展示最省力的方式是 Flask 提供 JSON 接口前端页面用 ECharts 直接调用——这套方案比 Jupyter Notebook 更适合毕业设计演示因为成果更容易被答辩老师直观感知。整体架构只有两层后端从 Parquet 或 CSV 读取分析结果通过 API 返回 JSON前端用原生 HTML JavaScript 调用 ECharts 渲染图表。先看 Flask 后端的最小实现from flask import Flask, jsonify import pandas as pd app Flask(__name__) app.route(/api/rating_dist) def rating_dist(): data pd.read_parquet(/data/douban_analysis_result) result data.to_dict(orientrecords) return jsonify(result) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)host0.0.0.0表示允许局域网访问方便用手机或另一台电脑演示。to_dict(orientrecords)把 DataFrame 转成 JSON 数组结构前端可以直接用来喂给 ECharts。5.2 手写 ECharts 配置评分直方图与 Top10 条形图在前端目录下创建index.html。这里给出直方图的核心配置完整页面可以按同样的思路扩展多个图表!DOCTYPE html html langzh-CN head meta charsetUTF-8 title豆瓣电影数据分析可视化/title script srchttps://cdn.jsdelivr.net/npm/echarts5.4.3/dist/echarts.min.js/script /head body div idchart stylewidth:800px;height:400px;/div script fetch(/api/rating_dist) .then(resp resp.json()) .then(data { const categories data.map(item item.rating_range); const values data.map(item item.cnt); const chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 豆瓣电影评分分布, left: center }, tooltip: { trigger: axis }, xAxis: { type: category, data: categories }, yAxis: { type: value }, series: [{ type: bar, data: values, itemStyle: { color: #2a5caa } }] }); }); /script /body /html前端部分的核心点是fetch到 Flask 接口后用data.map把 JSON 数据拆成两个数组分别对应 x 轴类目和 y 轴数值。echarts.init的作用是绑定页面里的 div 容器。5.3 可视化大屏的设计逻辑与性能注意热词里有大量「可视化大屏」相关词条。视频展示页会做一块更复杂的布局左侧放总体统计中间是年度趋势折线图右侧放类型 Top10。大屏不是图形的堆砌而是一个递进叙事先说总量再说规模最后说分布每一块图表都服务于一个分析结论。做多图页面时有一个高频踩坑点多个图表共用同一个id的 div 会导致仅最后一个图表正常渲染。常见解法是为每个图表分配独立 id或者用 class 配合循环绑定。demo 阶段足够简洁的独立 id 方案反而最可靠div idchart1 stylewidth:400px;height:300px;/div div idchart2 stylewidth:400px;height:300px;/div两组图表各自调用一次echarts.init分别绑定chart1和chart2互不干扰。5.4 可视化存储策略Parquet 到 JSON 的快速转化如果不想让 Flask 后台每次都跑 Spark 任务就把已经落盘的 Parquet 数据转成静态 JSON 文件由 Flask 直接读文件返回。这样在做演示的时候完全不需要 Spark 在后台运行启动速度会快很多import json # 把 Parquet 结果转成静态 JSON pdf df.toPandas() records pdf.to_dict(orientrecords) with open(/data/analysis_result.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse)ensure_asciiFalse是中文 JSON 输出的关键参数。如果漏掉这个中文会变成转义后的 Unicode 编码前端虽然可以正常解析但排查问题时非常不直观。6. 性能与排错Spark 数据倾斜、分区调优与毕业设计文档的对齐策略6.1 三大高频报错与现场修复方法直接给出最常遇到的三个错误现象和修法报错现象根因修复方案OutOfMemoryError: Java heap space执行内存不足调大 spark.executor.memory减少 shuffle 分区数到 2 或 4Py4JJavaError: An error occurred while calling o47.showString数据中有不支持的特殊字符.option(multiLine, True)或先 drop 空值列AnalysisException: Path does not existParquet 路径不存在或读取模式不对确认 spark.read.parquet 的参数是目录而不是文件其中 Py4JJavaError 是 Python 调 Spark 时报错最常见的现象。重点看错误栈里Caused by段落的内容那才是真正的底层异常不要被前面那一长串 Java 调用信息干扰。6.2 数据倾斜的判定与针对性处理热词里大量出现 Spark 面试题的相关词条数据倾斜几乎必有一问。大数据量场景下按类型分组聚合时数量极度集中那一组会导致单任务处理时间远超其它任务这会让整个作业卡在某一个 stage 上。判定方法很简单看 Spark Web UI 中 Stage 的 Shuffle Read Size 是否出现单个任务明显较大。处理倾斜的一个常用手段是加盐。在分组 key 后拼接一个 0 到 N 的随机前缀先打散聚合一次再去掉前缀汇总一次。这里放出核心代码结构from pyspark.sql.functions import concat, lit, rand # 加盐给每个 key 拼接一个随机数前缀 df_salted df.withColumn(salted_key, concat(df[genre], lit(_), (rand() * 10).cast(int))) # 第一轮聚合按盐化 key partial df_salted.groupBy(salted_key).agg(F.sum(rating).alias(partial_sum)) # 第二轮聚合去掉盐化前缀再汇总 from pyspark.sql.functions import split final partial.withColumn(genre, split(salted_key, _)[0]) \ .groupBy(genre) \ .agg(F.sum(partial_sum).alias(total_sum))加盐的粒度直接影响最终结果数值要注意的是rand() * 10产生 0 到 9 之间的值乘以 10 后取整会覆盖 0 到 9 的整数。盐数量不是越多越好4 到 16 之间足够撑过大多本地任务。6.3 分析框架在毕业设计文档中的呈现不写流水账毕业设计源码案例通常要配套论文。论文里不用贴整段代码但流程图、架构图、核心代码段三件套缺一不可。我常见的高分结构是论文章节先写爬虫模块的设计目标——数据规模、字段定义、存储格式再画一张 ETL 数据流图豆瓣页面 - requests 爬虫 - CSV/JSON - Spark 清洗 - Parquet - Flask API - ECharts最后放核心代码段时只放两段不要整章贴代码一段是 Spark 清洗一段是可视化接口6.4 答辩时的三个高频问题与回应准备毕业设计的问答环节比代码能不能跑更关键。很多同学能跑通 demo 却被追问到卡壳是因为没有把设计决策的「为什么」想清楚。我建议提前准备这三个问题第一个问题是「为什么用 Spark 而不是 Pandas」。回答要点是Pandas 处理千万级数据时会占用大量内存且难以水平扩展Spark 的弹性分布式数据集可以在数据量增长时通过加节点扩展吞吐能力。但要诚实说明在更小的数据量下 Pandas 起步更快。第二个问题是「爬虫的合法性边界在哪里」。回答要客观陈述项目严格遵守网站的 robots 协议控制请求频率仅采集公开页面数据且数据只用于学习研究。不要虚构任何授权资质这是任何项目都必须守住的红线。第三个问题是「分析结果对谁有决策价值」。不要说空话套话。直接回答评分分布可以辅助用户做观影选择年度趋势反映内容质量波动类型偏好分析可为内容平台做推荐策略提供参考。给出结论的同时要迅速展示对应的可视化图表——这也是为什么可视化阶段要把图表设计得清晰且带有数据说明的原因。至此从豆瓣爬虫到 Spark 分析再到可视化呈现的完整链路每一环节的选型、参数和坑点都已覆盖。按这条路径把你的毕业设计源码案例从「能跑」提升到「经得起问」才是这套工程组合最大的价值所在。本文还有配套的精品资源点击获取