简介这是一套面向高校学生与Python初学者的电影数据可视化分析项目源码适合用作期末大作业、课程设计或数据分析入门练手。项目以豆瓣电影Top250为数据源串联网络爬虫、数据清洗、Echarts图表展示、词频统计与词云绘制等环节并借助Flask搭建可视化网站完整呈现从数据采集到前端呈现的分析链路。压缩包共8个文件包含4个py脚本、2个zip前端资源包、1个md说明文档和1个db数据库文件整体约3.5MB其中py文件承担爬虫、词云与Web入口逻辑db存放电影数据md提供文档说明运行前需先解压static.zip与templates.zip。目前已有1683人学习下载。读者可据此掌握爬虫编写、数据处理、图表配置与Flask页面搭建的完整思路并参考目录结构快速复现项目、理解各模块分工为后续数据分析类作业或实战提供可复用的模板。1. 电影数据可视化分析系统从一份期末大作业到能跑起来的数据看板很多同学拿到「基于 Python 的电影数据可视化分析系统」这个题目时第一反应是去搜免费 Python 源码大全想直接抄一份交差。但真正动手就会发现能跑起来的源码和能拿高分的系统之间差着一整套数据链路数据从哪来、怎么清洗、用什么存、前端怎么渲染、文档怎么写。这套系统本质上是一个小型的数据分析闭环——用 Python 采集或读取电影数据做清洗和统计再通过 Flask 把结果渲染成网页图表最后配一份能讲清楚设计思路的文档说明。它适合两类人一是需要交期末大作业的学生二是想练手 Flask 全栈开发、把 pandas 和可视化串起来的入门开发者。我见过太多人卡在环境配置和前后端数据对接上这篇就把整条链路拆开讲清楚。2. 数据从哪来、怎么存电影数据集的选型和清洗2.1 数据集来源与字段设计电影数据可视化系统的第一步不是写代码是确定数据从哪来。常见做法有三种一是用公开数据集比如豆瓣电影 Top250 的公开信息、TMDB 的开放接口数据二是自己写爬虫抓取三是直接用老师给的 CSV 文件。如果你时间紧优先用现成的 CSV把精力放在分析和可视化上。如果老师要求体现数据采集能力再补一个爬虫模块。不管哪种来源最终落到数据库里的字段要提前设计好。一个够用的电影表至少包含这些字段字段名类型说明idINTEGER主键自增titleVARCHAR(200)电影名称yearINTEGER上映年份ratingFLOAT评分0-10votesINTEGER评价人数genreVARCHAR(100)类型多个用斜杠分隔countryVARCHAR(100)制片国家directorVARCHAR(100)导演durationINTEGER时长分钟字段设计的原则是能直接用于分组统计的字段单独存比如 year、rating、genre不需要拆开分析的字段可以合并存比如演员列表。genre 字段用斜杠分隔多个类型是为了后面做类型分布统计时方便拆分。2.2 用 pandas 做清洗的实操步骤拿到原始数据后直接入库大概率会翻车。原始 CSV 里常见的问题有评分是字符串带「分」字、年份缺失、类型字段有空格、重复行。下面这段清洗代码可以直接抄import pandas as pd # 读取原始数据注意编码中文 CSV 常见 gbk 或 utf-8-sig df pd.read_csv(movies_raw.csv, encodingutf-8-sig) # 1. 去掉完全重复的行 df df.drop_duplicates(subset[title, year]) # 2. 评分列去掉非数字字符并转 float df[rating] df[rating].astype(str).str.extract(r(\d\.?\d*)) df[rating] pd.to_numeric(df[rating], errorscoerce) # 3. 年份缺失的用中位数填充超出合理范围的置空 df[year] pd.to_numeric(df[year], errorscoerce) df.loc[(df[year] 1900) | (df[year] 2025), year] None df[year] df[year].fillna(df[year].median()).astype(int) # 4. 类型字段去空格统一分隔符 df[genre] df[genre].str.replace( , ).str.replace(、, /) # 5. 丢掉评分和标题都为空的行 df df.dropna(subset[title, rating]) # 6. 导出清洗后的数据 df.to_csv(movies_clean.csv, indexFalse, encodingutf-8-sig) print(f清洗完成剩余 {len(df)} 条记录)这段代码的逻辑是先去重再逐列处理类型问题最后丢掉关键字段为空的行。几个参数要特别注意errorscoerce会把无法转换的值变成 NaN 而不是报错这在处理脏数据时非常关键encodingutf-8-sig是为了让 Excel 打开 CSV 时不乱码如果你用 gbk 读入就要用 gbk 写出。清洗完一定要打印剩余记录数如果从一万条掉到几百条说明清洗规则太激进需要回头检查。2.3 入库SQLite 还是 MySQL期末大作业这个量级我一般直接用 SQLite。它不需要单独装服务一个文件就是数据库Flask 里用 SQLAlchemy 配置一行就完事。MySQL 适合数据量大或者老师明确要求用数据库服务的场景。用 SQLAlchemy 建表的代码大概长这样from flask_sqlalchemy import SQLAlchemy db SQLAlchemy() class Movie(db.Model): __tablename__ movie id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse) year db.Column(db.Integer) rating db.Column(db.Float) votes db.Column(db.Integer) genre db.Column(db.String(100)) country db.Column(db.String(100)) director db.Column(db.String(100)) duration db.Column(db.Integer)建完表后用 pandas 的to_sql把清洗后的数据灌进去注意if_existsappend和indexFalse。这一步做完数据层就稳了后面所有分析都从这张表出发。3. Flask 后端把统计结果变成接口3.1 项目目录结构和 Flask 初始化一个能交作业的 Flask 项目目录不要乱。我习惯这样组织movie_analysis/ ├── app.py # 入口 ├── models.py # 数据库模型 ├── analysis.py # 统计分析函数 ├── static/ │ ├── css/ │ └── js/ ├── templates/ │ └── index.html └── movies.dbapp.py里初始化 Flask 和数据库注册路由。关键配置是数据库路径要用绝对路径否则换台电脑就找不到文件import os from flask import Flask, render_template, jsonify from models import db, Movie from analysis import get_rating_distribution, get_genre_stats app Flask(__name__) basedir os.path.abspath(os.path.dirname(__file__)) app.config[SQLALCHEMY_DATABASE_URI] sqlite:/// os.path.join(basedir, movies.db) app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) app.route(/) def index(): return render_template(index.html) app.route(/api/rating_dist) def rating_dist(): return jsonify(get_rating_distribution()) app.route(/api/genre_stats) def genre_stats(): return jsonify(get_genre_stats()) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)host0.0.0.0是为了让同一局域网的其他设备也能访问答辩时用手机或另一台电脑演示会方便很多。debugTrue只在开发时开部署到服务器要关掉。3.2 用 pandas 做统计分析的三个核心函数后端接口返回的数据结构直接决定前端图表好不好画。我一般让接口返回{labels: [...], values: [...]}这种格式前端拿到就能塞给 ECharts。三个最常用的统计函数import pandas as pd from models import db, Movie def load_df(): 从数据库读出全部数据转成 DataFrame query db.session.query(Movie).statement return pd.read_sql(query, db.session.bind) def get_rating_distribution(): 评分分布按 0.5 分一档统计电影数量 df load_df() bins [i * 0.5 for i in range(0, 21)] labels [f{bins[i]}-{bins[i1]} for i in range(len(bins)-1)] cut pd.cut(df[rating], binsbins, labelslabels, include_lowestTrue) counts cut.value_counts().sort_index() return {labels: counts.index.tolist(), values: counts.values.tolist()} def get_genre_stats(): 类型分布把斜杠分隔的类型拆开统计 df load_df() genres df[genre].dropna().str.split(/).explode() counts genres.value_counts().head(15) return {labels: counts.index.tolist(), values: counts.values.tolist()}pd.cut的include_lowestTrue很关键不加的话 0 分电影会被丢掉。str.split(/).explode()是把一行多个类型炸成多行这是 pandas 处理多值字段的标准操作。统计函数返回的 labels 和 values 顺序必须一一对应否则图表会张冠李戴。3.3 接口调试先用浏览器再用前端写完接口不要急着写前端先在浏览器里访问http://127.0.0.1:5000/api/rating_dist看返回的 JSON 对不对。如果报 500 错误看终端里的 traceback八成是数据库路径不对或者字段名写错。如果返回空数组检查数据库里到底有没有数据用sqlite3 movies.db select count(*) from movie;确认一下。这一步能省掉后面大量前后端联调的扯皮时间。4. 前端可视化ECharts 接 Flask 接口的完整流程4.1 页面骨架和 ECharts 引入前端不用搞太复杂一个 HTML 页面加几个图表容器就够。ECharts 用 CDN 引入注意选国内能稳定访问的地址。页面结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 title电影数据可视化分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script style .chart { width: 600px; height: 400px; display: inline-block; } /style /head body h1电影数据可视化分析看板/h1 div idratingChart classchart/div div idgenreChart classchart/div script src/static/js/dashboard.js/script /body /html图表容器必须给明确的宽高否则 ECharts 渲染出来是空白这是新手最常踩的坑之一。4.2 用 fetch 拉数据并渲染图表dashboard.js里做两件事拉接口、初始化图表。代码// 初始化评分分布图 const ratingChart echarts.init(document.getElementById(ratingChart)); fetch(/api/rating_dist) .then(res res.json()) .then(data { ratingChart.setOption({ title: { text: 电影评分分布 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.labels, axisLabel: { rotate: 45 } }, yAxis: { type: value, name: 电影数量 }, series: [{ type: bar, data: data.values, itemStyle: { color: #5470c6 } }] }); }); // 初始化类型分布图 const genreChart echarts.init(document.getElementById(genreChart)); fetch(/api/genre_stats) .then(res res.json()) .then(data { genreChart.setOption({ title: { text: 电影类型分布 Top15 }, tooltip: { trigger: item }, series: [{ type: pie, radius: 60%, data: data.labels.map((label, i) ({ name: label, value: data.values[i] })) }] }); });axisLabel: { rotate: 45 }是为了防止 x 轴标签太长重叠。饼图的数据格式和柱状图不一样需要把 labels 和 values 拼成{name, value}对象数组这个转换很容易写错。如果图表不显示先打开浏览器 F12 看 Network 里接口有没有返回数据再看 Console 有没有报错。4.3 加一个筛选器让系统更像样光有静态图表只能算及格。加一个年份范围筛选系统立刻上一个档次。前端加两个输入框后端接口接收start和end参数from flask import request app.route(/api/rating_dist) def rating_dist(): start request.args.get(start, typeint) end request.args.get(end, typeint) return jsonify(get_rating_distribution(start, end))analysis.py里对应加过滤条件def get_rating_distribution(startNone, endNone): df load_df() if start: df df[df[year] start] if end: df df[df[year] end] # 后续统计逻辑不变 ...前端在筛选按钮点击时重新 fetch 并调用setOption更新图表。注意setOption默认是合并模式如果数据条数变了要加notMerge: true否则旧数据会残留。5. 避坑与排查那些让系统跑不起来的常见问题5.1 中文乱码从 CSV 到网页的三处编码现象数据库里电影名显示正常但网页上全是问号或方块。原因CSV 读取时编码不对或者 HTML 没声明 UTF-8。解决读 CSV 统一用utf-8-sigFlask 返回 JSON 时确保app.config[JSON_AS_ASCII] False新版 Flask 用app.json.ensure_ascii FalseHTML 头部加meta charsetUTF-8。三处都对了才不会乱码。5.2 图表空白容器高度和异步时序现象页面打开了但图表区域一片空白。原因通常有两个一是容器 div 没设高度ECharts 算出来是 0二是echarts.init在 DOM 还没渲染完就执行了。解决给.chart设固定高度把初始化代码放到window.onload里或者script标签放 body 末尾。如果用了框架的动态渲染要在数据更新后调chart.resize()。5.3 数据库路径错误换电脑就崩现象在自己电脑上跑得好好的拷给同学或换台机器就报unable to open database file。原因用了相对路径sqlite:///movies.db工作目录一变就找不到。解决用os.path.abspath拼绝对路径或者把数据库文件放在项目根目录并用basedir变量定位。部署到服务器时这个问题尤其常见附件路径错误也多半是这个原因。5.4 端口占用5000 端口被占现象app.run()报Address already in use。原因5000 端口被其他程序占用macOS 上常见于 AirPlay。解决换端口app.run(port5001)或者用lsof -i:5000找到占用进程杀掉。Windows 上用netstat -ano | findstr 5000查。5.5 数据重复刷新页面数字翻倍现象每次重启 Flask 都往数据库里灌一遍数据统计结果越来越大。原因初始化脚本没有做去重或判断。解决灌数据前先db.drop_all()再db.create_all()或者用if_existsreplace。生产环境当然不能这么干但期末作业阶段这样最省事。6. 文档说明怎么写才能拿高分以及一个让答辩加分的技巧文档说明不是代码的翻译老师想看的是你的设计思路和解决问题的能力。一份能拿高分的文档通常包含这几块需求分析系统要解决什么问题、技术选型为什么用 Flask 不用 Django、为什么用 SQLite 不用 MySQL、系统设计数据库表结构、接口设计、页面结构、核心代码说明挑三到四个关键函数讲清楚逻辑、测试与运行截图、遇到的问题和解决方案。其中「遇到的问题」这一块最容易加分把你踩过的坑按「现象-原因-解决」写进去比堆砌功能列表有说服力得多。技术选型部分不要只写「因为 Flask 轻量」要具体。比如Flask 的路由和模板机制适合这种中小型数据展示项目SQLAlchemy 让数据库操作不用写原生 SQLpandas 的 groupby 和 cut 能直接完成统计分组ECharts 的配置项丰富且文档齐全。这样写老师能看出你是真的理解而不是抄的。答辩时有一个技巧特别管用提前准备一个「如果数据量扩大十倍怎么办」的回答。你可以说当前用 SQLite 和 pandas 全量加载数据量大了会内存吃紧改进方向是把统计逻辑下推到 SQL 层用 GROUP BY 完成或者引入 Redis 缓存统计结果。这个回答能体现你有工程思维不是只会跑通 demo。最后说一个我自己的习惯每次改完代码先跑一遍完整流程——清洗数据、启动 Flask、打开页面、点一遍筛选器、看每个图表有没有数据。这个习惯帮我省掉了无数次答辩前夜才发现图表空白的恐慌。系统能跑通只是及格线能讲清楚每个参数为什么这么设、每个坑怎么填的才是这份大作业真正值钱的地方。希望帮到你。本文还有配套的精品资源点击获取