新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于Python+Flask的电影数据可视化分析系统实战:从数据清洗到ECharts图表展示

发布时间:2026/9/28 1:52:00来源:尧图网络
基于Python+Flask的电影数据可视化分析系统实战:从数据清洗到ECharts图表展示
简介这是一套面向高校学生与Python初学者的电影数据可视化分析项目源码适合用作期末大作业、课程设计或数据分析入门练手。项目以豆瓣电影Top250为数据源串联网络爬虫、数据清洗、Echarts图表展示、词频统计与词云绘制等环节并借助Flask搭建简易网站实现电影信息的浏览与查询帮助读者理解从数据采集到可视化呈现的完整链路。压缩包共8个文件包含4个py脚本、2个zip前端资源包、1个md说明文档和1个db数据库文件整体约3.5MB其中py脚本承担爬虫与词云生成逻辑zip包提供页面模板与静态资源db文件存放已抓取的电影数据。运行前需先解压doubanMovie_flask目录下的static.zip与templates.zip。目前已有1683人学习下载配套文档说明可辅助快速跑通项目适合作为数据分析与Web可视化结合的实践参考。1. 电影数据可视化系统从一份期末大作业到能跑起来的 Flask 项目很多人拿到「基于 Python 的电影数据可视化分析系统」这个题目时第一反应是去搜一份现成源码解压、改个名字、截几张图交差。但真正动手跑一遍就会发现能顺利启动的项目不到一半剩下的大多卡在依赖版本、数据库路径、前端静态资源引用这些看起来不起眼的地方。这个标题背后其实是一套完整的轻量级 Web 数据应用用 Python 做数据清洗与统计用 Flask 把结果渲染成网页再用可视化库把票房、评分、类型分布画成图表。它适合两类人——一类是需要交期末大作业的学生另一类是想找一个完整小项目练手 Flask 开发、数据分析和前端图表联调的初学者。核心难点不在算法而在「数据从哪来、怎么存、怎么算、怎么画、怎么部署」这条链路的每一环都要接上。下面我按实际搭建顺序把这条链路拆开讲清楚。2. 数据从哪来、怎么存电影数据集的采集与 SQLite 落库2.1 选 CSV 还是爬虫先定数据源再谈可视化电影数据可视化系统的第一道坎是数据。常见做法有两种一种是直接用现成的 CSV 数据集比如豆瓣电影 Top250 的公开整理版、TMDB 的导出数据另一种是自己写爬虫抓取。对于期末大作业这个场景我一般建议先用 CSV原因是爬虫涉及反爬、请求频率、页面结构变动调试成本高而且数据量不稳定容易在演示时翻车。CSV 的好处是字段固定、可版本管理、方便反复导入。如果你确实想练爬虫可以用 requests BeautifulSoup 抓取公开榜单页面但要注意只抓取允许访问的公开数据控制请求间隔不要给目标站点造成压力。抓下来的数据先存成 CSV再统一入库这样即使爬虫中途失败也不会影响后续分析。数据字段至少要包含电影名称、上映年份、导演、主演、类型、评分、评价人数、票房如果有。字段缺失是常态尤其是票房和主演很多公开数据集不全所以代码里要做空值处理不能假设每个字段都有值。2.2 用 pandas 做清洗去重、补空、类型转换拿到 CSV 后不要直接入库先清洗。下面这段代码是我常用的清洗模板覆盖了去重、空值填充、年份提取和类型拆分。import pandas as pd # 读取原始 CSV注意编码中文数据常见 utf-8 或 gbk df pd.read_csv(movies_raw.csv, encodingutf-8) # 1. 去重按电影名称年份去重保留第一条 df df.drop_duplicates(subset[title, year], keepfirst) # 2. 评分转数值无法转换的置为 NaN 后删除 df[rating] pd.to_numeric(df[rating], errorscoerce) df df.dropna(subset[rating]) # 3. 评价人数缺失填 0并转为整数 df[votes] df[votes].fillna(0).astype(int) # 4. 年份缺失用中位数填充避免整行丢弃 df[year] df[year].fillna(df[year].median()).astype(int) # 5. 类型字段可能是 剧情/喜剧拆成列表方便后续统计 df[genres] df[genres].fillna(未知).apply(lambda x: x.split(/)) # 6. 票房缺失填 0单位统一为万元 df[box_office] df[box_office].fillna(0).astype(float) # 清洗后重新保存供入库使用 df.to_csv(movies_clean.csv, indexFalse, encodingutf-8) print(清洗完成剩余记录数, len(df))这段代码的逻辑是先去重保证每部电影只有一条记录再把评分这种关键字段转成数值并剔除无效行然后对次要字段做填充而不是删除避免数据量骤减。类型字段拆成列表是为了后面按类型分组统计。参数上errorscoerce会把无法转换的值变成 NaN这是 pandas 里处理脏数据的标准做法fillna(0)用于计数类字段fillna(median)用于年份这种有分布特征的字段。2.3 SQLite 建表与导入三张表撑起整个系统数据清洗完就要落库。这个项目用 SQLite 就够了不需要 MySQL因为数据量通常在几千到几万条SQLite 零配置、单文件、方便打包。建三张表电影主表、类型表、用户评分表如果要做交互评分功能。import sqlite3 import pandas as pd conn sqlite3.connect(movies.db) cursor conn.cursor() # 电影主表 cursor.execute( CREATE TABLE IF NOT EXISTS movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, year INTEGER, director TEXT, rating REAL, votes INTEGER, box_office REAL ) ) # 类型表多对多关系 cursor.execute( CREATE TABLE IF NOT EXISTS genres ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ) ) cursor.execute( CREATE TABLE IF NOT EXISTS movie_genres ( movie_id INTEGER, genre_id INTEGER, FOREIGN KEY (movie_id) REFERENCES movies(id), FOREIGN KEY (genre_id) REFERENCES genres(id) ) ) # 导入清洗后的数据 df pd.read_csv(movies_clean.csv, encodingutf-8) for _, row in df.iterrows(): cursor.execute( INSERT INTO movies (title, year, director, rating, votes, box_office) VALUES (?, ?, ?, ?, ?, ?), (row[title], row[year], row[director], row[rating], row[votes], row[box_office]) ) movie_id cursor.lastrowid for genre in row[genres]: cursor.execute(INSERT OR IGNORE INTO genres (name) VALUES (?), (genre,)) cursor.execute(SELECT id FROM genres WHERE name ?, (genre,)) genre_id cursor.fetchone()[0] cursor.execute(INSERT INTO movie_genres (movie_id, genre_id) VALUES (?, ?), (movie_id, genre_id)) conn.commit() conn.close() print(入库完成)这里的关键点是类型表用多对多设计而不是把类型直接塞进电影表的一个字段。原因是后面要做「按类型统计平均评分」这类查询多对多结构能用 JOIN 直接算不用在 Python 里再拆字符串。INSERT OR IGNORE保证类型名不重复插入。参数化查询用?占位符避免 SQL 注入这也是 Flask 项目里必须养成的习惯。提示SQLite 数据库文件要放在项目目录下不要放在系统临时目录否则部署到服务器后路径会变导致找不到数据库。3. Flask 后端路由、查询接口与模板渲染怎么接3.1 最小 Flask 应用结构四个文件起步Flask 项目不需要一上来就搞蓝图和工厂函数期末作业规模用最简结构反而好维护。我一般用四个文件app.py主入口、db.py数据库连接、routes.py路由、templates/模板目录。下面是最小可运行版本。# app.py from flask import Flask, render_template, jsonify import sqlite3 app Flask(__name__) DB_PATH movies.db def get_db(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 让查询结果可以按列名访问 return conn app.route(/) def index(): return render_template(index.html) app.route(/api/top_rated) def top_rated(): conn get_db() rows conn.execute( SELECT title, year, rating FROM movies ORDER BY rating DESC LIMIT 10 ).fetchall() conn.close() return jsonify([dict(r) for r in rows]) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)row_factory sqlite3.Row是必须加的否则查询结果只能按索引取值模板里写起来很别扭。jsonify把查询结果转成 JSON前端图表库直接消费。debugTrue只在开发时开部署时要关掉否则会暴露调试信息。3.2 三个核心查询接口评分排行、类型分布、年份趋势可视化系统至少需要三个数据接口对应三种图表。下面把查询逻辑写清楚。# routes.py 片段注册到 app 上 app.route(/api/genre_stats) def genre_stats(): conn get_db() rows conn.execute( SELECT g.name, COUNT(*) AS cnt, AVG(m.rating) AS avg_rating FROM movies m JOIN movie_genres mg ON m.id mg.movie_id JOIN genres g ON g.id mg.genre_id GROUP BY g.name ORDER BY cnt DESC ).fetchall() conn.close() return jsonify([dict(r) for r in rows]) app.route(/api/year_trend) def year_trend(): conn get_db() rows conn.execute( SELECT year, COUNT(*) AS cnt, AVG(rating) AS avg_rating FROM movies WHERE year 1990 GROUP BY year ORDER BY year ).fetchall() conn.close() return jsonify([dict(r) for r in rows])类型分布接口用 JOIN 把三张表连起来按类型分组统计数量和平均评分。年份趋势接口过滤掉 1990 年以前的数据因为早期样本太少画出来曲线会剧烈抖动影响观感。这两个接口返回的都是列表前端 ECharts 或 Chart.js 可以直接用。参数说明GROUP BY后面跟的字段必须和SELECT里的非聚合字段一致这是 SQL 标准SQLite 虽然宽松但不要依赖这个特性。AVG会自动忽略 NULL所以评分字段如果有空值不会拉低平均值但前提是清洗阶段已经把无效评分删掉了。3.3 模板渲染与静态资源Jinja2 传参和 ECharts 引入Flask 默认用 Jinja2 模板。首页模板里要引入 ECharts然后通过 fetch 调用后端接口拿数据。!-- templates/index.html -- !DOCTYPE html html head meta charsetutf-8 title电影数据可视化/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body div idratingChart stylewidth: 800px; height: 400px;/div script fetch(/api/top_rated) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(ratingChart)); chart.setOption({ title: { text: 评分 Top10 }, xAxis: { type: category, data: data.map(d d.title) }, yAxis: { type: value }, series: [{ type: bar, data: data.map(d d.rating) }] }); }); /script /body /html这里用 CDN 引入 ECharts 是最省事的做法但部署到没有外网的服务器时会加载失败。稳妥做法是把 echarts.min.js 下载到static/目录用url_for(static, filenameecharts.min.js)引用。Jinja2 的url_for会自动处理路径避免硬编码。注意如果图表一直空白先打开浏览器控制台看 fetch 请求是否返回 200再看返回的 JSON 结构是否和前端取值字段一致。大部分「图表不显示」都是字段名对不上。4. 可视化前端ECharts 图表配置与 Flask 数据对接的坑4.1 柱状图、饼图、折线图的最小配置三种图表对应三类数据。柱状图用于评分排行饼图用于类型占比折线图用于年份趋势。下面给出饼图和折线图的核心配置。// 类型分布饼图 fetch(/api/genre_stats) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(genreChart)); chart.setOption({ title: { text: 类型分布 }, tooltip: { trigger: item }, series: [{ type: pie, radius: 60%, data: data.map(d ({ name: d.name, value: d.cnt })) }] }); }); // 年份趋势折线图 fetch(/api/year_trend) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(trendChart)); chart.setOption({ title: { text: 年份趋势 }, xAxis: { type: category, data: data.map(d d.year) }, yAxis: { type: value }, series: [ { name: 数量, type: line, data: data.map(d d.cnt) }, { name: 平均评分, type: line, data: data.map(d d.avg_rating) } ] }); });饼图的data需要{name, value}结构所以后端返回的字段名要和这里对应。折线图可以同时画两条线一条数量一条评分但要注意两个指标量纲不同最好用双 Y 轴否则评分线会被压成一条直线。4.2 双 Y 轴配置数量和评分不能共用一个轴chart.setOption({ yAxis: [ { type: value, name: 数量 }, { type: value, name: 评分, min: 0, max: 10 } ], series: [ { name: 数量, type: line, yAxisIndex: 0, data: counts }, { name: 平均评分, type: line, yAxisIndex: 1, data: ratings } ] });yAxisIndex指定系列用哪个 Y 轴0 是左边1 是右边。评分轴固定 0 到 10避免自动缩放导致曲线看起来波动很大。这个细节在演示时很加分因为老师一眼就能看出你考虑了量纲问题。4.3 响应式与自适应窗口变化时图表不跟着变怎么办ECharts 默认不会随窗口大小自动调整需要手动监听 resize 事件。window.addEventListener(resize, () { chart.resize(); });如果页面上有多个图表每个都要调用 resize或者用一个数组存起来统一处理。另一个常见问题是图表容器初始宽度为 0比如放在 flex 布局里还没渲染完就初始化导致图表挤成一条线。解决办法是给容器设固定宽高或者在 DOM 加载完成后再初始化。5. 避坑与排查部署和运行中最容易翻车的五个点5.1 现象启动报 ModuleNotFoundError原因依赖没装或版本不对解决用 requirements.txt 锁版本Flask、pandas、ECharts 前端库版本差异会导致 API 变化。比如 Flask 2.x 和 3.x 在before_first_request上有改动pandas 2.x 对fillna的行为也有调整。最稳的做法是生成 requirements.txt。pip freeze requirements.txt pip install -r requirements.txt如果换机器后报错先看报错模块名再确认版本。不要盲目pip install --upgrade升级可能引入新的不兼容。5.2 现象页面能打开但图表空白原因接口 404 或 JSON 字段不匹配解决先单独访问接口地址在浏览器直接输入http://127.0.0.1:5000/api/top_rated看返回什么。如果 404检查路由是否注册如果返回 HTML 而不是 JSON说明 Flask 走了模板渲染而不是 jsonify。字段不匹配的问题在控制台看 Network 面板对比返回的 key 和前端取的 key。5.3 现象SQLite 报 database is locked原因多线程同时写解决加 timeout 或改用连接池Flask 开发服务器默认多线程SQLite 在并发写入时会锁库。解决办法是在 connect 时加timeout10或者把写操作集中到一个线程。更彻底的做法是读用 SQLite、写用队列但期末作业规模加 timeout 就够了。conn sqlite3.connect(DB_PATH, timeout10)5.4 现象中文显示成方块原因ECharts 默认字体不含中文解决指定字体或引入中文字体在 setOption 里加textStyle: { fontFamily: Microsoft YaHei, sans-serif }或者确保页面 meta 声明了 utf-8。如果部署到 Linux 服务器服务器上可能没有中文字体需要安装字体包或改用图片渲染方案。5.5 现象部署到服务器后附件路径错误原因用了相对路径解决全部改成基于项目根目录的绝对路径这是热词里提到的高频问题。Flask 里用os.path.dirname(os.path.abspath(__file__))获取当前文件目录再拼接数据库路径和静态资源路径。import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(BASE_DIR, movies.db)这样无论从哪个目录启动路径都不会错。6. 让系统更像一个作品三个提升演示效果的进阶技巧第一个技巧是加一个搜索框用 Flask 接收关键词在 SQL 里用LIKE模糊匹配电影名返回结果后用表格展示。这个功能代码量不大但演示时交互感很强。第二个技巧是给图表加动画和主题ECharts 内置了 dark 主题一行echarts.init(dom, dark)就能换风格适合投影演示。第三个技巧是把分析结果导出成 CSV用 Flask 的send_file返回让老师能下载数据自己看。from flask import send_file import io app.route(/export) def export(): conn get_db() rows conn.execute(SELECT title, year, rating FROM movies ORDER BY rating DESC).fetchall() conn.close() output io.StringIO() output.write(title,year,rating\n) for r in rows: output.write(f{r[title]},{r[year]},{r[rating]}\n) output.seek(0) return send_file( io.BytesIO(output.getvalue().encode(utf-8-sig)), mimetypetext/csv, as_attachmentTrue, download_namemovies_export.csv )utf-8-sig是为了 Excel 打开 CSV 时不乱码这个细节很多人会忽略。as_attachmentTrue让浏览器直接下载而不是在页面显示。验证系统是否完整可以按这个顺序走一遍启动 Flask → 打开首页 → 看三个图表是否都有数据 → 点搜索框输入关键词 → 看结果是否返回 → 点导出按钮 → 看 CSV 是否能下载并正常打开。全部通过这个项目就达到了可以交作业的水平。我自己做这类项目最大的教训是不要等到最后一天才部署。本地跑通和服务器跑通是两回事路径、端口、依赖、字体每一个都可能让你熬夜。提前一天把部署流程走一遍留出改错的时间比多写一个图表有用得多。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

STM32 DMA突发传输实现动态PWM脉冲序列 2026/9/28 2:44:49

STM32 DMA突发传输实现动态PWM脉冲序列

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
MikroORM Seeding 完全指南:Seeder 类、实体工厂与数据库种子数据实践 2026/9/28 2:44:42

MikroORM Seeding 完全指南:Seeder 类、实体工厂与数据库种子数据实践

后端 【免费下载链接】mikro-orm TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases. 项目地址: https://gitcode.com/gh_mir…

阅读更多 →
U2Net证件照生成:轻量级人像抠图与标准化合成实战 2026/9/28 2:44:35

U2Net证件照生成:轻量级人像抠图与标准化合成实战

简介:本资源是一套基于Python与U2Net模型的轻量级证件照智能生成方案,面向深度学习初学者、计算机视觉实践者及图像处理开发者,解决传统证件照制作中背景替换不精准、光照不均、人像边缘毛糙等痛点。压缩包共18个文件,含5个核心Py…

阅读更多 →
拒绝丑模板!做h5最好的网站图解步骤全解析 2026/9/28 2:44:35

拒绝丑模板!做h5最好的网站图解步骤全解析

拒绝丑模板!做h5最好的网站图解步骤全解析 你是不是也被那些千篇一律的模板网站恶心到了?看着满屏的廉价配色和僵硬的布局,心里直嘀咕:“这能代表我们公司的形象吗?”模板网站太丑不够用,这不仅是审美问题,更是品牌信任的流失。想找到…

阅读更多 →
Mosquitto 1.6.10 发布:桥接配置校验、密码文件修复与 OpenSSL 3.0 支持详解 2026/9/28 2:44:29

Mosquitto 1.6.10 发布:桥接配置校验、密码文件修复与 OpenSSL 3.0 支持详解

物联网消息队列后端 【免费下载链接】mosquitto Eclipse Mosquitto - An open source MQTT broker 项目地址: https://gitcode.com/gh_mirrors/mosquit/mosquitto 点击查看 免费下载 导读 Mosquitto 1.6.10 是 Eclipse Mosquitto 于 2020 年 5 月发布的一个 bugfi…

阅读更多 →
ng-zorro-antd Resizable 栅格化拖拽调整:Grid 模式原理与实战 2026/9/28 2:44:29

ng-zorro-antd Resizable 栅格化拖拽调整:Grid 模式原理与实战

UI组件前端 【免费下载链接】ng-zorro-antd Angular UI Component Library based on Ant Design 项目地址: https://gitcode.com/gh_mirrors/ng/ng-zorro-antd 点击查看 免费下载 导读 本文围绕 ng-zorro-antd 组件库中 Resizable(可调整尺寸&#xff…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉