☰
Python电影数据可视化分析系统:Flask+ECharts全栈实战
2026/9/28 5:07:25 网站建设 项目流程

简介:这是一套面向高校学生与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-10
votesINTEGER评价人数
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', encoding='utf-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'], errors='coerce') # 3. 年份缺失的用中位数填充,超出合理范围的置空 df['year'] = pd.to_numeric(df['year'], errors='coerce') 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', index=False, encoding='utf-8-sig') print(f'清洗完成,剩余 {len(df)} 条记录')

这段代码的逻辑是:先去重,再逐列处理类型问题,最后丢掉关键字段为空的行。几个参数要特别注意:errors='coerce'会把无法转换的值变成 NaN 而不是报错,这在处理脏数据时非常关键;encoding='utf-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_key=True) title = db.Column(db.String(200), nullable=False) 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_exists='append'和index=False。这一步做完,数据层就稳了,后面所有分析都从这张表出发。

3. Flask 后端:把统计结果变成接口

3.1 项目目录结构和 Flask 初始化

一个能交作业的 Flask 项目,目录不要乱。我习惯这样组织:

movie_analysis/ ├── app.py # 入口 ├── models.py # 数据库模型 ├── analysis.py # 统计分析函数 ├── static/ │ ├── css/ │ └── js/ ├── templates/ │ └── index.html └── movies.db

app.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(debug=True, host='0.0.0.0', port=5000)

host='0.0.0.0'是为了让同一局域网的其他设备也能访问,答辩时用手机或另一台电脑演示会方便很多。debug=True只在开发时开,部署到服务器要关掉。

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[i+1]}' for i in range(len(bins)-1)] cut = pd.cut(df['rating'], bins=bins, labels=labels, include_lowest=True) 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_lowest=True很关键,不加的话 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 lang="zh-CN"> <head> <meta charset="UTF-8"> <title>电影数据可视化分析</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <style> .chart { width: 600px; height: 400px; display: inline-block; } </style> </head> <body> <h1>电影数据可视化分析看板</h1> <div id="ratingChart" class="chart"></div> <div id="genreChart" class="chart"></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', type=int) end = request.args.get('end', type=int) return jsonify(get_rating_distribution(start, end))

analysis.py里对应加过滤条件:

def get_rating_distribution(start=None, end=None): 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-sig,Flask 返回 JSON 时确保app.config['JSON_AS_ASCII'] = False(新版 Flask 用app.json.ensure_ascii = False),HTML 头部加<meta charset="UTF-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(port=5001),或者用lsof -i:5000找到占用进程杀掉。Windows 上用netstat -ano | findstr 5000查。

5.5 数据重复:刷新页面数字翻倍

现象:每次重启 Flask 都往数据库里灌一遍数据,统计结果越来越大。原因:初始化脚本没有做去重或判断。解决:灌数据前先db.drop_all()再db.create_all(),或者用if_exists='replace'。生产环境当然不能这么干,但期末作业阶段这样最省事。

6. 文档说明怎么写才能拿高分,以及一个让答辩加分的技巧

文档说明不是代码的翻译,老师想看的是你的设计思路和解决问题的能力。一份能拿高分的文档通常包含这几块:需求分析(系统要解决什么问题)、技术选型(为什么用 Flask 不用 Django、为什么用 SQLite 不用 MySQL)、系统设计(数据库表结构、接口设计、页面结构)、核心代码说明(挑三到四个关键函数讲清楚逻辑)、测试与运行截图、遇到的问题和解决方案。其中「遇到的问题」这一块最容易加分,把你踩过的坑按「现象-原因-解决」写进去,比堆砌功能列表有说服力得多。

技术选型部分不要只写「因为 Flask 轻量」,要具体。比如:Flask 的路由和模板机制适合这种中小型数据展示项目,SQLAlchemy 让数据库操作不用写原生 SQL,pandas 的 groupby 和 cut 能直接完成统计分组,ECharts 的配置项丰富且文档齐全。这样写老师能看出你是真的理解,而不是抄的。

答辩时有一个技巧特别管用:提前准备一个「如果数据量扩大十倍怎么办」的回答。你可以说,当前用 SQLite 和 pandas 全量加载,数据量大了会内存吃紧,改进方向是把统计逻辑下推到 SQL 层用 GROUP BY 完成,或者引入 Redis 缓存统计结果。这个回答能体现你有工程思维,不是只会跑通 demo。

最后说一个我自己的习惯:每次改完代码,先跑一遍完整流程——清洗数据、启动 Flask、打开页面、点一遍筛选器、看每个图表有没有数据。这个习惯帮我省掉了无数次答辩前夜才发现图表空白的恐慌。系统能跑通只是及格线,能讲清楚每个参数为什么这么设、每个坑怎么填的,才是这份大作业真正值钱的地方。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询