基于Python的电影数据可视化分析系统:从爬取到Flask展示的毕业设计实战
简介这是一套面向高校计算机相关专业学生的毕业设计完整源码主题为基于Python的电影数据可视化分析系统适合正在准备毕设、需要可运行项目参考的初学者与进阶学习者。项目将豆瓣电影数据爬取后存入SQLite数据库再借助Flask框架搭建后端服务配合ECharts、BootStrap与WordCloud实现图表展示与词云效果形成一套从数据采集到可视化呈现的完整链路。压缩包共约2000个文件以1684个py源码为主辅以124个html页面、93个js脚本及css样式、txt说明、pdf文档等整体约76.51MB目录结构清晰便于按模块查阅与二次修改。该资源经严格调试评审分达95分以上已有1018人学习下载。读者可据此掌握爬虫编写、数据库存储、Web后端与前端图表联调等关键环节并参考其页面组织与代码分层思路快速搭建自己的可视化分析项目。1. 电影数据可视化分析系统从一份毕业设计压缩包说起很多计算机毕业设计的选题最后都会落到「基于 Python 的某某数据分析与可视化系统」上电影数据这个方向尤其常见。原因很直接数据公开、字段丰富、图表好看答辩时演示效果直观。但真正动手做的时候大部分人卡在三个地方——数据从哪来、清洗到什么程度、可视化怎么组织成一个「系统」而不是几张散图。这份「基于 Python 的电影数据可视化分析系统」要解决的就是把爬取、清洗、存储、分析、展示串成一条能跑通的链路。它适合正在做计算机毕业设计、需要一套完整可复现方案的人也适合刚学完 Python 基础语法、想找一个真实项目练手的入门者。下面我按实际落地的顺序把这条链路拆开讲清楚包括每一步用什么库、参数怎么设、哪里容易翻车。2. 数据从哪来爬取方案选型与字段设计2.1 为什么优先选静态榜单页而不是动态接口电影数据的来源大致分三类公开数据集如 MovieLens、豆瓣历史评分导出、网页爬取、第三方 API。毕业设计里最稳的是「公开数据集 少量爬取补充」因为纯爬取会遇到反爬、字段缺失、页面结构变动等问题答辩前一周页面改版是真实会发生的血泪经验。如果一定要爬优先选静态榜单页。静态页面的 HTML 结构在请求后直接返回用 requests BeautifulSoup 就能解析不需要处理 JavaScript 渲染。动态接口XHR 返回的 JSON虽然数据干净但需要分析请求参数、处理签名或 token对毕业设计的投入产出比不划算。常见做法是用公开数据集打底保证有几千条以上的稳定数据再用爬虫抓一小批最新榜单体现「数据获取」这个环节的工作量。两者合并后统一字段。2.2 字段设计一张电影表该存哪些列字段设计决定了后面能画什么图。很多人一开始只存了电影名和评分做到可视化阶段发现没法按类型、年份、导演做分组只能回头重爬。建议一开始就定好下面这张表字段名类型说明是否必填movie_idVARCHAR(20)唯一标识可用榜单排名或哈希是titleVARCHAR(200)电影名称是yearINT上映年份是ratingFLOAT评分统一到 0-10是rating_countINT评价人数否genresVARCHAR(200)类型多个用竖线分隔是directorVARCHAR(100)导演否actorsVARCHAR(500)主演多个用竖线分隔否countryVARCHAR(100)制片国家/地区否durationINT片长分钟否genres 和 actors 用竖线分隔而不是拆成多表是为了降低毕业设计的复杂度。真正做多对多关联查询会让代码量翻倍而可视化阶段用str.split(|)展开就够了。2.3 爬取脚本的最小实现下面这段是抓静态榜单页的骨架重点是请求头、超时和重试三个参数import requests from bs4 import BeautifulSoup import time import random 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, retry3): for i in range(retry): try: resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except requests.RequestException as e: print(f第{i1}次请求失败: {e}) time.sleep(random.uniform(1, 3)) return None def parse_list(html): soup BeautifulSoup(html, html.parser) items [] for node in soup.select(.list-item): items.append({ title: node.select_one(.title).get_text(stripTrue), rating: float(node.select_one(.rating).get_text(stripTrue)), year: node.select_one(.year).get_text(stripTrue), }) return items if __name__ __main__: html fetch_page(https://example.com/movie/list?page1) if html: for row in parse_list(html): print(row)逻辑说明fetch_page做了三件事——带请求头伪装、设 10 秒超时、失败后随机间隔重试。timeout不设的话遇到慢响应会一直挂着random.uniform(1, 3)是给请求之间加随机间隔降低被封的概率。resp.apparent_encoding用来处理中文页面编码不一致的问题比写死utf-8稳。参数说明retry3是经验值再多会拖慢整体速度timeout10对静态页足够如果目标站响应慢可以调到 15。soup.select里的 CSS 选择器要按实际页面改这是最容易失效的地方建议把选择器单独抽成配置。提示爬取前先确认目标站的 robots.txt 和使用条款控制请求频率只抓毕业设计需要的量级不要做全站遍历。3. 清洗与存储把脏数据变成能画图的数据3.1 清洗的四个高频问题爬下来或下载的数据几乎不可能直接用来画图。常见问题有四类第一评分量纲不统一。有的站是 10 分制有的是 5 分制混在一起画分布图会失真。统一乘除到 10 分制。第二年份字段混入非数字。比如「2023(中国大陆)」这种需要正则提取前四位数字。第三类型字段分隔符不一致。有的用「/」有的用「|」有的用空格。统一替换成竖线。第四缺失值。导演、演员这类字段缺失很常见画图时要么过滤要么填「未知」不要留 NaN 直接进绘图函数否则图表会静默丢数据。3.2 用 pandas 做清洗的完整流程import pandas as pd import re def clean_movies(df): # 1. 年份提取前四位数字 df[year] df[year].astype(str).str.extract(r(\d{4})) df[year] pd.to_numeric(df[year], errorscoerce) # 2. 评分统一到 10 分制 df[rating] pd.to_numeric(df[rating], errorscoerce) df.loc[df[rating] 5, rating] df.loc[df[rating] 5, rating] * 2 # 3. 类型统一分隔符 df[genres] df[genres].astype(str).str.replace(r[/、\s], |, regexTrue) # 4. 缺失值处理 df[director] df[director].fillna(未知) df[duration] df[duration].fillna(df[duration].median()) # 5. 去重按标题年份 df df.drop_duplicates(subset[title, year], keepfirst) # 6. 过滤异常年份 df df[(df[year] 1900) (df[year] 2025)] return df.reset_index(dropTrue) df pd.read_csv(movies_raw.csv) df_clean clean_movies(df) df_clean.to_csv(movies_clean.csv, indexFalse, encodingutf-8-sig)逻辑说明str.extract(r(\d{4}))把年份里的非数字部分剥掉评分那行用loc条件判断只对 5 分制的记录乘 2避免把 10 分制的也放大类型替换用正则一次处理多种分隔符去重按「标题年份」是因为同名电影可能翻拍只按标题去重会误删。参数说明errorscoerce让无法转换的值变成 NaN 而不是报错这是清洗阶段的标准做法。encodingutf-8-sig是为了 Excel 打开 CSV 不乱码答辩演示时经常要在 Excel 里看数据。3.3 存储选型SQLite 还是直接 CSV毕业设计阶段数据量通常在几千到几万条SQLite 完全够用而且不用装数据库服务打包进压缩包就能跑。CSV 更简单但每次分析都要重新读文件、重新清洗效率低。我的建议是清洗后写入 SQLite分析时用 pandas 的read_sql读出来。这样数据只清洗一次后面所有图表都基于同一份干净数据。import sqlite3 conn sqlite3.connect(movies.db) df_clean.to_sql(movies, conn, if_existsreplace, indexFalse) # 分析时读取 df pd.read_sql(SELECT * FROM movies WHERE rating 8.0, conn) conn.close()if_existsreplace表示每次运行都重建表适合开发阶段。如果数据是增量更新的改成append并加去重逻辑。4. 可视化怎么做成「系统」而不是几张图4.1 图表选型什么数据配什么图可视化不是图越多越好而是每张图回答一个具体问题。下面这张对照表是我做这类系统时固定用的分析目标推荐图表库评分分布直方图matplotlib / pyecharts类型占比饼图或环形图pyecharts年份与评分关系折线图或散点图pyecharts各类型平均评分横向柱状图pyecharts导演作品数量 Top10柱状图pyecharts国家/地区分布地图pyecharts选 pyecharts 而不是纯 matplotlib是因为它生成的图表带交互悬停显示数值、图例点击隐藏在浏览器里演示效果明显更好而且能直接嵌入 Web 页面。4.2 用 pyecharts 生成核心图表from pyecharts.charts import Bar, Pie, Line from pyecharts import options as opts import pandas as pd df pd.read_csv(movies_clean.csv) # 各类型平均评分 genre_rows [] for _, row in df.iterrows(): for g in str(row[genres]).split(|): if g and g ! nan: genre_rows.append({genre: g, rating: row[rating]}) genre_df pd.DataFrame(genre_rows) avg genre_df.groupby(genre)[rating].mean().sort_values(ascendingFalse).head(10) bar ( Bar() .add_xaxis(avg.index.tolist()) .add_yaxis(平均评分, [round(v, 2) for v in avg.values]) .set_global_opts( title_optsopts.TitleOpts(title各类型平均评分 Top10), xaxis_optsopts.AxisOpts(axislabel_optsopts.LabelOpts(rotate30)), yaxis_optsopts.AxisOpts(name评分), ) ) bar.render(genre_rating.html)逻辑说明先把 genres 字段按竖线展开成多行再按类型分组求平均。head(10)只取前 10避免柱状图太挤。rotate30是让 x 轴标签倾斜中文类型名较长时不倾斜会重叠。参数说明round(v, 2)控制小数位图上显示两位就够。render输出 HTML 文件可以直接用浏览器打开也可以嵌入 Flask 页面。4.3 用 Flask 把图表串成一个系统几张独立的 HTML 不叫系统。加一层 Flask把图表挂到路由上再配一个首页导航才算完整。from flask import Flask, render_template import pandas as pd from pyecharts.charts import Bar from pyecharts import options as opts app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/genre) def genre_chart(): df pd.read_csv(movies_clean.csv) # ... 同上的聚合逻辑 ... bar Bar().add_xaxis(avg.index.tolist()).add_yaxis(平均评分, avg.values.tolist()) return render_template(chart.html, chartbar.dump_options_with_quotes()) if __name__ __main__: app.run(debugTrue, port5000)逻辑说明dump_options_with_quotes()把图表配置序列化成 JSON前端用 echarts 初始化时直接吃这个配置。这样图表数据是后端算好传过去的前端只负责渲染。参数说明debugTrue只在开发时用正式演示前关掉否则改代码会自动重启。port5000是 Flask 默认端口被占用就换 5001。注意模板文件要放在templates/目录下静态资源放static/这是 Flask 的默认约定放错位置会报 TemplateNotFound。5. 避坑与排查那些答辩前才暴露的问题5.1 中文乱码图表标题变成方块现象pyecharts 生成的图表里中文标题和坐标轴标签显示成方块或问号。原因HTML 模板里没有声明 UTF-8 编码或者 CSV 读取时编码不对。解决在 HTML 的head里加meta charsetUTF-8读 CSV 时显式指定encodingutf-8-sig。如果数据源是 GBK先转成 UTF-8 再存。5.2 端口被占用Flask 启动报 Address already in use现象运行app.run()时报错端口 5000 已被占用。原因上一次运行的进程没退干净或者系统里有其他服务占了 5000。解决换端口app.run(port5001)或者用lsof -i :5000Linux/Mac、netstat -ano | findstr 5000Windows找到进程号后结束它。5.3 爬虫被封请求返回 403 或空内容现象前几次请求正常后面全部返回 403或者返回的 HTML 里没有目标数据。原因请求频率过高触发风控或者 User-Agent 被识别。解决加大请求间隔time.sleep调到 2-5 秒轮换 User-Agent必要时加 Referer 头。如果还是不行说明目标站反爬较强换公开数据集更省事。5.4 图表数据为空图上什么都没有现象图表能渲染出来但没有任何数据点或柱子。原因分组聚合后结果为空通常是过滤条件太严或者字段值全是 NaN。解决在聚合前打印df.shape和df.head()确认数据确实读进来了检查过滤条件比如rating 8.0是否把所有记录都排除了检查分组字段是否有值。5.5 打包后跑不起来换台电脑就报 ModuleNotFoundError现象在自己电脑上正常拷到同学电脑或答辩教室的机器上就报缺库。原因依赖没导出或者 Python 版本不一致。解决用pip freeze requirements.txt导出依赖对方用pip install -r requirements.txt安装。更稳的做法是把整个项目放进虚拟环境或者用 PyInstaller 打包成可执行文件。答辩前一定要在目标机器上完整跑一遍。6. 让系统更像「系统」三个进阶技巧第一个技巧是加数据更新入口。在 Flask 里加一个/refresh路由点击后重新执行爬取和清洗把新数据写入 SQLite。这样演示时能体现「系统是活的」而不是一份静态数据。实现上把爬取和清洗封装成函数路由里调用即可注意加一个简单的执行状态提示避免用户重复点击。第二个技巧是加筛选交互。pyecharts 本身支持部分交互但更灵活的做法是在前端加下拉框选择年份或类型后通过 AJAX 请求后端重新聚合数据并刷新图表。后端接口返回 JSON前端用 echarts 的setOption更新。这个改动不大但演示效果提升明显。第三个技巧是加导出功能。答辩老师经常会问「这个结果能不能导出」提前加一个「导出 CSV」按钮把当前筛选后的数据用 pandas 的to_csv写出来前端用a download触发下载。实现成本很低但能堵住一个常见提问。验证系统是否完整我一般按这个清单过一遍数据能重新爬取或加载、清洗后无异常值、每张图都有数据、筛选后图表能更新、换台机器能跑起来、导出功能正常。这六项都过了基本不会在答辩现场翻车。我自己做这类项目最大的教训是不要一开始就追求图表数量先把数据链路跑通哪怕只有三张图只要每张都有明确的分析目标比堆十张没人看得懂的图强得多。另外requirements.txt 和 README 一定要写答辩前一周你永远不知道自己会忘了哪个库的版本。希望帮到你。本文还有配套的精品资源点击获取