资讯动态

Python电影数据可视化系统源码全解析:从Flask到ECharts

发布时间:2026/10/3 3:57:59 来源:尧图企业网站定制
简介基于Python的电影数据可视化分析系统源码包面向高校期末大作业和课程设计可帮助完成数据采集、清洗、存储与可视化全链路实践。包内共8个文件包括4个.py脚本Flask应用入口、豆瓣爬虫、词云生成等、2个模板/静态资源zip包、1个SQLite数据库文件douban.db和1份README说明压缩包整体仅3.5MB。目前已有2565人学习下载后通过app.py即可启动Web平台利用数据库中的电影数据进行交互式图表分析和词云展示。这套源码提供了从爬虫到可视化的完整可运行方案结构清晰适合课程设计参考也便于在此基础上扩展推荐、评论分析等功能。1. 源码zip里装的不是代码是一条完整的数据流水线拿到「基于Python的电影数据可视化分析系统源码.zip」多数人的第一反应是解压、装依赖、跑app.py然后被ModuleNotFoundError和一张空白页面来回折磨半小时。这不是你手生而是这类课程设计源码的共同特点骨架完整、说明缺失跑通靠运气跑通之后能改哪里反而没人讲清楚。拆开看这个系统就是一条流水线爬虫或公开数据源拿电影数据Pandas清洗统计Flask包成JSON接口ECharts渲染成柱状图、折线图和词云看板。数据获取、清洗、存储、后端接口、前端展示全串在一起是初学Python时性价比最高的练手方向也是课程设计、毕业设计里出现频率最高的套路之一。这篇笔记按「解压后看什么 → 数据层怎么做 → 后端接口怎么写 → 图表怎么配 → 哪些坑最常见 → 怎么改成自己的」展开目标是拿到同类源码包后半天内跑通并动手改造。2. 先不急着run源码包结构、数据来源与存储选型2.1 解压之后先看这三样依赖清单、入口文件、数据文件这类Python可视化分析系统的目录结构高度相似打开zip之后不要急着双击app.py先按下面这个典型结构对照一遍。你手上的包可能差一个两个文件但主框架基本是这个形态。movie_visualization/ ├── app.py # Flask 入口路由和接口都在这 ├── requirements.txt # 依赖清单版本锁得越死越省事 ├── data/ │ └── movie_data.csv # 清洗后的电影数据常见编码是UTF-8 ├── spider/ │ └── fetch_movie.py # 爬虫脚本可选跑不通不影响主流程 ├── static/ │ ├── js/ │ │ └── echarts.min.js │ └── css/ │ └── style.css └── templates/ └── index.html # 看板主页ECharts初始化多半在这里先打开requirements.txt看依赖是不是flask、pandas、requests、sqlite3这一套。sqlite3是Python自带不需要额外装。接着打开app.py看端口号常见的课程设计源码端口是5000或者5001知道端口后面调试省很多事。最后确认data目录下有数据文件如果只有空目录说明数据要自己跑spider生成这一步不解决后面全是白用功。这类源码和天气分析系统、农产品价格数据可视化-flask、网约车大数据综合项目flaskecharts属于同一套骨架区别只在数据字段和图表类型。你如果能把这套结构看懂换成交通、电商、校园大数据都是一样的流程。安装依赖时不建议裸敲pip install因为系统里多环境时pip可能指向另一个Python。更稳的做法是python -m pip install -r requirements.txt # 如果requirements缺失逐个补齐 python -m pip install flask pandas requests用python -m pip能保证包装进当前运行python对应的环境。这一步是很多新人翻车的起点。装完依赖后先用python -m flask --help验证再用python app.py启动比直接双击文件更容易看到报错。2.2 数据清洗Pandas做对三件事接口数据才不翻车电影数据集的常见字段是title、rating、year、genre、votes、director。但课程设计源码里的字段名经常不统一有的叫score有的叫release_year还有的日期里夹着汉字。所以第一步永远是先看列名再决定怎么清洗。import pandas as pd df pd.read_csv(data/movie_data.csv, encodingutf-8) # 先打印列名字段名对不上后面全白搭 print(df.columns.tolist()) # 1. 把评分列转成数值转不了的就地变成NaN不要直接报错 df[rating] pd.to_numeric(df[rating], errorscoerce) # 2. 年份列经常混着2019年、2019/01/01这种脏值先用正则抽四位数字 df[year] df[year].astype(str).str.extract(r(\d{4})) df[year] pd.to_numeric(df[year], errorscoerce) # 3. 删掉没有评分或年份的行否则聚合时会出现一堆NaN干扰统计 df df.dropna(subset[rating, year]) # 4. 类别字段填空饼图和词云都要求非空字符串 df[genre] df[genre].fillna(未知) # 验证清洗结果 print(df.groupby(year)[rating].mean().round(2).head())这里最关键的是pd.to_numeric的errorscoerce参数。它能把“8.5分”“8.5/10”这类脏值统统变成NaN再由你决定是删还是填。用errorsraise会让脚本中断直接写float()遇到脏数据直接抛异常。评分从字符串变成数值后后面SQL里的AVG才能算对。第二关键是dropna这一步。很多源码清洗时不删NaN结果聚合出来的平均分被NaN污染成null前端图表出现缺口。至于genre字段如果你后续要做饼图必须保证它不为空否则ECharts会默认把空值当作一个叫“undefined”的类别。清洗完的数据建议直接重新导出到data目录后面Flask进程读取的就是这份干净文件df.to_csv(data/movie_data_clean.csv, indexFalse, encodingutf-8)如果你后续要写进SQLite更推荐用自己建的movie.db而不是直接操作CSV。Pandas写入SQLite的常见写法是from sqlalchemy import create_engine engine create_engine(sqlite:///movie.db) df.to_sql(movies, engine, if_existsreplace, indexFalse)用sqlalchemy的好处是写入时自动按DataFrame列建表不用手写CREATE TABLE。如果你只用sqlite3连接新版Pandas会提示需要sqlalchemy这个坑比较隐蔽。装上sqlalchemy之后后续查询接口可以完全在自己的表上做不依赖CSV。2.3 存SQLite还是MySQL课设和演示场景要分清电影数据可视化系统最常被问到的一个问题是“要不要上MySQL”。我的答案是单机演示用SQLite多人看板才需要MySQL。这两者的选择直接影响你后面接口怎么写、部署到哪台机器。对比项SQLiteMySQL安装成本Python自带零配置需要安装服务端还要装驱动并发能力单机低并发写多会锁库支持多连接演示更稳定迁移难度直接拷movie.db一个文件需要导SQL再导入目标库适用场景课设、本地个人系统多人访问、内网演示、后续扩展如果你是课程设计答辩SQLite完全够了。把movie.db文件拷到另一台机器只要路径对就能跑。但如果答辩现场需要多台电脑同时访问看板SQLite会出现database is lockedMySQL就不会。你可以在答辩前把连接字符串换一下改成本地MySQL。用pymysql连接MySQL的写法如下注意charset要写utf8mb4否则中文插入会报错import pymysql conn pymysql.connect( host127.0.0.1, userroot, password你的密码, databasemovie_db, charsetutf8mb4 )有的源码直接用flask-sqlalchemy那就不用手动管理连接了。你只需要把数据库URI从sqlite改为mysqlpymysql://。但前提是MySQL里先建好movie_db这个库表结构可以由DataFrame的to_sql自动生成。换库之后原来SQL里的GROUP BY语法不变SQLite和MySQL都支持这也是它适合做课设的原因。我不建议一上来就花半小时配MySQL。先把SQLite版本跑通再根据展示诉求决定要不要换。否则环境问题会淹没业务问题你都不知道到底是数据错了还是接口错了。2.4 数据来源爬虫不是这个系统的核心数据才是电影数据从哪来常见有三条路作者打包了一份CSV、去TMDB这类开放API拉、或者写爬虫抓豆瓣。先说结论如果zip里已经带了CSV直接用不要重造轮子。如果只有爬虫脚本优先检查脚本里有没有写死的API地址和key。TMDB开放API是相对省心的数据源不需要处理登录和反爬只要注册一个免费key就能用。requests的典型拉取代码import requests url https://api.themoviedb.org/3/movie/popular params { api_key: 你的key, language: zh-CN, page: 1 } resp requests.get(url, paramsparams, timeout10) data resp.json()这里的timeout参数很关键。不写timeout一旦网络卡住爬虫脚本会一直挂着看起来像死循环。拿到JSON后可以直接pd.json_normalize(data[results])转成DataFrame再套用上面的清洗步骤。豆瓣的接口反爬严格一般课程设计源码里就算写了豆瓣爬虫基本也是跑两次就403。你在自己的机器上调通它要花的时间远超项目本身不值得。真正要交作业时把CSV数据集准备好爬虫脚本能跑通一次截图就够了。很多可视化源码其实只是借了“爬虫”的壳核心价值在后面的清洗、接口和图表展示。3. Flask后端接口怎么写先定JSON结构再写路由3.1 入口文件里的应用初始化与连接管理Flask源码的开头几乎都是同一个模式创建app实例、定义数据库连接、注册路由。很多人会在多个文件之间来回import最后报ImportError或CircularImport。课程设计规模不需要蓝图单个app.py完全够但如果你后续加页面多可以拆成蓝图。先看应用初始化这十行from flask import Flask, jsonify, render_template, g import sqlite3 app Flask(__name__) def get_db(): conn sqlite3.connect(movie.db) conn.row_factory sqlite3.Row return connconn.row_factory sqlite3.Row是这十行里最值钱的一行。没它你的fetchone返回的是普通元组访问字段要靠下标r[0]有了它返回对象可以直接r[title]来取值。后面jsonify之前甚至不用手写dict转换sqlite3.Row本身就能被序列化。很多源码为了省事直接用字典列表但那是在后端多写了一段循环换来的。路由函数里常见两种连接管理一种每次请求新建连接、用完close另一种用flask的g对象在请求周期内复用。第一次跑通优先用前面那种简单直观app.route(/) def index(): return render_template(index.html)这个根路由负责返回大屏HTML接口路由全部挂在/api下面把数据和页面分开。如果你拿到的是前后端分离版本根路由可以直接返回一个JSON前端用别的端口发请求那就要加CORS。3.2 三个核心接口覆盖90%的电影看板需求大多数电影可视化系统逃不开三个统计维度总体情况、年份趋势、类型分布。先把这三个接口写对剩余图表都是同一套逻辑的变体。下面是把SQL写在Flask路由里的标准写法。app.route(/api/summary) def summary(): conn get_db() total conn.execute(SELECT COUNT(*) AS cnt FROM movies).fetchone()[cnt] avg conn.execute(SELECT AVG(rating) AS avg FROM movies).fetchone()[avg] conn.close() return jsonify({ total: total, avg_rating: round(float(avg or 0), 2) }) app.route(/api/year_trend) def year_trend(): conn get_db() rows conn.execute( SELECT year, ROUND(AVG(rating), 2) AS avg_rating, COUNT(*) AS cnt FROM movies GROUP BY year ORDER BY year ).fetchall() conn.close() return jsonify({ years: [r[year] for r in rows], avg_rating: [r[avg_rating] for r in rows], count: [r[cnt] for r in rows] })avg or 0这个细节是为了防止空表时AVG返回Nonejsonify序列化None没问题但前端的浮点数处理会多一层判断。SQL里的ROUND能帮你把平均分小数位控制住比在Python里round更直接毕竟数据没到Python就已经是两位小数了。第三个接口是类型分布注意genre字段经常是一行存多个类型比如“剧情,爱情”。这种字段直接GROUP BY会被算成一个整体饼图出来全是“剧情,爱情”“剧情,悬疑”这种组合项。常见的处理是后端直接把一行拆成多行再按主类型统计app.route(/api/genre_pie) def genre_pie(): conn get_db() rows conn.execute( SELECT genre, COUNT(*) AS cnt FROM movies GROUP BY genre ORDER BY cnt DESC LIMIT 10 ).fetchall() conn.close() return jsonify([ {name: r[genre], value: r[cnt]} for r in rows ])这个接口返回的数组结构直接就是ECharts饼图series.data要求的格式前端不需要再做map重排。这是写接口时最容易忽略的原则接口结构跟着图表结构走而不是跟着数据库表走。如果前端每种图表都要二次转换字段名说明后端接口没设计好。3.3 前端Fetch与接口字段对齐差一个字母图表就是空的接口写完之后前端fetch拿到数据最关键的一步是字段名对得上。很多同学图和接口各调各的控制台也不报错就是图不渲染。原因往往是后端返回avg_rating前端读avgRating。fetch(/api/year_trend) .then(response response.json()) .then(data { console.log(data); // 调试期不要删先确认字段结构 trendChart.setOption({ xAxis: { data: data.years }, series: [{ data: data.avg_rating }] }); });这里的console.log是调试阶段的后悔药。先看控制台打印出来的对象到底有哪些键再写给图表。后端接口如果返回的是{years: [...], avg_rating: [...]}前端就按这个结构取。如果嫌麻烦可以定义接口规范文档但课设项目直接看打印更快。3.4 端口、debug模式和跨域三个影响联调的小问题app.py最后一段通常是if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)host0.0.0.0表示监听所有网卡这样局域网里另一台电脑可以用http://你的IP:5000访问。答辩时用手机连同一个WiFi打开看板效果比把电脑转过去给老师看要专业。port5000如果被占用换5001、5002都行但前端fetch里的地址和浏览器地址也同步改。debugTrue有两个副作用第一是Flask带了一个reloader代码一改就自动重启但有时会和SQLite抢锁第二是错误信息直接显示在页面上不适合给其他人演示。跑通之后把debug关掉。如果前端不是由Flask的templates渲染而是单独开了一个静态服务器那就需要处理跨域。最省事的做法from flask_cors import CORS CORS(app)加这一行Flask后端就允许了来自其他端口的浏览器请求。不写CORS时前端控制台会报“CORS policy: No Access-Control-Allow-Origin header”这个报错见过就别慌不是业务代码的问题。4. ECharts数据可视化看板从单个图表到一整屏4.1 图表初始化的最小页面先让一个图跑起来拿到这套源码后最快验证后端接口能不能和前端串通的方法是先把templates/index.html里的图表删到只剩一个折线图。下面这套是能跑通的最小页面不要一上来就弄十几个图表排查问题会无从下手。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title电影数据看板/title script src{{ url_for(static, filenamejs/echarts.min.js) }}/script style .chart { width: 100%; height: 400px; } /style /head body div idmain classchart/div script const chart echarts.init(document.getElementById(main)); fetch(/api/year_trend) .then(res res.json()) .then(data { chart.setOption({ title: { text: 历年电影平均评分 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.years }, yAxis: { type: value, min: 0, max: 10 }, series: [{ name: 平均评分, type: line, smooth: true, data: data.avg_rating }] }); }); /script /body /html这里script标签里的echarts.min.js来自本地static目录。如果你把前端改成用CDN需要确保答辩现场能联网如果离线本地文件是唯一稳妥选择。这也是为什么很多源码包里带一份echarts.min.js的原因。容器div必须要有确定高度。上面CSS里写死了height: 400px这是最不容易出错的做法。ECharts初始化时如果容器高度为0图表画布就是空白控制台还不会有明显报错。4.2 折线图、柱状图、饼图的option差异集中在seriesECharts的option核心差异不在坐标系而在series里的type和data结构。折线图转柱状图只需要把type从line改成bar加两个和柱子相关的配置。下面是一套完整的柱状图option展示每年电影生产数量。option { title: { text: 历年电影数量 }, tooltip: { trigger: axis }, grid: { left: 50, right: 20, top: 50, bottom: 30 }, xAxis: { type: category, data: years }, yAxis: { type: value }, series: [{ name: 电影数量, type: bar, data: counts, barWidth: 40%, // 柱子宽度百分比相对坐标轴 itemStyle: { color: #5470c6 // 统一颜色省得一张图太花 } }] };barWidth这个参数很实用不写时ECharts会根据容器宽度自动算但数据量少时柱子会非常宽数据多时挤成一条线。设置为40%后视觉上稳定很多。itemStyle里统一颜色是让看板看起来整洁的常用手段特别是多图表时每张图都五颜六色会显得很业余。饼图又是另一套结构。饼图不需要xAxis和yAxis只认series.data里的name和value。接口端已经返回了[{name,value}]格式前端可以直接塞进去option { tooltip: { trigger: item }, legend: { bottom: 0 }, // 图例放底部避免遮挡环形 series: [{ type: pie, radius: [35%, 65%], // 环形比例 data: genrePieData, label: { show: true, formatter: {b}: {d}% } }] };radius数组的写法是环形饼图的核心。第一个百分比是内半径第二个是外半径差值越大环越粗。{d}%是ECharts内置的百分比占位符可以直接在标签里显示各项占比不用后端再算一份百分数。4.3 大屏适配resize不是万能药根字号要跟着窗口走电影数据可视化系统如果做成大屏最常见的适配问题是图表在1920显示器正常换到笔记本上就溢出或缩成一团。原因是只写了固定px。正确的做法是整个页面按1920设计稿做然后用rem动态缩放。function setRem() { document.documentElement.style.fontSize (window.innerWidth / 1920) px; } setRem(); window.addEventListener(resize, setRem);这段脚本把根字号设置成浏览器宽度的1920分之一。假设设计稿是1920宽那16px的字号就写成1rem实际显示在1366宽的屏幕上会自动缩小。图表容器宽度用百分比或vw高度用rem。每个图表实例还要监听resize事件否则窗口改变后图表是脏的拖影或空白window.addEventListener(resize, () chart.resize());这里要强调chart.resize()只负责让已有图表重算尺寸不负责容器高度。如果容器本身height为0resize也救不回来。调试时先看元素面板里div的实际尺寸再决定是补CSS还是补JS。4.4 地图组件新版ECharts不内置中国地图数据很多电影可视化源码附带一张“各地区电影数量”的地图但跑到一半报错“Map china not found”。这是ECharts 5之后的改动地图GeoJSON不再内置需要手动注册。解决方式很简单先准备好china.json再用registerMap注册。fetch(/static/js/china.json) .then(res res.json()) .then(mapData { echarts.registerMap(china, mapData); chart.setOption({ series: [{ type: map, map: china, data: movieMapData }] }); });movieMapData的格式是[{name: 广东, value: 12}, ...]name必须和GeoJSON里的省份全称完全一致少一个字都显示不出来。如果你拿到的源码里没有china.json可以找一个公开的GeoJSON放到static目录注册名称用统一的值。5. 跑通这套源码最常见的5个坑都是血泪经验5.1 ModuleNotFoundError装过了但没装进当前解释器最玄学的一个现象命令行里pip list能看到flask运行python app.py仍然报No module named flask。原因pip和python不属于同一个解释器。常见于电脑里同时装了Anaconda和官网Python或者VSCode里选了虚拟环境但终端还是全局环境。这类报错最迷惑人的地方在于你明明装过系统却像失忆一样。解决先确认解释器路径再用python -m pip装依赖。which python which pip python -m pip install flask pandas requestspython -m pip能确保包装进当前python所在环境。如果你用VSCode按CtrlShiftP选“Python: Select Interpreter”选中和终端一致的解释器。用venv的话创建后先激活再装包。5.2 中文乱码CSV编码和JSON编码是两个不同的坑现象控制台打印中文正常网页里出现锟斤拷或者接口返回一堆\u7528\u6237。原因CSV文件实际是gbk编码read_csv用utf-8读导致乱码Flask的jsonify默认把中文转成unicode转义浏览器能解析但代码里看着难受。解决两处分开处理。df pd.read_csv(data/movie_data.csv, encodinggbk, errorsreplace) app.json.ensure_ascii False # Flask 2.2及以上版本如果CSV是utf-8把encoding改成utf-8。errorsreplace的作用是遇到坏字节时替换成问号而不是让整个脚本崩溃。旧版Flask里ensure_ascii设置可能叫app.config[JSON_AS_ASCII] False两个都是同一个功能的入口。切换编码后重新导出一次CSV后续读文件就稳定了。5.3 图表不出来且F12没报错先量容器高度现象页面打开后图表区域空白浏览器控制台没有任何报错后端接口也返回了正常数据。刷新几次还是一样。原因ECharts在初始化时读取容器的高度如果CSS没有给容器确定高度或者父级用了flex但子项没有撑高图表画布高度就是0。resize监听也救不了因为它只重算尺寸不会从无到有。解决给图表容器写死高度。#main { width: 100%; height: 400px; }如果图表在flex布局里确保父级有确定高度并且子项没有把高度压缩成auto。调试时在Elements面板里找这个div看Computed样式里的height是不是0一眼就能定位。养成写死min-height的习惯能避免很多“明明代码都对”的尴尬。5.4 地图报“map china not found”ECharts 5不再内置地图数据现象柱状图和饼图都正常一切到地图页面控制台就报Map china not found页面上只有个空坐标轴。原因ECharts 5默认不内置地图GeoJSON地图数据点要通过echarts.registerMap手动注册。很多课程设计源码是拿ECharts 4写的原封不动跑到ECharts 5就会这样。解决准备好china.json在页面里先fetch再注册然后才setOption。对应代码在4.4节已经写过这里不再重复。要特别提醒的是检查源码里引用的echarts版本如果版本较老可能不存在的registerMap是正常的升级前先看官方的升级说明。5.5 SQLite报“database is locked”先关掉Flask的debug模式现象连续刷新接口几次某次请求直接500控制台报sqlite3.OperationalError: database is locked。原因Flask debugTrue会启动reloader代码改动时自动重启进程相当于有两个进程都可能打开数据库再遇到一个写事务没提交SQLite的文件锁就冲突了。低并发课设里这个锁主要来自reloader而不是真正的高并发。解决先关debug再试看问题是否消失。同时把数据库操作改成用完即关conn get_db() try: conn.execute(INSERT OR REPLACE INTO movies ...) conn.commit() finally: conn.close()每次请求都新建连接用完后close不要用全局连接对象。如果一定要保留debug模式可以把数据库引擎换成MySQL再测试锁的问题会少很多。还有一种情况是另一个终端开着sqlite3命令行没退出也会锁库查一下后台进程。6. 把这份源码改造成自己的系统定时更新与验证技巧6.1 用定时脚本让数据自动更新课程设计演示时最怕数据太旧被老师问住。常见做法是写一个fetch_daily.py定时调一次开放API把结果覆盖写进CSV和数据库。Windows下可以用任务计划程序Linux下用cron不想碰系统任务就装个schedule库定时触发。注意把API key单独放进config.py不要硬编码在爬虫里否则代码一分享key就泄露。# fetch_daily.py import requests import pandas as pd resp requests.get(API_URL, params{api_key: API_KEY}, timeout10) df pd.json_normalize(resp.json()[results]) df.to_csv(data/movie_data.csv, indexFalse, encodingutf-8) print(f更新完成共{len(df)}条)6.2 加一个词云页让看板维度更完整电影标题、类型、简介都可以做词云。中文词云比英文多一个jieba分词步骤而且WordCloud默认字体不支持中文必须指定中文字体路径否则全显示方框。这块代码量不大但能直观提升项目的“分析感”。import jieba from wordcloud import WordCloud text .join(jieba.lcut( .join(movie_df[title].tolist()))) wc WordCloud(font_pathC:/Windows/fonts/simhei.ttf, width800, height600).generate(text) wc.to_file(static/img/wordcloud.png)6.3 跑通后的三个验证动作第一把后端接口返回的结构和前端读取的字段逐一核对用console.log打印一遍别靠猜。第二改变窗口大小确认所有图表的resize都正常工作。第三重启一次系统确认数据不是靠内存临时变量存着的刷新页面后数据还在。我自己的习惯是解压源码后先跑一遍原始代码确认数据流是通的再逐行改成自己的数据和样式。这样每一步改动都能定位是后端问题还是前端问题不会把环境错误和业务错误混在一起。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑