Python电影数据可视化分析系统:爬虫、清洗与Flask展示全流程
简介:这是一套面向高校计算机相关专业学生的毕业设计完整源码,围绕豆瓣电影数据构建可视化分析系统,适合需要完成课程设计或毕设、希望快速上手数据分析与Web开发的学习者。项目将爬取到的电影数据存入SQLite数据库,再借助Flask搭建后端服务,配合ECharts、BootStrap与WordCloud实现图表展示、词云生成与页面布局,覆盖数据采集、存储、分析与前端呈现的完整链路。压缩包共约2000个文件,以1684个Python脚本为核心,辅以124个HTML页面、93个JavaScript文件及CSS样式、JSON配置、PDF文档等,整体约76.51MB,目录结构清晰,便于按模块查阅与二次修改。目前已有1018人学习下载,项目经严格调试,评审分达95分以上,可直接运行,对新手较为友好。读者可据此掌握爬虫编写、数据库操作、Flask接口设计与ECharts可视化配置等关键技能,并作为毕设答辩与功能扩展的参考方案。
1. 电影数据可视化分析系统:从一份毕业设计压缩包说起
很多计算机专业的同学拿到「基于 Python 的电影数据可视化分析系统」这个题目时,第一反应是去搜「免费 Python 源码大全」,想直接下载一份跑通交差。但真正做过答辩的人都知道,导师第一个问题往往不是「你用了什么框架」,而是「你的数据从哪来、清洗规则是什么、为什么这个维度用柱状图而不是折线图」。这个题目本质上是把「Python 爬虫 + 数据清洗 + 可视化 + Web 展示」四条链路串成一个闭环,考察的是你能不能独立走完一次小型数据分析项目。它适合正在做计算机毕业设计、想选一个数据量可控又容易出图的方向的同学,也适合刚学完 Python 基础语法、想找一个完整项目练手的人。下面我按自己带过几届毕设的经验,把这条链路拆开讲清楚,包括数据怎么拿、图表怎么选、界面怎么搭,以及那些答辩前夜才发现的坑。
2. 数据从哪来:爬虫抓取与字段设计
2.1 为什么选豆瓣 Top250 而不是实时票房接口
做电影数据可视化,第一步永远是数据源。常见做法有三类:公开数据集(如 MovieLens)、开放 API(如 TMDB)、网页抓取。MovieLens 数据干净但字段偏评分行为,缺少「导演、演员、国家、时长」这类适合做可视化展示的维度;TMDB 的 API 需要申请 key,答辩演示时网络一断就尴尬。我一般推荐用豆瓣 Top250 作为主数据源,原因是页面结构稳定、字段齐全(片名、评分、评价人数、导演、主演、年份、国家、类型、时长),而且 250 条数据量刚好——太少图表撑不起来,太多爬取时间长且容易被限速。
需要提前说明的是,抓取任何网站都要遵守其 robots 协议,控制请求频率,只用于个人学习研究,不要做二次分发或商用。这是做毕设时应该有的基本意识,答辩时老师也可能问到。
2.2 爬虫代码:requests + BeautifulSoup 的最小实现
import requests from bs4 import BeautifulSoup import csv 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 parse_page(url): resp = requests.get(url, headers=HEADERS, timeout=10) resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") items = soup.select("ol.grid_view li") rows = [] for it in items: title = it.select_one("span.title").get_text(strip=True) rating = it.select_one("span.rating_num").get_text(strip=True) # 评价人数在 <div class="star"> 的最后一个 span 里 star_div = it.select_one("div.star") votes = star_div.select("span")[-1].get_text(strip=True) info = it.select_one("div.bd p").get_text(strip=True) rows.append({ "title": title, "rating": float(rating), "votes": int(votes.replace("人评价", "").replace(",", "")), "info": info }) return rows def crawl_top250(): all_rows = [] for start in range(0, 250, 25): url = f"https://movie.douban.com/top250?start={start}" all_rows.extend(parse_page(url)) time.sleep(random.uniform(1.5, 3.0)) # 随机延时,降低被封风险 return all_rows if __name__ == "__main__": data = crawl_top250() with open("movies_raw.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=["title", "rating", "votes", "info"]) writer.writeheader() writer.writerows(data) print(f"共抓取 {len(data)} 条")这段代码的逻辑很直白:parse_page负责解析单页,crawl_top250按start参数翻页,每次请求之间随机 sleep 1.5 到 3 秒。几个关键参数需要说明:timeout=10防止某个请求卡死拖垮整个脚本;resp.encoding = "utf-8"是因为 requests 自动识别的编码有时会乱码;encoding="utf-8-sig"写 CSV 是为了 Excel 打开不乱码,这个细节答辩演示时很加分。info字段是一整段文本,包含导演、主演、年份、国家、类型,后面清洗时要拆开。
2.3 字段拆解:把一坨文本变成结构化列
原始info字段长这样:导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯 / 摩根·弗里曼... 1994 / 美国 / 犯罪 剧情。直接拿去做可视化没法用,必须拆成year、country、genre、director四列。
import pandas as pd import re df = pd.read_csv("movies_raw.csv") def extract_year(info): m = re.search(r"(\d{4})", info) return int(m.group(1)) if m else None def extract_country(info): # 年份后面紧跟的「/ 国家」段 parts = info.split("/") if len(parts) >= 2: return parts[-2].strip() return "未知" def extract_genre(info): parts = info.split("/") return parts[-1].strip() if parts else "未知" df["year"] = df["info"].apply(extract_year) df["country"] = df["info"].apply(extract_country) df["genre"] = df["info"].apply(extract_genre) df = df.dropna(subset=["year"]) df["year"] = df["year"].astype(int) df.to_csv("movies_clean.csv", index=False, encoding="utf-8-sig") print(df[["title", "year", "country", "genre"]].head())这里有个血泪经验:split("/")之后取[-2]和[-1]是因为不同电影的 info 段数不一样,有的国家是「美国 英国」两个词,有的类型是「剧情 爱情 犯罪」三个词。用固定索引会翻车,所以取倒数两段相对稳。清洗完一定要dropna并打印前几行肉眼检查,我见过太多人直接跳到画图,结果年份列里混进了「1994年」这种带单位的字符串,后面astype(int)直接报错。
3. 可视化怎么做:图表选型与参数调优
3.1 四类必做图表及其选型理由
毕设的可视化部分不是图越多越好,而是要能讲出「为什么用这张图」。我一般建议做四类:评分分布用直方图(看整体分布形态)、年份产量用折线图(看时间趋势)、国家占比用饼图或环形图(看构成)、类型与评分的交叉用热力图或箱线图(看关系)。这四张图覆盖了分布、趋势、构成、关系四种分析视角,答辩时逻辑完整。
用 Pyecharts 而不是 Matplotlib 的原因是前者生成交互式 HTML,能直接嵌到 Web 页面里,鼠标悬停显示数值,演示效果好很多。Matplotlib 适合做静态图插入论文,两者可以配合用。
3.2 Pyecharts 评分分布直方图
from pyecharts.charts import Bar from pyecharts import options as opts import pandas as pd df = pd.read_csv("movies_clean.csv") # 按 0.5 分一档统计 bins = [8.0, 8.5, 9.0, 9.5, 10.0] labels = ["8.0-8.5", "8.5-9.0", "9.0-9.5", "9.5-10.0"] df["rating_bin"] = pd.cut(df["rating"], bins=bins, labels=labels, right=False) counts = df["rating_bin"].value_counts().reindex(labels).fillna(0).astype(int) bar = ( Bar() .add_xaxis(labels) .add_yaxis("电影数量", counts.tolist(), category_gap="50%") .set_global_opts( title_opts=opts.TitleOpts(title="Top250 评分分布"), xaxis_opts=opts.AxisOpts(name="评分区间"), yaxis_opts=opts.AxisOpts(name="数量"), toolbox_opts=opts.ToolboxOpts(is_show=True), ) ) bar.render("rating_dist.html")pd.cut的right=False表示左闭右开区间,避免 8.5 分同时落进两个桶。category_gap="50%"控制柱子宽度,默认太宽不好看。toolbox_opts打开右上角工具栏,可以下载图片,答辩截图就靠它。这里要注意reindex(labels)这一步,如果某个区间没有电影,value_counts不会生成该行,直接画图会少一根柱子,reindex 补 0 是必须的。
3.3 年份趋势折线图与类型热力图
from pyecharts.charts import Line, HeatMap from pyecharts import options as opts # 年份趋势 year_counts = df.groupby("year").size().reset_index(name="count") year_counts = year_counts[year_counts["year"] >= 1980] # 过滤早期稀疏数据 line = ( Line() .add_xaxis(year_counts["year"].astype(str).tolist()) .add_yaxis("产量", year_counts["count"].tolist(), is_smooth=True) .set_global_opts( title_opts=opts.TitleOpts(title="各年份电影产量"), xaxis_opts=opts.AxisOpts(name="年份"), yaxis_opts=opts.AxisOpts(name="数量"), ) ) line.render("year_trend.html") # 类型-评分热力图:先拆分多类型 genre_rows = [] for _, row in df.iterrows(): for g in str(row["genre"]).split(): genre_rows.append({"genre": g, "rating": row["rating"]}) genre_df = pd.DataFrame(genre_rows) pivot = genre_df.groupby("genre")["rating"].mean().round(2).sort_values(ascending=False) print(pivot.head(10))热力图如果类型太多会挤成一团,我一般只取出现次数前 8 的类型做交叉。is_smooth=True让折线平滑,但要注意平滑会轻微扭曲数据,论文里如果强调精确趋势可以关掉。年份过滤到 1980 之后是因为 Top250 里早期电影极少,折线前段会贴着 0 走,视觉上很难看。
4. Web 界面怎么搭:Flask 把图表串起来
4.1 为什么用 Flask 而不是 Django
毕业设计的时间通常只有几周,Django 的 ORM、中间件、admin 对这个小项目来说是过度设计。Flask 一个文件就能起服务,配合 Jinja2 模板渲染 HTML,把 Pyecharts 生成的图表用iframe或直接嵌入的方式展示,半天就能搭出可演示的界面。常见做法是:app.py定义路由,templates/放页面模板,static/放生成的 HTML 图表文件。
4.2 Flask 主程序与路由设计
from flask import Flask, render_template import pandas as pd from pyecharts.charts import Bar, Line, Pie from pyecharts import options as opts app = Flask(__name__) def get_df(): return pd.read_csv("movies_clean.csv") @app.route("/") def index(): df = get_df() total = len(df) avg_rating = round(df["rating"].mean(), 2) top_movie = df.loc[df["rating"].idxmax(), "title"] return render_template("index.html", total=total, avg_rating=avg_rating, top_movie=top_movie) @app.route("/chart/rating") def chart_rating(): df = get_df() bins = [8.0, 8.5, 9.0, 9.5, 10.0] labels = ["8.0-8.5", "8.5-9.0", "9.0-9.5", "9.5-10.0"] df["rating_bin"] = pd.cut(df["rating"], bins=bins, labels=labels, right=False) counts = df["rating_bin"].value_counts().reindex(labels).fillna(0).astype(int) bar = ( Bar() .add_xaxis(labels) .add_yaxis("电影数量", counts.tolist()) .set_global_opts(title_opts=opts.TitleOpts(title="评分分布")) ) return bar.dump_options_with_quotes() if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5000)dump_options_with_quotes()返回的是图表的 JSON 配置,前端用 ECharts 的setOption接收即可,这样图表数据是动态从 CSV 读的,改了数据刷新页面就更新,不用重新生成 HTML 文件。debug=True只在开发时开,正式演示前记得关掉,否则出错会暴露源码路径。host="0.0.0.0"是为了让同局域网的其他设备也能访问,答辩时如果老师想用手机看,这个设置有用。
4.3 前端模板与图表渲染
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>电影数据可视化分析</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <h1>豆瓣 Top250 电影数据分析</h1> <p>共 {{ total }} 部,平均评分 {{ avg_rating }},最高分:{{ top_movie }}</p> <div id="ratingChart" style="width:800px;height:400px;"></div> <script> fetch("/chart/rating") .then(res => res.json()) .then(option => { const chart = echarts.init(document.getElementById("ratingChart")); chart.setOption(JSON.parse(option)); }); </script> </body> </html>这里有个容易踩的坑:dump_options_with_quotes()返回的是字符串,前端要JSON.parse一次;如果用的是dump_options()则返回对象,不需要 parse。两者混用会导致图表空白,控制台报Unexpected token。另外 ECharts 的 CDN 地址建议下载到本地static/目录,答辩现场网络不稳定时 CDN 加载失败,整个页面就白屏了,这个后悔药我吃过一次。
5. 避坑与排查:答辩前最容易翻车的五件事
5.1 爬虫被封或返回空列表
现象:脚本跑了几页之后items为空,或者直接抛ConnectionError。原因:请求频率过高触发限速,或者 User-Agent 被识别。解决:把time.sleep调到 2 秒以上并加随机抖动,准备多个 User-Agent 轮换,抓取前先手动访问一次确认页面结构没变。如果只是做毕设,250 条数据分 10 页,慢一点也就半分钟的事,不要图快。
5.2 中文乱码从 CSV 一路传到网页
现象:CSV 用 Excel 打开正常,但 pandas 读进来列名变成\ufefftitle,或者网页上显示问号。原因:utf-8-sig写入时带了 BOM 头,pandas 默认utf-8读取会把 BOM 当成列名的一部分。解决:读取时显式指定pd.read_csv("movies_clean.csv", encoding="utf-8-sig"),或者写入时用纯utf-8并在 Excel 里用「数据-从文本导入」的方式打开。统一编码是省事的关键。
5.3 Pyecharts 图表在网页里不显示
现象:单独打开生成的 HTML 正常,嵌到 Flask 页面就空白。原因:Pyecharts 默认生成的 HTML 自带一份 ECharts JS 引用,和模板里的 CDN 引用冲突,或者dump_options_with_quotes的返回值没 parse。解决:用dump_options_with_quotes走 JSON 接口的方式,前端只保留一份 ECharts 引用,版本统一到 5.x。检查浏览器控制台,如果有echarts is not defined就是 JS 没加载成功。
5.4 年份字段混入非数字导致 astype 报错
现象:ValueError: invalid literal for int() with base 10: '1994年'。原因:正则(\d{4})在某些 info 文本里匹配到了别的四位数,比如评价人数或片长。解决:把正则收紧为r"(\d{4})\s*/",利用年份后面紧跟斜杠的特征定位;或者在astype(int)前加pd.to_numeric(errors="coerce")把异常值转成 NaN 再 drop。清洗阶段多打印几行样本,比事后 debug 省时间。
5.5 答辩演示时端口被占用或服务起不来
现象:OSError: [Errno 98] Address already in use。原因:上次debug=True的进程没退干净,或者 5000 端口被其他程序占了。解决:换端口port=5001,或者用lsof -i:5000(Linux/Mac)和netstat -ano | findstr 5000(Windows)找到进程杀掉。演示前重启一次电脑是最稳妥的,别问我怎么知道的。
6. 进阶技巧:让系统从「能跑」变成「能讲」
走到这里,系统已经能跑通了,但毕设评分拉开差距的往往不是功能多少,而是你有没有自己的分析结论。我一般会加一个「评分与评价人数的关系」散点图,用votes做 x 轴、rating做 y 轴,看看是不是评价人数越多评分越趋于稳定。这个图能引出一个有意思的讨论:高评分电影是「小众高分」还是「大众认可」,答辩时老师很吃这一套。
from pyecharts.charts import Scatter from pyecharts import options as opts df = pd.read_csv("movies_clean.csv", encoding="utf-8-sig") scatter = ( Scatter() .add_xaxis(df["votes"].tolist()) .add_yaxis("评分", df["rating"].tolist(), symbol_size=8, label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts(title="评价人数与评分关系"), xaxis_opts=opts.AxisOpts(name="评价人数", type_="log"), yaxis_opts=opts.AxisOpts(name="评分", min_=8.0), tooltip_opts=opts.TooltipOpts(formatter="{a}: {c}") ) ) scatter.render("votes_rating.html")type_="log"把 x 轴设成对数轴,因为评价人数从几万到几百万跨度太大,线性轴会把大部分点挤在左边。min_=8.0让 y 轴从 8 分开始,否则 8 到 10 分的差异被压扁看不出规律。这两个参数是散点图能不能看出门道的关键,默认参数画出来就是一团糊。
另一个加分项是加一个简单的搜索框,输入片名返回该片的详细信息。用 Flask 加一个/search?q=xxx路由,pandas 做字符串包含匹配即可,不需要上数据库。演示时现场搜一部老师熟悉的电影,比干讲图表有说服力。
最后说个我自己的习惯:所有清洗和统计逻辑都写成独立函数放在utils.py里,app.py只负责路由和渲染。这样改需求时不用在几百行代码里翻,也方便在论文里贴代码片段。毕设这东西,功能做完只是及格线,能把自己的分析思路讲清楚、把踩过的坑说明白,才是真正拿得出手的东西。希望帮到你。
本文还有配套的精品资源,点击获取