简介:面向毕业设计场景的电影数据可视化分析系统源码包,适合计算机相关专业学生用于课程设计、毕设参考或数据分析实践。项目将豆瓣电影数据爬取后存入SQLite,基于Flask搭建后端,结合ECharts、Bootstrap、WordCloud完成前端可视化展示,覆盖数据采集、存储、接口开发与图表呈现的完整流程。压缩包共2000个文件,以1684个Python脚本为核心,辅以124个HTML页面、93个JavaScript交互脚本、18个CSS样式文件,另有少量TXT数据说明、PDF文档与Markdown笔记,整体体积约76.51MB,目录结构清晰,便于按模块阅读。源码经严格调试,评审得分95分以上,具备开箱即用的可运行性,并已吸引1018人学习下载。下载后可获得完整项目源码、数据文件及页面模板,既能直接运行观察可视化效果,也可参考其爬虫、Flask路由与ECharts配置方式,为毕业设计或数据分析项目提供可复用的实现思路。
1. 这个毕业设计到底在做什么:不是画图,是把“电影数据”变成能答辩的完整闭环
先说一个可能反直觉的结论:这类“基于Python的电影数据可视化分析系统”,真正让你熬夜的通常不是画图,而是“数据从哪里来、怎么洗干净、用什么口径去分析”。如果你只是从网上下一个 csv 再画几张柱状图,那它叫“作业”不叫“系统”;毕业设计要的是一个能自圆其说的闭环——从数据采集、清洗、存储,到指标计算和可视化展示,每一步都有你的设计理由。
这个标题对应的其实是一套非常标准的“数据分析 + 可视化”全流程项目,常见做法是用 Python 做数据处理(pandas、numpy),把豆瓣、TMDB、猫眼等平台的电影数据抓取或整理成结构化表格,然后通过图表展示票房、评分、类型分布、年份趋势等维度的规律。适合的人群很明确:计算机、大数据、信息管理相关专业的本科生或研究生,尤其是需要快速搭建一个演示性强、有真实数据支撑、能讲清楚“我做了什么”的毕设项目。
接下来我按自己做这类项目的习惯,把整条链路拆开讲:数据怎么来、怎么洗、怎么存、怎么分析、怎么可视化,以及最后怎么封装成一个能演示的系统。你按这个顺序做,每一步都有对应的代码和参数可以抄,遇到问题也有地方查。
2. 先用 Python 把电影数据“盘活”:抓取、解析与本地化存储
2.1 数据获取的三个来源:爬虫采集、公开数据集、手工整理
做电影数据可视化,第一步不是写代码,而是确定数据源。我接触过的毕设项目里,最常见的数据来源有三个:爬虫采集(比如豆瓣 Top250、猫眼票房榜)、公开数据集(Kaggle 的 TMDB 5000 Movie Dataset、IMDb 子集)、以及导师或课程提供的 csv 文件。
三者各有适用场景。如果你想要“工作量足、有技术含量、答辩有故事可讲”,爬虫是首选,它天然覆盖网络请求、HTML 解析、反爬应对、异常处理这些点;如果时间紧或者评委更看重分析逻辑,公开数据集更省事,但你需要额外说明数据来源和口径。我一般建议是:主数据用公开数据集打底,再写一个小爬虫补充实时数据或特定榜单,这样既有稳定性又有亮点。不要一上来就全量爬,容易被反爬机制卡住,后面会专门讲。
2.2 用 requests + BeautifulSoup 抓取豆瓣 Top250:最小可运行代码
以豆瓣电影 Top250 为例,这个页面结构稳定、数据字段完整(片名、评分、评价人数、经典台词、年份、地区、类型),是毕业设计出镜率最高的目标。这段代码是当时我调试好留存的版本,你可以直接在 PyCharm 里跑。
import requests from bs4 import BeautifulSoup import time import csv headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" } all_movies = [] for page in range(10): # Top250 共10页,每页25条 url = f"https://movie.douban.com/top250?start={page * 25}&filter=" resp = requests.get(url, headers=headers, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") for item in soup.select(".item"): title = item.select_one(".title").get_text() rating = item.select_one(".rating_num").get_text() quote = item.select_one(".quote span") quote = quote.get_text() if quote else "" # 提取年份/地区/类型,示例:1994 / 美国 / 犯罪 剧情 info = item.select_one(".bd p").get_text().strip().split("\n")[1].strip() all_movies.append([title, rating, quote, info]) time.sleep(2) # 适度休眠,降低请求频率 with open("douban_top250.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["title", "rating", "quote", "info"]) writer.writerows(all_movies) print(f"抓取完成,共 {len(all_movies)} 条数据")这段代码的逻辑很直白:循环请求10页,解析每页的.item节点,取出标题、评分、台词和元信息,最后写入 csv。这里有两个参数你务必理解,不要照抄了事。第一个是headers,里面必须带 User-Agent,豆瓣会拒绝裸请求返回 418;第二个是encoding="utf-8-sig",这个编码会在文件头写入 BOM,用 Excel 打开 csv 时中文不乱码,如果你后续用 pandas 读取,建议改成utf-8避免字段名带\ufeff前缀。
2.3 解析字段时的常见问题:评分是字符串,年份藏在段落里
很多人在第一次跑这种代码时,拿到的 csv 打开看是“对”的,但放进数据分析阶段才发现问题。比如info字段是1994 / 美国 / 犯罪 剧情,评分是"9.7"这种字符串,评价人数在另一个节点里还没被取出来。这就引出一个原则:采集阶段多取原始字段,清洗阶段再做转换。
我的习惯是每个字段都保留原始文本,哪怕是混合在一起的段落文本也没关系,这样信息不丢,后面用正则或者 split 去拆即可。再补一个字段提取的小技巧:如果要单独把年份和地区拆成两列,用这一行就能搞定。
import re def parse_info(info_text: str) -> dict: parts = [p.strip() for p in info_text.split("/")] # 示例: ['1994', '美国', '犯罪 剧情'] year = re.search(r"\d{4}", parts[0]) return { "year": year.group() if year else "", "region": parts[1] if len(parts) > 1 else "", "genre": parts[2] if len(parts) > 2 else "", }靠正则从字符串里抽出四位年份,再用长度判断兜底。为什么用re而不是直接切片?因为有些条目的年份可能是1994(中国大陆)或1994-01-01这种形态,直接切片会漏。与常见误用不同,不要在爬虫阶段就把这些逻辑塞进页面解析里,遇到页面结构调整你就得全量重写;分离采集与解析,出错了只改一个函数就好。
3. 数据清洗与存储:把 csv 变成能直接做分析的 DataFrame
3.1 pandas 数据处理的基本盘:读取、去重、类型转换
数据拿到手之后,接下来最重要的不是画图,而是把数据整理成“可分析状态”。用 pandas 读取刚才的 csv,然后做四件事:去掉完全重复的行、把评分列从字符串转成浮点数、从信息列里拆出年份和类型、过滤掉缺失严重的行。这是任何电影分析系统落地时都会遇到的基础数据预处理步骤。
import pandas as pd df = pd.read_csv("douban_top250.csv", encoding="utf-8-sig") # 1) 去重:以标题为准,保留第一条出现的记录 df = df.drop_duplicates(subset=["title"]) # 2) 评分转数值,出错时置空(coerce 表示无法转换则取 NaN) df["rating"] = pd.to_numeric(df["rating"], errors="coerce") # 3) 拆分info字段,生成三个新列 infos = df["info"].apply(parse_info) df["year"] = infos.apply(lambda x: x["year"]) df["region"] = infos.apply(lambda x: x["region"]) df["genre"] = infos.apply(lambda x: x["genre"]) # 4) 删除关键字段为空的行 df = df.dropna(subset=["rating", "year"]) df = df.reset_index(drop=True) print(df.shape) print(df.dtypes)这段代码里有两个关键参数需要刻意理解。subset=["title"]是去重的判断依据,你可以改成["title", "year"],因为两部同名电影可能年份不同;errors="coerce"是pd.to_numeric的参数,它让非法输入变为NaN而不是抛异常,这样你可以用dropna统一过滤掉脏数据。实际项目中我一般会把print(df.shape)放在清洗前后各一次,人工对比减少量,以此确认清洗逻辑没有误伤数据。
清洗完之后,建议顺手统计一下年份范围、评分的分布、类型字段的取值情况,这些就是你做可视化时“要讲什么故事”的素材。比如你会发现评分集中在小数点后一位,8.5~9.0 区间的数量最多,这就可以成为分析指标的起点。
3.2 存储选型:为什么毕设不一定要上 MySQL
数据清洗到这份上,接下来面临一个问题:数据存哪?很多毕设会把“用了 MySQL”当卖点,但对于 250 条或 5000 条的电影数据,上数据库的实际价值不大,还增加部署复杂度。我一般会分两种情况处理:如果系统里需要交互式查询(按年份筛选、按类型筛选、支持组合条件),就用 SQLite,零配置文件、Python 内置支持;如果只是纯静态展示,直接把清洗后的 DataFrame 导出成新的 csv 或 Excel 即可。
SQLite 是毕设里性价比最高的折中方案。它只有一个 .db 文件,不需要安装服务端,还能写 SQL 展示你的数据库能力。
import sqlite3 conn = sqlite3.connect("movies.db") df.to_sql("movie_top250", conn, if_exists="replace", index=False) conn.close()to_sql这个接口是 pandas 封装的,等价于建表 + 逐行插入。if_exists="replace"表示每次运行都覆盖旧表,适合开发调参阶段;如果到了演示阶段,建议改成"append"或者直接删除分支,避免误操作把数据清掉。这里有个细节:df.to_sql默认会按 pandas 的列类型推断 SQLite 的字段类型,如果后续要写 SQL 做范围查询,建议在to_sql之前手动把年份列转成 int。否则 SQLite 里存的是文本,WHERE year BETWEEN 2000 AND 2010查不准。
3.3 构造“能被答辩追问”的分析指标
数据入库不是终点,分析才是。毕设答辩时评委最常问的一句话是:“你这指标体系是怎么设计的?”如果回答“就是把年份和评分画一下”,会显得没有分析逻辑。我建议你在系统里定义几组固定的分析维度,每一组对应一张图。
常见的分析切入点有五个:电影年份分布(看产量趋势)、评分分布直方图(看整体评分结构)、类型与评分的关系(箱线图或均值对比)、地区与数量的关系(柱状图)、评分与评价人数的相关性(散点图)。这几个维度全部能从刚才的 DataFrame 计算出来,不需要额外采集数据。
# 1) 各年份电影数量 year_count = df.groupby("year").size().reset_index(name="count") # 2) 评分分布(按0.5分一档) rating_bins = pd.cut(df["rating"], bins=[8.0, 8.5, 9.0, 9.5, 10.0], right=False) rating_dist = rating_bins.value_counts().sort_index() # 3) 类型与评分:先拆类型字段,展开成多行再算均值 genre_rating = df.assign(genre=df["genre"].str.split(" ")) genre_rating = genre_rating.explode("genre") genre_rating_mean = genre_rating.groupby("genre")["rating"].mean().sort_values(ascending=False)上面这段代码有三个点值得解释。pd.cut的bins我特意留了 8.0 以下没放进区间,是因为 Top250 最低分约 8.2,分箱能呈现高分段内部的密度差异;right=False让区间左闭右开,评分 8.5 会落在[8.5, 9.0)而不是[8.0, 8.5),这是处理浮点边界时常见的坑。explode("genre")是把“犯罪 剧情”这种多类型文本拆成两行,这样每部电影参与多次统计,算均值时单个类型样本量更充足。
如果你在答辩时需要回答“为什么用均值不用中位数”,补一句即可:评分数据近似正态,且无极端值,均值与中位数差异极小,用均值更直观。这样整个分析逻辑就闭环了。
4. 数据可视化落地:从 Matplotlib 静态图到 ECharts 交互看板
4.1 PyECharts 4 / 5 系列选型与渲染方式选择
到了可视化这一步,你就需要决定“用什么画图”。通常有两类选择:Matplotlib + Seaborn 适合生成论文插图,出的是静态 PNG,规格统一;PyECharts 适合做网页动态展示,图表可以带 tooltip、缩放、图例切换等交互。对毕业设计而言,我的建议是两者结合使用:论文/报告里插图用 Matplotlib,系统演示界面用 PyECharts,两个都出现会让你的工作量看起来更饱满,也是 ECharts 数据可视化大屏这类项目常见的落地方式。
PyECharts 的使用门槛比 Matplotlib 稍高一点,它采用“链式调用”风格。核心逻辑是:先创建一个图对象,设置add()方法传入数据,再用render()输出 html 文件。下面是一个基础的柱状图示例,展示各类型电影的平均评分。
from pyecharts.charts import Bar from pyecharts import options as opts # genre_rating_mean 来自上一节的聚合结果 categories = genre_rating_mean.index.tolist() values = [round(v, 2) for v in genre_rating_mean.values] bar = ( Bar() .add_xaxis(categories) .add_yaxis("平均评分", values) .set_global_opts( title_opts=opts.TitleOpts(title="不同类型电影平均评分对比"), yaxis_opts=opts.AxisOpts(min_=8.0, max_=9.5), # 控制Y轴范围,突出差异 ) ) bar.render("genre_rating_bar.html")add_yaxis是核心的数据注入接口,set_global_opts用来配置标题、坐标轴、图例等全局属性。注意到我把min_设为 8.0、max_设为 9.5,这是关键参数:不设的话,ECharts 会从 0 开始显示,所有柱子的高度差会被压缩得几乎看不见,观感很差。这类坐标轴范围控制适用于所有评分对比场景,也是企业级数据可视化交付时最先被画图人员要求调整的细节。
PyECharts 的输出文件是一个完整的 html,你可以把它嵌入到 Flask 模板中。静态图与动态图的技术栈差异就在这一步体现出来:Matplotlib 画完是“图片文件”,PyECharts 画完是“交互组件”,后者能直接放到 Web 前程里复用。
4.2 把图表挂到 Flask 页面里:搭建最小可演示的 Web 系统
完整的数据可视化系统,不能只停留在“生成一堆 html 文件”的阶段。你需要一个入口页面,把多张图表组织在一起,供用户切换查看。Flask 是 Python 生态里做这件事最轻的方案,也符合“Python 可视化系统”这个标题的预期。下面是一个最小可运行的 Flask + PyECharts 集成模板。
先写 Python 主程序:
from flask import Flask, render_template from pyecharts.charts import Bar, Line, Pie from pyecharts import options as opts import pandas as pd app = Flask(__name__) # 启动时加载一次数据,避免每次请求都读csv df = pd.read_csv("douban_top250_clean.csv", encoding="utf-8-sig") def create_year_chart(): year_count = df.groupby("year").size().sort_index() line = ( Line() .add_xaxis(year_count.index.tolist()) .add_yaxis("电影数量", year_count.values.tolist()) .set_global_opts(title_opts=opts.TitleOpts(title="电影数量年度趋势")) ) return line @app.route("/") def index(): chart = create_year_chart() return render_template("index.html", chart_html=chart.render_embed()) if __name__ == "__main__": app.run(debug=True)对应的templates/index.html文件核心部分:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>电影数据分析系统</title> </head> <body style="font-family: 'Microsoft YaHei', sans-serif; margin: 30px;"> <h2>基于Python的电影数据可视化分析系统</h2> <div>{{ chart_html | safe }}</div> </body> </html>注意render_template("index.html", chart_html=chart.render_embed())这行,它是整条链路的关键:render_embed()会返回一段完整的 JavaScript + HTML 字符串,模板里用| safe告诉 Jinja2 这段字符是预先渲染好的,不需要转义。如果不加safe过滤器,页面上会直接显示<div>源码。.py 文件里app.run(debug=True)开启调试模式,改动后自动重启,但毕设演示要记得关掉或设置为False,否则他人访问你的端口时能看到调试器交互界面,存在安全隐患。
4.3 图表配色与布局:让演示效果接近“数据可视化大屏”
技术实现走通以后,真正拉开分数差距的是“展示细节”。同一个数据,直接使用默认主题和花 20 分钟调过的配色,在答辩现场的观感完全不同。我不建议用 Matplotlib 默认的蓝色列表样式,也不建议在 PyECharts 里反复堆砌颜色参数。我一般是直接复用现成的配色方案,例如 PyECharts 内置的chalk、infographic主题,或者指定自定义颜色数组。
自定义颜色的示例很简单,在add_yaxis时通过itemstyle_opts控制:
from pyecharts.charts import Pie from pyecharts import options as opts genre_count = df["region"].value_counts() pie = ( Pie() .add( series_name="地区分布", data_pair=[list(z) for z in zip(genre_count.index, genre_count.values)], radius=["35%", "60%"], # 内径35%,外径60%,形成环形图 ) .set_global_opts(title_opts=opts.TitleOpts(title="产地分布占比")) )radius=["35%", "60%"]是做出环形图的关键参数,它的含义是内径和外径相对容器宽度的比例。这个比例做出来视觉效果比实心饼图更清爽,也更贴近毕设系统中常见的可视化风格。页面层面,用 CSS 的display: flex把多个图表容器横向排列,配合之前 Flask 传图表的方式,就能在首页呈现两个或三个图表并排的效果,再往上做就是一张简单的大屏幕看板。
5. 参数调优与避坑:我用这套系统跑数据踩过的 5 个坑
5.1 评分列读进来是 object 类型,画图时全部报错
现象:从 csv 读取数据后,df["rating"].dtype显示为object,plt.plot时提示无法将字符串转换为浮点数。
原因:csv 中评分列存在空值或空格字符。pandas 读取时整列被推断为字符串类型,后续没有做类型转换直接使用。
解决:读取后统一执行df["rating"] = pd.to_numeric(df["rating"], errors="coerce"),然后dropna去除空行。这个动作必须放在任何聚合统计之前。我在排查时发现一个隐蔽变种:某些爬虫将评分写成了"9.7 "(带尾随空格),errors="coerce"可以完美处理这种情况,比strip()后再转换更省事。
5.2 PyECharts 生成的 html 在中文路径下无法打开
现象:文件放到D:\毕业论文\charts\演示.html路径下,双击浏览器显示一片空白,控制台报 ECharts 资源加载失败。
原因:PyECharts 默认通过相对路径引用本地 js 资源,在含中文或特殊字符的路径下,某些浏览器的静态资源加载策略会出问题。
解决:换用chart.render("demo.html")放到纯英文路径下;或者调用bar.render_embed()将图表直接嵌入 Flask 模板,跳过独立的 html 文件。项目目录结构我建议从一开始就建成英文命名,整个毕设源码压缩包和演示文件都遵守这个约定,避免莫名奇妙的编码问题。
5.3 爬虫跑一半被拒绝访问,提示 418 或触发验证码
现象:循环请求到第3页或第4页时,resp.status_code变成 418,页面内容不再是电影列表。
原因:请求频率过高,同一 IP 在短时间内被网站限流。也可能是部分页面返回了反爬挑战页。
解决:加重试机制和延时。延时设置在 2~5 秒之间,不要固定间隔,用time.sleep(random.uniform(2, 5))模拟人工浏览节奏。再增加一个简单的重试逻辑:请求失败后等待更长时间,最多重试 3 次。这条建议适用于几乎所有站点的数据采集,“睡一觉再继续”比“疯狂重试”有效得多。
5.4 Flask 模板里图表不显示,只有一串警告文字
现象:页面能打开,但图表区域显示类似 “This is an ECharts option...” 的字符串或 JavaScript 代码。
原因:render_embed()返回值在 Jinja2 模板中被自动转义,浏览器把标签当作普通文本显示了。这是新手最容易遇到的情况。
解决:模板中必须使用| safe过滤器,即写成<div>{{ chart_html | safe }}</div>。如果使用的是render_template_string而非模板文件,同样需要加| safe。这个坑每天都会“坑”掉若干人,但解决方案只有这一行,记住即好。
5.5 打包部署时 pyecharts 资源文件缺失,页面图表空白
现象:在自己电脑上运行正常,把项目复制到另一台电脑或打包成 exe 后,网页图表加载不出来。
原因:PyECharts 的静态资源(js 文件)存在于库目录内,复制项目时未被同步过去;或打包工具未将 ECharts 的 js 资源包含进产物。
解决:确认复制完整的项目目录包括templates、static和venv依赖;打包成 exe 时,在打包配置里添加pyecharts的资源目录。最稳妥的办法是:不打包,直接让对方安装 Python 依赖后运行源码,毕设答辩场景下用源码演示反而更能体现工作量。
6. 进阶:把数据可视化分析系统做成“有结论”的演示项目
毕设答辩的时间有限,通常在 10 到 15 分钟之间。你能演示的操作其实是有限的。与其让评委在系统里到处点,不如把核心分析结论直接呈现在首页,让评委一眼看到“这个系统得出了什么发现”。我建议你做一个“分析结论区”,用卡片或文本块列出 3 到 4 条关键发现,例如“评分 9.0 以上的电影占比约 12%”“2000 年后电影数量占比超过一半”“剧情类电影的平均评分显著高于动作类”。每条结论后面配上对应图表入口,讲的时候有的放矢。
如果你想再进一步,有两个高性价比的进阶方向。第一个是给 Flask 系统增加一个搜索框,支持按电影名查询评分和类型,用df[df["title"].str.contains(keyword)]即可实现;第二个是做一个简单的年度票房或评分排行榜,用df.nlargest(10, "rating")快速计算并展示 Top10 榜单。这两个功能的代码量都很小,但会显著提升系统完整性,让项目看起来更接近一个真正可用的分析系统。
最后一个血泪教训:从开发的第一天起,就用虚拟环境管理依赖,不要直接使用全局 Python 环境安装包。我见过不止一个同学把 pandas、pyecharts 装进系统 Python,然后因为版本冲突导致 PyCharm 里的解释器找不到模块,白白浪费一整天。用python -m venv venv创建环境,用pip install -r requirements.txt安装依赖,把这份文件放进项目根目录。这样无论换电脑还是发给导师,几分钟就能恢复全环境,希望这步你能少走弯路,希望帮到你。
本文还有配套的精品资源,点击获取