ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python电影数据可视化系统:爬虫、Flask与PyECharts实践

Python电影数据可视化系统:爬虫、Flask与PyECharts实践 简介面向计算机相关专业毕业设计学生与项目实战学习者提供一套电影数据可视化分析系统完整源码包覆盖数据采集、持久化、可视化分析与票房预测全流程。压缩包共三十八个文件六个Python源码模块负责主程序、数据库、基础/详情爬取和辅助工具三个Jupyter演示脚本覆盖基于Pandas与SQL的可视化分析及票房预测建模二十四张结果图帮助理解分析结论和数据库结构另附SQL建库脚本及PDF/README说明文档大小仅5.18MB下载后可正常运行。系统从豆瓣TOP250与猫眼票房榜采集评分、票房等数据利用Pandas与MySQL两种方案完成持久化并依据可视化结论构建票房预测模型适合毕业设计、课程设计或期末大作业。已有4332人学习使用整体结构清晰、调试严格能帮助快速完成从数据抓取到预测展示的完整课题。1. 为什么毕业设计都选它Python 电影数据可视化分析系统的真实价值电影数据可视化分析系统是毕业设计里最稳的一类选题数据源公开可爬、技术链路完整覆盖爬虫、数据处理、数据库、Web 后端和前端图表一套系统串起整个大学阶段的核心技术栈。用 Python 做这套系统的优势在生态成熟——pandas 处理表格、PyECharts 生成交互图表、Flask 起轻量接口从零到可答辩通常不超过四个月。适合的人群很明确计算机、数据科学相关专业本科生以及想拿完整项目履历去求职的转行者。本文不打算逐行贴全量源码——那些在源码包里都能看到——而是把系统最关键的四个模块数据采集清洗、可视化图表生成、Web 集成、部署排错按实际开发顺序拆开讲清楚设计逻辑。每段代码骨架都可以直接改参数复用你拿到的源码包如果技术栈版本有出入也能按这里的思路快速迁移。2. 系统架构与技术选型从 Python 生态到可视化前端的分层设计2.1 Flask PyECharts MySQL这套组合为什么是毕业设计的标准答案先看整体架构。一个典型的电影数据可视化分析系统分四层数据采集层爬虫脚本、数据存储层数据库、后端服务层Flask API、前端展示层HTML 页面 ECharts 渲染。这个分层不是拍脑袋定的每一层都能单独替换而不影响其他层这也是答辩时讲系统可扩展性的落脚点。常见选型组合如下层次常用技术备选方案选型理由采集层requests BeautifulSoupScrapyrequests 轻量单脚本就能调试适合万级以下数据量存储层MySQL / SQLiteMongoDB电影数据是结构化表格关系型数据库查询聚合更直接后端FlaskDjangoFlask 单文件起服务路由简单五六个接口足够可视化PyEChartsMatplotlib / Plotly输出 ECharts 标准 JSON前端渲染交互图表答辩效果好为什么不用 DjangoDjango 自带 Admin、ORM 和中间件体系适合大型业务系统但毕业设计系统通常只需要五六个 JSON 接口Flask 的路由装饰器写起来短一半。为什么不用 MatplotlibMatplotlib 输出静态图片浏览器里没法悬停看数值、没法缩放答辩演示的交互感差一截。2.1.1 PyECharts 的运行机制服务端组装 JSON前端负责渲染PyECharts 的核心机制值得专门说清楚它不是服务端画好一张图返回给前端而是在服务端拼装一段 ECharts option 配置 JSON由浏览器里的 echarts.min.js 去渲染。Flask 拿到图表对象后有两种传递方式——直接把 JSON 注入 HTML 模板的 script 标签或通过/api/chart/xxx接口返回前端异步加载。前者适合页面首屏就要全部图表的情况后者适合图表数量多、需要按需加载的场景。理解这一点后续的接口设计和性能优化都建立在它上面。2.2 数据流向从爬虫入库到图表渲染的完整链路整条数据链路可以概括为爬虫抓取 HTML 页面 → pandas 清洗为标准表格 → 写入 MySQL → Flask 查询接口做聚合统计 → PyECharts 生成 option JSON → 前端 ECharts 实例渲染。这里有一个容易被忽略、但答辩常被追问的设计点图表需要的数据几乎从来不是明细记录而是聚合结果。比如各年份电影数量趋势图SQL 里一句GROUP BY year就计算完毕不需要把几万条记录全传到前端。后端接口的职责不是把表数据原样返回而是按图表维度做聚合计算后再返回。想清楚这一点接口的设计就变成为图表服务而不是为表服务。2.3 目录结构与核心文件职责拿到源码包第一步不是急着跑起来而是先看目录结构确认每个文件的职责边界。我一般会这样组织movie_analysis/ ├── app.py # Flask 入口注册路由 ├── config.py # 数据库连接、爬虫间隔、演示模式开关 ├── models.py # SQLAlchemy 数据模型定义 ├── scraper/ │ ├── spider.py # 电影榜单爬虫 │ └── cleaner.py # pandas 清洗脚本 ├── visualization/ │ ├── charts.py # PyECharts 图表生成函数 │ └── options.py # 图表公共参数配置 ├── templates/ │ └── index.html # 前端页面 ├── static/ │ ├── js/ │ └── css/ └── requirements.txtrequirements.txt 建议锁定版本flask3.0.3 pandas2.2.3 pyecharts2.0.7 requests2.31.0 beautifulsoup44.12.3 SQLAlchemy2.0.36 pymysql1.1.1用精确锁版本而不是是因为数据可视化相关库的接口演进较快——pyecharts 从 1.x 升 2.x 时render系列接口就有调整。答辩前一周临时升级依赖导致图表全挂的情况每年都有毕业生踩中。代码说明config.py独立放连接参数和开关是为了后面演示模式切换和数据库迁移时不改业务代码visualization/单独成包保证图表生成逻辑与 Flask 路由解耦想换成 Plotly 时只需替换这个包。3. 数据获取与清洗爬虫脚本怎么写才不会被反爬机制拦在门外3.1 用 requests BeautifulSoup 抓取电影榜单数据数据获取是整个系统的地基。常见数据源包括豆瓣电影 Top250、猫眼票房榜、TMDB 公开 API 等。多数毕业设计选豆瓣 Top250因为页面结构稳定、字段齐全片名、导演、主演、评分、年份、地区、类型、经典台词10 页 250 条数据也足够支撑所有图表。以 Top250 为例URL 的start参数以 25 为步长递增核心抓取逻辑import requests from bs4 import BeautifulSoup import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 } def fetch_movie_list(start0): url fhttps://movie.douban.com/top250?start{start} resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, html.parser) items [] for li in soup.select(div.item): title li.select_one(span.title).text info li.select_one(div.bd p).text.strip().split(\n) rating li.select_one(span.rating_num).text quote_node li.select_one(span.inq) quote quote_node.text if quote_node else items.append({ title: title, director_actors: info[0].strip(), year_region_type: info[1].strip(), rating: float(rating), quote: quote, }) return items def crawl_top250(): all_data [] for start in range(0, 250, 25): all_data.extend(fetch_movie_list(start)) time.sleep(2) return all_data代码说明HEADERS里的User-Agent必须伪装成真实浏览器否则大多数站点直接返回 403。select返回节点列表select_one返回单个节点选择器写错是爬虫报错的第一大来源。time.sleep(2)控制请求频率不是可有可无的装饰——降低对目标站点的压力也避免触发限流。resp.raise_for_status()在状态码非 200 时抛出异常失败时快速暴露问题而不是拿到错误页面继续解析。如果你的数据源不是豆瓣不要照抄选择器。打开目标页面按 F12在 Elements 面板用 CtrlF 搜索目标字段的文本观察 DOM 层级后再改select参数。政府开放数据平台、电影数据库公开接口往往没有严格反爬优先考虑这些渠道。3.2 pandas 清洗去重、缺失值处理、类型转换爬下来的数据不能直接用能踩的坑都在这里。豆瓣页面的year_region_type字段实际是年份 / 地区 / 类型的拼接文本形如1994 / 美国 / 犯罪 剧情必须按/分割才能拆成独立列import pandas as pd def clean_movie_data(raw_items): df pd.DataFrame(raw_items) # 拆分年份、地区、类型 split_cols df[year_region_type].str.split(/, expandTrue) df[year] split_cols[0].str.strip().astype(int) df[region] split_cols[1].str.strip() df[genre] split_cols[2].str.strip() df.drop(columns[year_region_type], inplaceTrue) # 多榜单合并时按标题去重保留首次出现 df.drop_duplicates(subset[title], keepfirst, inplaceTrue) # 评分缺失用中位数填充比用 0 填充对统计更友好 df[rating] df[rating].fillna(df[rating].median()) df[quote] df[quote].fillna() # 类型字段多值如犯罪 剧情提取第一个主类型 df[main_genre] df[genre].str.split().str[0] return df清洗逻辑要从实际数据出发。str.split(/, expandTrue)返回多列 DataFrame是处理复合字段的标准操作。drop_duplicates的keepfirst指保留首次出现的记录——如果你合并爬取了多个榜单同一部电影会重复出现这个参数直接决定去重结果。缺失值策略分字段评分用中位数填充是为了不影响后续平均值统计经典台词填空字符串是为了写入数据库时不触发 NOT NULL 约束。清洗完务必打印数据概况验证结果print(df.info()) print(df.describe())df.info()展示每列字段类型和非空数量df.describe()给出数值列的统计摘要。答辩演示时这两行输出能直接证明数据质量有验证过比口头描述有说服力得多。3.3 数据入库SQLite 与 MySQL 的取舍入库方案取决于部署环境。如果答辩现场不要求独立数据库服务SQLite 就够——文件即库、零配置、拷走整个项目就能跑。如果课程要求用 MySQL 或源码里用了 SQLAlchemy ORM则建库建表import pymysql def save_to_mysql(df): conn pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasemovie_db, charsetutf8mb4, ) cursor conn.cursor() # title 做唯一键配合 INSERT IGNORE 实现幂等写入 create_table_sql CREATE TABLE IF NOT EXISTS movies ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL UNIQUE, director_actors TEXT, year INT, region VARCHAR(128), genre VARCHAR(255), main_genre VARCHAR(64), rating FLOAT, quote TEXT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; cursor.execute(create_table_sql) insert_sql INSERT IGNORE INTO movies (title, director_actors, year, region, genre, main_genre, rating, quote) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) rows [tuple(r) for r in df[[title, director_actors, year, region, genre, main_genre, rating, quote]].to_numpy()] cursor.executemany(insert_sql, rows) conn.commit() cursor.close() conn.close()INSERT IGNORE遇到唯一键冲突时静默跳过配合title UNIQUE约束实现幂等写入——爬虫重复执行不会产生脏数据。executemany批量插入比循环执行单条execute快一个数量级250 条数据几乎瞬时完成。charsetutf8mb4必须与建表语句的DEFAULT CHARSETutf8mb4保持一致否则中文写入后页面显示乱码这是 pymysql 最常见的坑。4. 可视化图表的实现PyECharts 各类图表的参数设置与联动4.1 柱状图与折线图评分分布与年份趋势可视化是系统的门面答辩老师第一眼看的不是代码而是图表。PyECharts 的通用模式是先创建图表对象再设置数据和全局配置最后导出 JSON。以评分分布柱状图和年份趋势折线图为例from pyecharts.charts import Bar, Line from pyecharts import options as opts def rating_distribution_bar(df): # 评分分桶统计 bins [0, 6, 7, 8, 9, 10] labels [0-6, 6-7, 7-8, 8-9, 9-10] df[rating_bucket] pd.cut(df[rating], binsbins, labelslabels, rightFalse) count_series df[rating_bucket].value_counts().reindex(labels) bar ( Bar() .add_xaxis(labels) .add_yaxis(电影数量, count_series.tolist(), category_gap40%) .set_global_opts( title_optsopts.TitleOpts(title电影评分分布, subtitleTop250 样本), yaxis_optsopts.AxisOpts(name数量), xaxis_optsopts.AxisOpts(name评分区间), toolbox_optsopts.ToolboxOpts( feature{save_as_image: {title: 保存图片}} ), ) .set_series_opts(label_optsopts.LabelOpts(positiontop)) ) return bar def year_trend_line(df): year_counts df[year].value_counts().sort_index() line ( Line() .add_xaxis(year_counts.index.tolist()) .add_yaxis(上映电影数, year_counts.values.tolist(), is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title电影年份分布趋势), datazoom_opts[opts.DataZoomOpts(range_start10, range_end90)], ) ) return linepd.cut做评分分桶返回的 Categorical 类型配合reindex(labels)保证柱状图横轴顺序固定不会因为某些区间计数为 0 而缺失。category_gap40%控制柱间距数值过小柱子挤在一起、过大数据稀疏。折线图is_smoothTrue让线条变成贝塞尔曲线视觉上比折线更专业。DataZoomOpts是重点——年份跨度大时不加缩放组件整条折线挤成一团加了这个组件答辩时能拖动滑块展示不同时间窗口交互感直接拉满。4.1.1 Toolbox 工具箱答辩演示的隐藏加分项ToolboxOpts里的save_as_image不是装饰。老师问图表能导出吗时现场点工具箱按钮导出 PNG 比任何解释都有说服力。toolbox 还支持data_zoom、restore、data_view组件其中data_view可以弹出图表的底层数据表格这是讲清楚数据从哪来、怎么算的最直接的工具。4.2 饼图与关系图类型占比和导演作品分析饼图适合占比展示但电影类型统计有个经典坑一部电影可能属于多个类型直接统计genre字段会把多类型电影重复计数到多个扇区。正确做法是先拆分再统计from collections import Counter from pyecharts.charts import Pie def genre_distribution_pie(df): all_genres [] for g in df[genre]: all_genres.extend(g.split()) counter Counter(all_genres) pie ( Pie() .add( series_name类型占比, data_paircounter.most_common(8), radius[35%, 60%], label_optsopts.LabelOpts(formatter{b}: {d}%), ) .set_global_opts( title_optsopts.TitleOpts(title电影类型分布 Top8), legend_optsopts.LegendOpts(orientvertical, pos_top15%, pos_left2%), ) ) return pieCounter.most_common(8)只保留前 8 个高频类型避免扇区过多导致图面混乱。radius[35%, 60%]是环形图效果内圈留白区域后续可叠加中心的汇总数字比实心饼图更现代。label_opts的formatter{b}: {d}%是 ECharts 的模板字符串语法{b}为扇区名称、{d}为百分比——这个参数直接决定标签是否可读。如果原数据里genre是用逗号分隔的把g.split()换成g.replace(, ).split()即可。4.3 词云影评关键词提取的中文分词处理词云的数据来源一般是影评文本的分词结果。如果爬虫只拿到了经典台词字段而没有完整影评就用台词合并后分词效果也够。中文分词用 jieba 精确模式import jieba from pyecharts.charts import WordCloud def review_wordcloud(df): text .join(df[quote].tolist()) words jieba.cut(text, cut_allFalse) stop_words {我们, 他们, 没有, 一个, 这部, 还是, 就是} filtered [w for w in words if len(w) 1 and w not in stop_words] counter Counter(filtered).most_common(30) wordcloud ( WordCloud() .add(series_name影评热词, data_paircounter, word_size_range[12, 60]) .set_global_opts(title_optsopts.TitleOpts(title影评关键词词云)) ) return wordcloudjieba.cut的cut_allFalse使用精确模式避免全模式切出大量无意义碎片。停用词列表不能省——我们就是这类词频率高但无分析价值不过滤会全挤进词云中心把有信息量的词挤到边缘。实际项目中停用词表会维护到几十上百个按数据表现逐步补充。4.4 Flask 集成图表接口与前端渲染对接图表对象生成后用dump_options_with_quotes()输出 JSON 配置交给前端。推荐统一走接口前端用同一套 ECharts 初始化逻辑处理所有图表from flask import Flask, render_template, jsonify app Flask(__name__) app.route(/api/charts) def charts_api(): df load_data_from_db() return jsonify({ bar: rating_distribution_bar(df).dump_options_with_quotes(), line: year_trend_line(df).dump_options_with_quotes(), pie: genre_distribution_pie(df).dump_options_with_quotes(), }) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)前端对应的渲染逻辑fetch(/api/charts) .then(res res.json()) .then(data { const barChart echarts.init(document.getElementById(bar-container)); barChart.setOption(data.bar); const lineChart echarts.init(document.getElementById(line-container)); lineChart.setOption(data.line); });dump_options_with_quotes()生成合法 JSON 字符串前端setOption直接消费。用fetch异步加载的好处是首屏不用等全部数据到位图表多时体验差异明显。每个图表对应一个带 id 的 div 容器宽度高度在 CSS 里固定——ECharts 在隐藏或零尺寸容器里初始化会出现 0 宽度渲染这是图表空白最常见的元凶。5. 部署排查与性能优化从本地跑通到答辩演示的最后一公里5.1 数据定时刷新让图表活起来答辩最怕被问数据是不是写死的。把爬虫和刷新机制做成自动化闭环是消除这个质疑的最直接手段。常见做法是用schedule库定时触发爬虫任务import schedule import time def job(): print(f[{time.strftime(%Y-%m-%d %H:%M:%S)}] 开始刷新数据...) raw crawl_top250() df clean_movie_data(raw) save_to_mysql(df) print(数据刷新完成) # 每天凌晨 2 点执行 schedule.every().day.at(02:00).do(job) while True: schedule.run_pending() time.sleep(60)schedule的语义化接口可读性强while True加sleep(60)每秒检查一次任务队列执行频率远超任务本身需要逻辑足够。选凌晨两点是因为目标站点负载低且不影响白天答辩演示。如果老师对工程化要求更高改用APScheduler的BackgroundScheduler集成进 Flask启动应用时自动拉起后台调度不用单独维护进程。5.2 三个高频报错及排查思路答辩前夜最容易爆的坑提前排掉比现场补救靠谱。按出现频率排序报错场景典型信息解决思路爬虫被拒HTTP 403 / 418补全请求头Referer、Accept-Language请求间隔放大到 3–5 秒图表空白页面加载但图表不显示F12 Console 查 JS 报错确认 JSON 合法检查容器 div 是否有固定高度中文乱码入库后页面显示问号建表utf8mb4、连接参数utf8mb4、HTML 声明 utf-8 三处保持一致5.3 答辩演示的小样本快跑开关真到答辩环节现场跑完整 250 条爬虫既耗时又有被限流风险。在config.py里加一个演示开关是最实用的救场技巧# config.py SMALL_MODE True # 答辩演示时设为 True只爬前 2 页 MAX_PAGES 2 if SMALL_MODE else 10爬虫循环从range(0, 250, 25)改为range(0, MAX_PAGES * 25, 25)。演示时开SMALL_MODE一分钟内跑通爬取 → 清洗 → 入库 → 接口刷新 → 图表更新的完整闭环既证明了系统是真实的又不会让老师等得尴尬。这个开关本身也是加分项——它体现你考虑了真实场景的时间约束。另一个实用技巧给图表标题加时间戳副标题比如subtitlef数据更新于 {time.strftime(%H:%M:%S)}。刷新数据后重新请求接口副标题变化老师能直观确认图表是重新计算过而不是静态页面。每次答辩演示前把SMALL_MODE打开、刷新一次数据再进会场基本不会出意外。本文还有配套的精品资源点击获取
返回列表