简介:面向python毕业设计及数据可视化课程实践的完整项目资源,整合豆瓣电影真实数据,从爬虫采集、数据清洗、存储到可视化展示形成完整闭环,涵盖pandas分析、Flask后端与ECharts交互图表,采用MySQL存储结果。系统通过ECharts生成评分分布、类型占比、年度趋势等多角度动态视图,使分析结论一目了然。资源包共1054个文件,以688个py源码文件为核心,另含pyc编译文件、前端html/js/css、SQL脚本、完整论文docx、虚拟环境配置等,整体约30MB,结构清晰便于分模块查阅。已有256人学习下载,适合毕业设计选题、课程设计以及爬虫与可视化方向自学者,尤其适合需要快速完成毕设系统搭建的计算机相关专业学生。获取后可获得可运行完整工程、数据库脚本、论文文档与Pycharm环境,快速搭建开发环境,并支持二次开发,大幅缩短设计与编码时间,可作为论文与答辩环节的可靠支撑。
1. 从爬虫到可视化:这套豆瓣电影分析系统把毕设最难的环节全打通
每年毕业季都能看到不少人在数据分析类题目上翻车:爬虫抓回来数据不会清洗,清洗完不知道怎么存,存进去了 Flask 后端和 ECharts 图表又对不上格式。这套基于 Python 的豆瓣电影数据分析及可视化系统,把爬虫、pandas 处理、Flask 接口、ECharts 图表渲染串成一条完整流水线,源码、SQL 脚本和论文三样齐备。适合两类人:一类是选题偏数据可视化、需要快速出成果的毕业生,另一类是刚学完 Python 基础、想完整走一遍项目流程的开发者。它解决的问题很明确——让你不纠结“下一步该干什么”,直接照着现有模块改数据、换指标、调整图表,把精力花在理解和演示上。
2. 系统架构与数据流:Flask + ECharts + pandas 是怎么串起来的
2.1 分层设计与模块划分:爬虫、清洗、API、前端各管什么
拿到资源先别急着跑,第一件事是看清目录结构。整套系统按数据流向分成四层:采集层是爬虫模块,负责从豆瓣电影 Top250 页面抓取影片基础信息;处理层由 pandas 接手,做缺失值填充、类型拆分、评分区间映射这些脏活;存储层落到 SQL 脚本建好的数据表;展示层是 Flask 渲染的页面加上 ECharts 的图表组件。每一层都能单独调试,这一点在后期改需求时特别重要。
我第一次拆这类项目时习惯先把四个目录对号入座。爬虫模块一般独立成一个包,里面按页面职责拆了请求函数和解析函数;pandas 处理逻辑集中在数据清洗脚本里;app 目录放 Flask 路由和模板;static 里是 echarts.min.js 和自定义图表配置。这套划分的妙处在于每一层都能单独验证——爬虫挂了不影响前端看静态页,图表渲染不对也不用手忙脚乱地翻后端日志。
数据流向是单向的:爬虫把网页解析成 DataFrame,清洗后写入数据库,Flask 查询数据库并按图表需求聚合,最后以 JSON 形式交给前端 ECharts 渲染。这个链路里最容易出问题的是格式约定:后端的聚合结果很可能不符合前端期望的数组结构,所以 2.3 会把三个核心接口单独拿出来讲清楚。
2.2 数据库表结构与 SQL 脚本:从落库到查询接口
数据库用的是 MySQL,SQL 脚本建了一张电影主表和两张维度辅助表。电影主表字段覆盖片名、导演、主演、年份、地区、类型、评分、评价人数、一句短评等十几个字段,具体字段设计如下。
| 字段名 | 类型 | 说明 |
|---|---|---|
| movie_id | int | 自增主键 |
| title | varchar | 片名 |
| director | varchar | 导演 |
| rating | decimal | 评分,保留一位小数 |
| rating_num | int | 评价人数 |
| year | smallint | 上映年份 |
| types | varchar | 类型,多个类型用逗号分隔 |
| region | varchar | 制片地区 |
| quote | varchar | 短评 |
SQL 脚本里除了建表语句,还预置了部分样例数据,我建议第一次跑先用样例数据验证流程,确认 Flask 查询、ECharts 渲染都正常后,再决定要不要重新跑爬虫覆盖全量数据。导入脚本用一条命令就行:
mysql -u root -p douban_movie < douban_movie.sql注意 douban_movie 这个库名必须提前建好。脚本里如果带了 CREATE DATABASE,那直接执行也行,取决于作者是否把库创建语句写进了脚本。导入完成后用密码登录确认行数、抽查几条中文记录是否正常,这一步能避开一大半的后续返工。
2.3 Flask 蓝图与 ECharts 数据格式约定:前后端最重要的三个接口
Flask 部分用的是蓝图注册路由,接口设计得很直接。核心是三个数据接口:评分分布、年份趋势、类型占比。以年份趋势为例,后端做的事是查数据库、按 year 分组聚合、排序后返回一个字典列表。
@app.route("/api/trend") def trend(): rows = db.session.execute( "SELECT year, COUNT(*) AS cnt FROM movie GROUP BY year ORDER BY year" ).fetchall() return jsonify([ {"year": r[0], "count": r[1]} for r in rows ])这段代码逻辑很简单:执行分组聚合 SQL,把查询结果改造成列表字典结构。前端 ECharts 拿到这个 JSON 后,x 轴取每个字典的 year,y 轴取 count。值得记住的是这里用了字典列表而不是 DataFrame 直接转 JSON,因为 ECharts 的 data 字段只认数组结构,传 DataFrame 的索引对象会给前端带来额外转换负担。
评分分布接口的写法类似,只是 SQL 换成分段统计:
SELECT FLOOR(rating) AS bucket, COUNT(*) AS cnt FROM movie GROUP BY FLOOR(rating) ORDER BY bucket这里用 FLOOR 把评分映射到整数区间,8.5 和 8.9 都算 8 分档。如果你想让图表更细,改成 CEILING 或者按 0.5 分档都行,前端图表结构不用动。这就是分层设计的好处:后端怎么聚合只影响数值,前端只关心拿到的是不是数组。
3. 手把手复现:从爬虫采集到可视化大屏的完整流程
3.1 环境准备与依赖安装:版本锁定减少玄学报错
资源包配套的论文里通常会写依赖列表,核心是 Flask、pandas、requests、flask-sqlalchemy 和 pymysql。我个人强烈建议先用虚拟环境装,避免污染系统 Python。创建并激活环境、安装依赖:
python -m venv venv source venv/bin/activate pip install flask flask-sqlalchemy pandas requests pymysql装完 pymysql 后记得确认 MySQL 服务已启动,并且本机装了对应版本的驱动。Windows 环境激活虚拟环境是 venv\Scripts\activate,路径习惯不同但原理一样。这里最容易翻车的点是 pandas 和 numpy 的版本冲突,建议在 requirements.txt 的基础上再跑一遍 pip check,看到 "no broken requirements" 再继续。
提示:不要用系统全局 Python 直接跑这种多依赖项目,装了一堆包之后万一冲突,排查成本远高于重建一个虚拟环境。
3.2 爬虫模块:Top250 抓取与清洗逻辑
爬虫目标页是豆瓣电影 Top250,翻页参数是 start 偏移量,每页 25 条。核心代码要处理好请求头伪装、解析、字段抽取和异常重试四件事。
import requests from bs4 import BeautifulSoup import pandas as pd import time HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} BASE_URL = "https://movie.douban.com/top250" data = [] for start in range(0, 250, 25): resp = requests.get(BASE_URL, params={"start": start}, headers=HEADERS, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") items = soup.select(".grid_view .item") for it in items: title = it.select_one(".title").text.strip() rating = float(it.select_one(".rating_num").text) info = it.select_one(".bd p").text.strip().split("\n") info_line = info[1].strip() if len(info) > 1 else "" year_str = info_line.split("(")[0].strip() region = info_line.split("(")[1].split(")")[0].strip() if "(" in info_line else "" types = info_line.split(")")[1].strip() if ")" in info_line else "" quote_el = it.select_one(".quote .inq") quote = quote_el.text.strip() if quote_el else "" data.append([title, rating, year_str, region, types, quote]) time.sleep(2) df = pd.DataFrame(data, columns=["title", "rating", "year_str", "region", "types", "quote"]) df.to_csv("movies.csv", index=False, encoding="utf-8-sig")start 偏移量决定翻页,range(0, 250, 25) 正好覆盖 Top250 全部页面;time.sleep(2) 是请求间隔,两秒是多数情况下的安全下限;timeout=10 防止单个请求卡死整个循环。quote 字段做了空值兜底,因为 Top250 里有部分影片没有短评。选 .grid_view .item 这个选择器是因为它直接定位到每条电影卡片,比逐个找标题节点更稳。
解析部分依赖页面结构,info_line 里通常是 "1994 (美国) 剧情 / 犯罪" 这样的格式,所以用括号切分就能拆出年份、地区和类型。这种解析逻辑对格式变化敏感,但 Top250 页面结构多年没大改,实测能稳定跑通。如果在这里遇到 418 或 403,通常是请求头不够完整,补上 Accept-Language 和 Referer,或者把间隔调到 3 秒以上即可,这个放在第 4 章详细讲。
3.3 pandas 二次处理:评分、年份、类型分布的计算
爬虫得到的是原始 DataFrame,接下来要做字段拆分和聚合。年份从中文页面里提取,类型字段是逗号分隔的多值,需要 explode 展开再分组。
df["year"] = df["year_str"].str.extract(r"(\d{4})").astype(int) df["types"] = df["types"].str.split("/") df = df.explode("types") score_range = pd.cut(df["rating"], bins=[0, 6, 7, 8, 9, 10], labels=["<6", "6-7", "7-8", "8-9", "9-10"]) year_cnt = df.groupby("year").size().reset_index(name="count") type_cnt = df.groupby("types").size().sort_values(ascending=False).head(10)正则表达式 (\d{4}) 提取四位年份,防止年份字段里混入其他字符导致 astype(int) 报错。explode 的作用是把“类型”字段按斜杠拆开后变成多行,这样才能准确统计每种类型出现的次数。pd.cut 的 bins 边界是按豆瓣评分分布特点定的,榜单里 9 分以上算神作,8 到 9 是经典,7 到 8 是口碑片,低于 7 在榜单里几乎不存在,所以下区间合并成 <6。
这里最容易忽略的是类型字段里可能残留空格或全角字符,explode 之前最好先统一 strip,否则“剧情”和“剧情 ”会被当成两个类型,饼图里出现两条几乎一样的扇区。这个问题在 4.5 会再展开。
3.4 Flask 接口与 ECharts 渲染:参数调整与联动
后端接口返回 JSON 后,前端模板里用 ECharts 渲染折线图和饼图。下面这个例子是年份趋势折线图,它通过 fetch 请求 /api/trend 后把数据填进 option。
fetch("/api/trend") .then(r => r.json()) .then(rows => { const years = rows.map(item => item.year); const counts = rows.map(item => item.count); const chart = echarts.init(document.getElementById("trendChart")); chart.setOption({ tooltip: { trigger: "axis" }, xAxis: { type: "category", data: years }, yAxis: { type: "value" }, series: [{ type: "line", data: counts, smooth: true }] }); });smooth: true 会让折线圆滑,视觉上更接近论文里的示意图。特别注意 echarts.init 的容器元素必须已经有确定高度,否则图表会渲染成 0 高度。常见做法是在 CSS 里给 .chart-box 写死 400px 或 500px 高度。
到这里,整套链路从爬虫到图表已经通了一遍。你会发现整条链路最值得改的就是接口层的聚合逻辑,前端几乎不用动。评分分布改成柱状图、类型占比改成饼图,都只是改 series 的 type 字段和 option 里对应的配置。
4. 避坑指南:爬虫、数据库、图表五个高频翻车点
4.1 爬虫返回 418 或 403:不是被封,是请求头没给够
现象:requests 请求 Top250 页面,状态码返回 403 或 418,偶尔 200 但 HTML 内容里没有电影条目。
原因:服务器通过 User-Agent、Accept、Accept-Language 等头部判断客户端是不是浏览器。只给一个 UA 字符串在高峰时段很容易被识别拦截。
解决:把请求头补全,加上 Accept、Accept-Language、Referer 三个字段,同时把请求间隔从 1 秒调到 3 秒以上。一次抓 250 条数据也就多花几十秒,没必要冒险拉低间隔。如果还是 418,先停 5 分钟再跑,不要连续重试,重试永远不如等待有效。
4.2 MySQL 导入脚本后中文全部变成问号
现象:执行 SQL 脚本后,SELECT 查出来片名全是 ??。
原因:数据库连接客户端字符集和表结构字符集不一致。脚本里建表语句没有指定 utf8mb4,或者 mysql 命令行客户端默认用了 latin1。
解决:脚本导入前先执行一句 SET NAMES utf8mb4,建表语句里在表名后加 DEFAULT CHARSET=utf8mb4。已经建好的表可以直接 ALTER TABLE movie CONVERT TO CHARACTER SET utf8mb4; 再重新导入。这个问题没有后悔药,养成习惯,建表脚本第一行就写 SET NAMES。
4.3 ECharts 图表不渲染且控制台报 container 无宽度
现象:页面打开后图表区域空白,浏览器控制台提示 "Initialize failed: invalid dom"。
原因:echarts.init 执行时 DOM 元素还没有被 CSS 布局成可见尺寸。最常见是容器设置了百分比高度而父级没有高度。
解决:给图表容器一个固定高度,比如 .chart-box { height: 420px; }。如果页面用 flex 布局,确保 flex 子项有明确高度,或者用 chart.resize() 在 window.resize 事件里重新计算尺寸。div 宽高的问题十次里有八次是这个原因,先查 CSS 再查数据。
4.4 pandas 排序错乱:rating 列被当成字符串
现象:评分排序后出现 9.1、8.9、8.10 这种结果,看着像评分倒挂了。
原因:爬虫解析时 rating 被当成文本保留,字符串排序按字典序比较,8.10 排在 8.9 前面。
解决:在清洗阶段强制转类型。df["rating"] = pd.to_numeric(df["rating"], errors="coerce"),转换失败的自动变 NaN,后续再用 dropna 过滤。注意不要直接用 astype(float),因为遇到空字符串会直接抛异常中断整个清洗流程。
4.5 分组聚合结果比预期少:类型字段里的空白字符
现象:类型分布柱状图里,“剧情”和“剧情 ”同时出现,饼图被拆出两条几乎一样的扇区。
原因:原始页面里类型之间可能有空格或全角字符,split 后残留不可见字符导致分组键看起来一样但实际不同。
解决:explode 之前先统一清洗,df["types"] = df["types"].apply(lambda x: [i.strip() for i in x])。聚合后再验证一次唯一值数量:df["types"].nunique(),若与预期相差较大,打印所有唯一值检查。
5. 把毕业设计做到答辩能打:论文结构、演示路径与扩展方向
5.1 论文技术路线:跟着代码顺序写,别倒着编
资源包里的论文是完整版,结构上一般是:绪论、相关技术概述、系统需求分析、系统设计与实现、系统测试、总结。这六个章节的顺序和实际开发顺序完全吻合,照着自己的代码逐章填内容就行,不用倒着编。对应关系可以参考下面这个思路。
| 论文章节 | 应该写什么 | 对应工程里的依据 |
|---|---|---|
| 绪论 | 课题背景、国内外现状、研究意义 | 行业数据与选题背景 |
| 相关技术概述 | Flask、pandas、ECharts、爬虫技术 | 官方文档与教材 |
| 需求分析 | 功能性需求与非功能性需求 | 系统功能清单 |
| 设计 | 架构图、数据库ER图、接口设计 | 代码目录结构、SQL脚本 |
| 实现 | 各模块关键代码与运行截图 | 实际运行结果 |
| 测试 | 功能测试、性能测试、异常处理 | 接口返回与数据库记录 |
写实现章节时不要整段贴代码,先描述思路,再贴核心代码片段。答辩过不过只看两件事:你能不能讲清楚模块间的关系;数据流能不能从头说到尾。论文里每个接口最好配一张运行截图,截图就是 6.3 要说的演示材料。
5.2 答辩演示路径:三分钟讲清系统闭环
答辩最忌讳从爬虫代码开始讲,评委最想看到的是系统整体。我建议按“页面展示 → 数据接口 → 数据库 → 爬虫更新”的顺序走。先在浏览器打开可视化大屏,展示评分分布、年份趋势、类型占比三张图,说明每张图回答了什么问题,这一段控制在 40 秒内;再打开浏览器开发者工具切到 Network 面板,点一个接口查看返回 JSON,说明图表数据来源,这一段 30 秒;接着打开 MySQL 客户端展示原始表,抽查一条记录,说明字段如何清洗入库,这段 30 秒;最后回到爬虫模块,现场跑一次单页抓取,展示入库记录确实增加,这段 60 秒。
整个演示过程不要提任何一行函数名,只讲数据从哪来、经过什么处理、展示成什么样。评委如果追问具体实现,你手里有论文和代码,随时可以展开。如果现场网络环境不好,提前把爬虫结果缓存到 CSV,演示时直接从 CSV 读,避免现场翻车。
5.3 扩展方向:从 Top250 到交互式联动分析
如果导师要求扩展,最容易做的是把单页 Top250 扩展成多榜单爬取,比如按类型爬取评分最高的 100 部。改动点有三个:爬虫入口 URL 加参数,数据库加 type 字段,接口按类型过滤。这三个点涉及代码改动最小,但论文里能多出两章内容,没做过扩展的可以优先考虑。
另一个低成本扩展方案是把静态图表改成带交互筛选的联动图表,点击年份折线图的某个年份,饼图只展示该年份的类型分布。用 ECharts 的 click 事件加一次条件查询就能实现。
chart.on("click", params => { fetch(`/api/type?year=${params.value}`) .then(r => r.json()) .then(rows => pieChart.setOption({ series: [{ data: rows.map(x => ({ name: x.type, value: x.count })) }] })); });这个扩展代码量不大,但论文和演示中都明显加分。评委看到的是“系统不只是一个静态展示页面,而具备交互分析能力”。后端对应加一个 /api/type 接口,接收 year 参数后按类型分组返回,和 2.3 里的聚合写法完全一致。
6. 最后两小时:验证数据一致性、换端口、导答辩截图
答辩前最后一天,我通常不再动任何功能代码,只按固定流程做三件事:验证数据库行数与爬虫抓取条数一致;换掉默认端口避免现场冲突;导出图表截图并入论文。这三件事单看都不难,但每年顺手踩坑的人不少。
数据一致性验证先执行:
mysql -u root -p -e "SELECT COUNT(*) FROM douban_movie.movie;" python -c "import pandas as pd; df = pd.read_csv('movies.csv'); print(len(df))"对比两行输出是否相同,Top250 预期是 250 条。多一条可能是重复入库,少一条多半是请求被限流漏抓,按第 4 章的方案补一次即可。
端口冲突是演示翻车前几名。Flask 默认跑在 5000,本地开发时常被其他服务占用,改一行就稳妥:
app.run(host="0.0.0.0", port=8088, debug=False)host 设成 0.0.0.0 是为了应对答辩现场用另一台电脑访问,debug=False 防止出错时页面直接抛堆栈,观感很差。
最后导截图。把浏览器窗口固定到一个宽度,保证折线图和饼图比例正常,再截三张全页图,插进论文系统测试章节。截图不用修图,但窗口宽度一定要先固定,否则同一套数据在不同宽度下图表布局差异很大,论文里看起来不严谨。
说一个自己踩过的坑:有次演示前临时改代码,改完发现端口占用了,现场翻了半天才启动起来。从那以后,每次答辩前我都强制按“核对数据、换端口、截大图”三步走一遍再关机,这个习惯救了我好几次。这套资源拿回去,建议先原样跑通再做扩展,改任何模块前留一份能跑的备份。希望帮到你。
本文还有配套的精品资源,点击获取