简介:这份资源面向计算机相关专业学生与Python初学者,提供一套可直接用于毕业设计的豆瓣电影数据分析与可视化完整方案,覆盖从环境搭建、网络爬虫、数据清洗到图表展示的全流程,帮助解决选题难、代码零散、论文无支撑的问题。压缩包共1054个文件,约30.07MB,以688个py源码文件为核心,辅以pyc缓存、html与js前端页面、jpg与png图表素材、csv数据样本及sql建库脚本,另含论文docx文档,结构完整便于按模块查阅。已有257人学习下载,可作为同类课题的参考范本。项目基于Flask搭建后端、pandas处理数据、ECharts渲染多角度动态视图,读者可据此理解爬虫抓取豆瓣电影数据、MySQL存储与可视化呈现的衔接逻辑,并借鉴论文写作框架与目录组织方式,快速完成自己的设计与答辩准备。
1. 从一份"数据说不清"的毕设需求说起
每年做毕设选题,总有一类题目看着简单、做起来翻车:电影数据分析。表面上是"爬点数据、画几张图",真动手才发现三个坑——爬虫被封、数据脏得没法算、图表画出来导师一句"这说明了什么"就哑口无言。这套基于 Python 的豆瓣电影数据分析与可视化系统,正是冲着这三个坑去的:用爬虫把电影数据落库,用 pandas 做清洗和指标计算,用 Flask 把分析结果做成可交互的 Web 界面,前端交给 ECharts 渲染。它适合两类人:一是要交一份完整毕设、需要源码加论文全套材料的学生;二是想拿一个"数据采集—清洗—分析—展示"闭环项目练手的从业者。下面按需求、设计、实现、避坑、验收的顺序,把这份方案拆开讲透。
2. 需求与技术选型:为什么是 Flask + pandas + ECharts 这套组合
2.1 先把需求边界划清楚
做数据分析类毕设,最常见的失败不是技术不够,而是需求没边界。我一般会先把功能拆成"必须有"和"锦上添花"两档,避免做到一半发现方向跑偏。这个项目的核心需求可以归成四块:
| 模块 | 必须实现的功能 | 交付物 |
|---|---|---|
| 数据采集 | 抓取电影列表、评分、评价人数、类型、年份、导演演员等字段 | 爬虫脚本 + 原始数据 |
| 数据存储 | 结构化落库,支持按条件查询 | SQL 建表脚本 + 数据文件 |
| 数据分析 | 评分分布、类型统计、年份趋势、高分电影排行、演员/导演关联 | pandas 分析脚本 |
| 可视化展示 | Web 页面呈现图表,支持筛选与联动 | Flask 路由 + ECharts 页面 |
把这张表定下来,后面所有代码都是围着它转。很多同学一上来就写爬虫,爬到一半发现字段设计不合理,回头重来,血泪经验就是这么来的。字段设计要在写第一行爬虫代码之前定死,这是整个项目的地基。
2.2 技术栈为什么这么选
Flask 而不是 Django,理由很实在:这个项目是"分析为主、Web 为辅",不需要 Django 那套 ORM、Admin、中间件的全家桶,Flask 轻量、路由清晰,几十行就能把接口跑起来,答辩时也讲得清楚。pandas 负责数据清洗和聚合,是数据分析的事实标准,groupby、value_counts、describe几行就能出统计结果。ECharts 负责前端渲染,中文文档全、图表类型多、配置项灵活,柱状图、饼图、折线图、词云都能覆盖。爬虫部分用 requests 配合解析库即可,规模不大不需要上分布式框架。
提示:技术选型的原则是"够用且讲得清"。答辩时导师问"为什么不用 XX",你能说出取舍理由,比堆一堆用不上的框架加分得多。
2.3 数据字段与表结构设计
数据表设计直接决定后面分析能不能做。核心表我一般设计成一张电影主表加若干辅助字段,避免过度范式化导致查询复杂。建表脚本大致如下:
-- 电影主表:一行一部电影 CREATE TABLE movie ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(255) NOT NULL COMMENT '电影名称', release_year INT COMMENT '上映年份', rating DECIMAL(3,1) COMMENT '评分 0-10', rating_count INT COMMENT '评价人数', genres VARCHAR(255) COMMENT '类型,逗号分隔', country VARCHAR(100) COMMENT '制片国家', director VARCHAR(255) COMMENT '导演', actors VARCHAR(500) COMMENT '主演,逗号分隔', duration INT COMMENT '片长(分钟)', crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间' );字段说明几个关键点:rating用DECIMAL(3,1)而不是浮点,避免精度问题;genres和actors用逗号分隔存字符串,虽然不符合范式,但分析时用 pandas 的str.split展开很方便,属于"为分析场景做的合理妥协";crawl_time留着做数据版本追溯,爬虫重跑时能区分批次。这套表结构不复杂,但每个字段都能在后面的分析里派上用场,没有冗余。
3. 总体架构与模块划分:四层结构怎么落地
3.1 四层架构的职责边界
整个系统按数据流向分成四层,每层职责单一,方便单独调试:
采集层 → 存储层 → 分析层 → 展示层 爬虫脚本 MySQL pandas Flask + ECharts采集层只负责"把网页变成结构化记录",不做任何清洗;存储层只负责"落库和查询",不掺业务逻辑;分析层从库里读数据、算指标、输出 JSON;展示层调接口、渲染图表。这样分层的好处是:爬虫挂了不影响分析(用已有数据照样跑),分析逻辑改了不用动前端。我见过不少同学把所有逻辑塞进一个 Flask 路由里,改一个指标要动三处代码,后期维护就是灾难。
3.2 目录结构约定
工程目录按层组织,一眼能看出每个文件干什么:
movie_analysis/ ├── spider/ # 采集层 │ └── douban_spider.py ├── db/ # 存储层 │ ├── schema.sql # 建表脚本 │ └── db_helper.py # 数据库连接封装 ├── analysis/ # 分析层 │ └── analyzer.py # 各类统计函数 ├── web/ # 展示层 │ ├── app.py # Flask 入口 │ ├── templates/ # 页面模板 │ └── static/ # ECharts 与样式 └── requirements.txt这个结构不是摆设。答辩演示时,导师让你"讲讲数据怎么从网页到图表的",你顺着目录讲一遍,逻辑链就清楚了。requirements.txt 把依赖版本钉死,换台机器pip install -r就能复现,避免"在我电脑上能跑"的尴尬。
3.3 模块间的数据契约
层与层之间靠数据契约解耦。分析层输出的 JSON 结构要提前定好,前端照着这个结构写渲染逻辑。比如评分分布接口约定返回:
{ "bins": ["0-2", "2-4", "4-6", "6-8", "8-10"], "counts": [12, 45, 230, 890, 320] }前端拿到bins和counts直接喂给 ECharts 的xAxis.data和series.data,不需要再做转换。契约定死之后,前后端可以并行开发,分析层还没写完时,前端用假数据先把图表调通,这是工程化的基本操作。
4. 核心模块实现:爬虫、清洗、分析与接口
4.1 爬虫模块:请求、解析与限速
爬虫的核心是"稳"而不是"快"。下面是一个带重试和限速的采集骨架:
import requests import time import random from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36" } def fetch_page(url, retry=3): """带重试的页面抓取,失败返回 None""" for i in range(retry): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text except requests.RequestException: pass time.sleep(2 * (i + 1)) # 退避等待,逐次拉长 return None def parse_movie_list(html): """解析列表页,返回电影基础信息列表""" soup = BeautifulSoup(html, "html.parser") movies = [] for item in soup.select(".item"): title = item.select_one(".title").get_text(strip=True) rating = item.select_one(".rating_num") movies.append({ "title": title, "rating": float(rating.get_text()) if rating else None, }) return movies def crawl(pages=10): """按页采集,每页之间随机停顿,降低请求频率""" all_movies = [] for p in range(pages): url = f"https://example.com/movies?start={p * 25}" html = fetch_page(url) if html: all_movies.extend(parse_movie_list(html)) time.sleep(random.uniform(1.5, 3.0)) # 随机间隔,模拟正常访问 return all_movies这段代码有三个设计点值得说。第一,fetch_page的重试采用指数退避,第 i 次失败等2*(i+1)秒,避免连续请求把对方打挂。第二,time.sleep(random.uniform(...))用随机间隔而不是固定间隔,固定间隔的请求节奏太规律,容易被识别。第三,解析函数只提取列表页能拿到的字段,详情页字段(导演、演员、片长)另开函数按需抓取,避免一次请求拿太多字段导致解析逻辑臃肿。参数上,pages控制采集规模,毕设演示 10 到 20 页足够,timeout=10防止单页卡死拖垮整个任务。
4.2 数据清洗:pandas 把脏数据变成可算数据
爬下来的数据几乎不可能直接算。缺失值、重复记录、类型不一致、评分是字符串,这些问题要在分析前统一处理:
import pandas as pd def clean_movies(df): """清洗电影数据,返回可分析的 DataFrame""" # 1. 去重:同名同年的视为重复 df = df.drop_duplicates(subset=["title", "release_year"], keep="first") # 2. 评分转数值,无法转换的置空 df["rating"] = pd.to_numeric(df["rating"], errors="coerce") # 3. 丢弃评分缺失的记录(评分是核心指标,缺失无分析价值) df = df.dropna(subset=["rating"]) # 4. 年份异常值处理:超出合理范围的置空 df.loc[(df["release_year"] < 1900) | (df["release_year"] > 2030), "release_year"] = None # 5. 类型字段拆分,便于后续按类型统计 df["genres"] = df["genres"].fillna("").str.split(",") return df.reset_index(drop=True)清洗顺序有讲究:先去重再处理缺失,否则重复记录会干扰缺失统计;评分缺失直接丢而不是填均值,因为评分是这个项目的分析主体,填出来的假数据会污染分布图。errors="coerce"是关键参数,把无法解析的值变成 NaN 而不是抛异常,保证整批数据能跑完。类型字段拆成列表后,后面用explode就能按类型展开统计,这是 pandas 处理"一对多"字段的标准手法。
4.3 分析模块:五个核心指标的计算
分析层把清洗后的数据算成前端能直接用的结构。核心指标我一般做五个:评分分布、类型占比、年份趋势、高分排行、导演作品数。以评分分布和类型统计为例:
def rating_distribution(df): """评分分布:按 2 分一档分箱统计""" bins = [0, 2, 4, 6, 8, 10] labels = ["0-2", "2-4", "4-6", "6-8", "8-10"] df["rating_bin"] = pd.cut(df["rating"], bins=bins, labels=labels, include_lowest=True) counts = df["rating_bin"].value_counts().reindex(labels, fill_value=0) return {"bins": labels, "counts": counts.tolist()} def genre_stats(df, top_n=10): """类型统计:展开类型字段,取出现次数最多的前 N 类""" exploded = df.explode("genres") exploded = exploded[exploded["genres"] != ""] counts = exploded["genres"].value_counts().head(top_n) return {"genres": counts.index.tolist(), "counts": counts.tolist()}pd.cut做等宽分箱,include_lowest=True保证 0 分也被算进第一档,这个参数漏了会出现边界值丢失。reindex(labels, fill_value=0)保证即使某个区间没有数据,返回的 counts 长度也和 bins 对齐,前端不会因为数组长度不一致画歪。explode把列表字段展开成多行,一行电影有多个类型就变成多行,再value_counts就是标准的"一对多统计"套路。top_n参数控制展示数量,饼图放太多类会糊成一团,10 个以内比较合适。
4.4 Flask 接口与 ECharts 渲染
后端把分析函数包成接口,前端用 fetch 拿数据渲染:
from flask import Flask, jsonify, render_template from analysis.analyzer import rating_distribution, genre_stats from db.db_helper import load_movies app = Flask(__name__) @app.route("/") def index(): return render_template("index.html") @app.route("/api/rating") def api_rating(): df = load_movies() # 从数据库读全量数据 return jsonify(rating_distribution(df)) @app.route("/api/genre") def api_genre(): df = load_movies() return jsonify(genre_stats(df))前端页面里,ECharts 初始化后调接口填数据:
// 评分分布柱状图 fetch('/api/rating') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('ratingChart')); chart.setOption({ xAxis: { type: 'category', data: data.bins }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: data.counts }] }); });接口设计上,每个图表一个独立接口,而不是一个大接口返回所有数据。好处是页面可以按需加载,某个图表出错不影响其他图表;坏处是请求数变多,但毕设规模下这点开销可以忽略。load_movies()每次请求都读全量数据,数据量在万级以内没问题,如果数据量大要加缓存,这是可以写进论文"优化方向"的点。
5. 避坑与常见问题:五个最容易翻车的地方
5.1 爬虫请求被拒或返回空页面
现象:脚本跑着跑着开始返回 403 或空 HTML,解析出来零条记录。原因通常是请求头不完整、请求频率过高、或者目标页面结构变了。解决分三步:先检查 HEADERS 是否带了完整的 User-Agent 和 Referer;再把请求间隔拉长到 2 秒以上并加随机;最后打印resp.status_code和resp.text[:500]看返回内容,确认是反爬还是页面改版。我一般会在爬虫里加一个"连续 N 页零结果就停"的保护,避免空跑。
5.2 数据落库时中文乱码
现象:数据库里电影名变成问号或乱码。原因多半是连接字符集没设对。解决:建库时指定CHARACTER SET utf8mb4,连接串里加charset='utf8mb4',pandas 的to_sql也要传charset参数。utf8mb4 而不是 utf8,是因为部分电影名含特殊字符,utf8 存不下。这个坑踩一次记一辈子。
5.3 图表数据为空但接口有返回
现象:接口用浏览器打开能看到 JSON,页面上图表却是空白。原因通常是前端字段名对不上,或者 ECharts 容器没有高度。解决:先看浏览器控制台有没有报错,再检查data.bins和接口返回的 key 是否一致(大小写、下划线都算),最后确认图表容器的 CSS 有明确高度,ECharts 在高度为 0 的容器里渲染不出来,这是玄学问题里最不玄学的一个。
5.4 分析结果和直觉不符
现象:评分分布图看起来"高分特别多",和印象里不一样。原因可能是采集样本有偏,比如只爬了排行榜页面,天然都是高分电影。解决:采集时混合列表页和分类页,保证样本覆盖不同评分区间;分析前先跑一遍df["rating"].describe()看均值和中位数,和公开认知偏差太大就说明样本有问题。数据有偏不是代码 bug,但答辩时被问到答不上来就是硬伤。
5.5 换机器跑不起来
现象:在自己电脑上一切正常,换台机器python app.py就报模块找不到或版本不兼容。原因是依赖没锁定、路径写死。解决:所有依赖写进 requirements.txt 并标注版本,路径用os.path拼接而不是硬编码绝对路径,数据库连接信息抽到配置文件里。交付前在一台干净环境里完整跑一遍,这是最省事的后悔药。
6. 验收与答辩:怎么证明这套系统真的能跑
验收这件事,很多同学的理解是"能演示就行",但导师看的往往不是演示本身,而是你对系统的掌控程度。我一般建议按"三步验收法"准备:第一步,功能验收,对照第 2 章那张需求表逐项打勾,每个模块至少演示一个正常场景和一个边界场景,比如爬虫演示正常采集,再演示目标页改版后的降级处理;第二步,数据验收,现场跑一遍describe()和几个统计函数,证明数据清洗前后记录数、评分均值的变化是合理的,能解释"为什么清洗掉了这些记录";第三步,代码验收,准备好回答"这个函数为什么这么写""这个参数改大会怎样",能当场改一个参数看效果,比背稿子有说服力。
答辩陈述的时间通常只有几分钟,结构比内容更重要。我的习惯是按"问题—方案—证据"三段讲:先讲电影数据分析的三个痛点(采集难、数据脏、展示弱),再讲这套系统怎么用四层结构对应解决,最后用两三个图表和一组清洗前后的数据对比作为证据。图表不要全放,挑评分分布和类型占比两张最有话可说的,讲清楚"这张图说明了什么",而不是"这张图用了什么技术"。导师追问时,往实现细节上引,比如被问"数据量大了怎么办",就答"当前万级数据全量读库够用,再大可以加 Redis 缓存或预计算指标表",展示你知道边界在哪。
下面这张表可以贴在答辩 PPT 最后一页,作为交付清单:
| 验收项 | 验收方式 | 合格标准 |
|---|---|---|
| 数据采集 | 现场跑爬虫脚本 | 能稳定采集并落库,有重试与限速 |
| 数据清洗 | 展示清洗前后对比 | 缺失、重复、异常值有处理记录 |
| 分析指标 | 逐个调接口 | 五个核心指标返回结构正确 |
| 可视化 | 打开页面操作 | 图表正常渲染,筛选联动可用 |
| 工程完整 | 干净环境部署 | requirements 可复现,无硬编码路径 |
从那以后我每次带项目,验收前都强制自己在一台没装过任何依赖的机器上从零跑一遍,跑不通就不算完成。这个习惯帮我提前发现过无数次"在我电脑上能跑"的假象。希望这套拆解能帮到你,把毕设从"能交"做到"能讲、能改、能复现"。
本文还有配套的精品资源,点击获取