基于Python的猫眼电影数据分析可视化系统全链路实战

发布时间:2026/9/30 0:38:29
基于Python的猫眼电影数据分析可视化系统全链路实战 简介基于Python的猫眼电影数据分析可视化系统毕业设计论文面向计算机相关专业学生及从事数据分析、可视化开发的开发者。系统以requests库获取猫眼电影接口数据通过Pandas完成去重、缺失值及异常值处理再借助Matplotlib与Echarts实现电影评分、票房趋势、类型分布等多维度分析并利用Flask搭建Web系统供直观浏览。文档对系统研究背景、国内外现状、需求分析、系统设计及测试等均有完整论述能够帮助读者梳理毕业设计或项目开发的整体脉络。资源为1个docx文档大小仅3.31MB内容包含中英文摘要、关键词、目录及正文。目前已有382人学习下载对于需要完成同类型电影数据分析课题或快速上手爬虫可视化项目的人员是一份结构清晰、可借鉴性强的参考资料能有效降低系统搭建与论文撰写的前期探索成本。1. 基于 Python 的猫眼电影数据分析可视化系统一条能跑通的全链路项目如果你正在找一份能把「爬虫 → 数据清洗 → 存储 → 分析 → 可视化 → 推荐」串起来的实战项目又不想自己从零摸爬滚打踩三个月的坑那这个基于 Python 的猫眼电影数据分析可视化系统值得好好拆一遍。它不是一个只会画两张图的 Demo而是一套完整的毕业设计级工程用 requests 把猫眼电影数据抓回来Pandas 做清洗和处理Flask 搭出 Web 界面ECharts 和 Matplotlib 负责把评分、票房、类型分布等维度画成直观图表最后还塞了一个基于协同过滤的电影推荐模块。对正在做毕设、想系统入门数据分析、或者打算拿真实电影数据练手的从业者来说这套东西的价值在于它把每个环节都走通了一遍——你能看到数据从网站到页面图表之间的完整流向而不是只拿到一堆零碎的技术点。2. 数据抓回来只是第一步爬虫模块与存储设计2.1 选型思考为什么用 requests 而不是 Scrapy这套系统原文里明确写了用 requests 库发 HTTP 请求这个选择放在毕设场景里其实很合理。requests 是同步请求库代码直观、调试方便配合 BeautifulSoup 解析 HTML 就能完成整个采集流程。Scrapy 虽然支持异步并发、自带调度器和中间件性能上限高但对单个数据源、数据量在万级左右的毕设项目来说它的学习成本反而成了包袱——你得理解 Spider、Item Pipeline、Downloader Middleware 一堆概念才能把最简单的抓取跑起来。我一般会建议如果目标是快速验证数据分析和可视化链路requests BeautifulSoup 完全够用如果要长期维护大规模采集任务或者应对反爬升级频繁的场景再切换 Scrapy 也不迟。这套系统的核心价值不在爬虫本身而是把数据转化成业务洞察爬虫只要能稳定拿到数据工具越简单越好。2.2 爬虫核心代码请求头、会话保持与容错猫眼电影的榜单页面和详情页结构相对稳定直接用 requests 发送 GET 请求就能拿到服务端渲染的 HTML。下面是爬虫模块的核心逻辑采集猫眼 Top100 榜单的电影基础信息。import requests from bs4 import BeautifulSoup import time import random def fetch_maoyan_top100(): 抓取猫眼 Top100 榜单电影信息 返回: [{name, score, release_date, actors, ...}, ...] base_url https://maoyan.com/board/4 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Referer: https://maoyan.com/ } movies [] for page in range(0, 10): # Top100 共 10 页每页 10 条 params {offset: page * 10} try: resp requests.get( base_url, paramsparams, headersheaders, timeout(3, 10) # (连接超时, 读取超时) ) resp.raise_for_status() resp.encoding resp.apparent_encoding # 自动识别页面编码 except requests.RequestException as e: print(f[ERROR] 第 {page1} 页请求失败: {e}) continue soup BeautifulSoup(resp.text, html.parser) items soup.select(.board-wrapper .movie-item) for item in items: name_tag item.select_one(.name a) score_tag item.select_one(.score) star_tag item.select_one(.star) release_tag item.select_one(.releasetime) if not name_tag: continue movie { name: name_tag.get_text(stripTrue), score: score_tag.get_text(stripTrue) if score_tag else , actors: star_tag.get_text(stripTrue).replace(主演, ) if star_tag else , release_time: release_tag.get_text(stripTrue).replace(上映时间, ) if release_tag else , page: page 1 } movies.append(movie) # 控制请求频率随机睡眠 1~3 秒避免触发反爬 time.sleep(random.uniform(1, 3)) return movies这段代码有几个参数值得展开说。timeout(3, 10)表示连接超时 3 秒、读取超时 10 秒如果网络抖动或者目标服务器响应慢请求不会无限挂起。resp.encoding resp.apparent_encoding这行很关键——猫眼页面虽然标记了 charset但实际内容可能混有特殊字符用apparent_encoding让 requests 自动从内容里检测编码能避免中文乱码。random.uniform(1, 3)是对反爬的基础尊重固定频率的请求反而更容易被识别为机器行为。另外resp.raise_for_status()会把 4xx、5xx 响应直接抛成异常配合 try/except 可以保证某页失败时不会终止整个采集流程。这套容错设计是爬虫落地的基本功单页失败要跳过、请求要限速、编码要兜底。2.3 数据落地MySQL 表结构与导入数据抓回来后不能一直躺在内存里需要设计合理的存储结构。原文技术选型部分明确提到了 MySQL这里给出电影信息表的核心设计。字段名类型约束说明idINTPRIMARY KEY AUTO_INCREMENT自增主键movie_nameVARCHAR(255)NOT NULL电影名称ratingDECIMAL(3,1)NULL猫眼评分如 9.5actorsVARCHAR(1000)NULL主演列表release_dateDATENULL上映日期genreVARCHAR(255)NULL电影类型逗号分隔box_officeDECIMAL(12,2)NULL累计票房元created_atDATETIMEDEFAULT CURRENT_TIMESTAMP抓取时间把 DataFrame 直接写入 MySQL 用to_sql()配合 SQLAlchemy 就能实现但要注意if_exists参数设为append而不是replace否则每次跑爬虫都会把全表清空重写。数据量小的时候这种方案没有问题如果后续要支持增量更新建议在movie_name上加唯一索引用INSERT ... ON DUPLICATE KEY UPDATE做幂等写入。from sqlalchemy import create_engine engine create_engine(mysqlpymysql://root:passwordlocalhost:3306/movie_db?charsetutf8mb4) df pd.DataFrame(movies) df.to_sql(movie_info, conengine, if_existsappend, indexFalse)这里charsetutf8mb4必须加上否则遇到 emoji 或者生僻字会报 Incorrect string value 错误。这个坑非常常见后面避坑章节会详细展开。3. 脏数据不过夜Pandas 清洗与预处理实战3.1 清洗流程设计的核心逻辑爬虫拿到的原始数据一定带脏字段为空、格式不统一、存在重复抓取、评分字段带着意外字符。原文描述的清洗思路是「去除重复值、处理缺失值、异常值」实际操作时要按固定顺序处理顺序错了会导致清洗结果不可控。我的固定流程是先读数据并预览 → 去重 → 处理缺失值 → 处理异常值 → 统一数据类型 → 派生新字段。这个顺序是有讲究的。去重要在处理缺失值之前做因为重复记录里可能一条有值一条为空异常值处理要在类型转换之前做否则字符串和数值混在一起pd.to_numeric()会直接报错或者把整列变成 object 类型。3.2 清洗代码去重、缺失值、异常值的具体处理import pandas as pd import numpy as np # 1. 读取原始采集数据 df pd.read_csv(maoyan_movies.csv, encodingutf-8-sig) print(f原始数据量: {df.shape}) # 2. 去重电影名是天然业务主键 df df.drop_duplicates(subset[movie_name], keepfirst) print(f去重后数据量: {df.shape[0]}) # 3. 缺失值处理评分是分析核心字段缺失则删除 df df.dropna(subset[rating]) # 演员、类型字段可能为空用占位符填充不删除记录 df[actors] df[actors].fillna(未知) df[genre] df[genre].fillna(未分类) # 4. 异常值处理评分范围必须在 0~10 df df[(df[rating] 0) (df[rating] 10)] # 票房不为负且去除明显异常的大数比如合计超过同类均值 10 倍的 df df[df[box_office] 0] df df[df[box_office] df[box_office].quantile(0.99) * 10] # 5. 类型转换release_date 转成 datetime方便后续按时间维度聚合 df[release_date] pd.to_datetime(df[release_date], errorscoerce) # 清洗后再删一次to_datetime 转换失败的值会变成 NaT df df.dropna(subset[release_date])代码里值得展开说明的是drop_duplicates(subset[movie_name], keepfirst)——这里subset指定判断重复的依据列keepfirst意味着保留第一条而丢弃后续重复记录。如果原始数据里有同一部电影被爬虫重复抓到但没有更新的需求keepfirst足够如果重复记录中后抓的更全可以用keeplast。异常值过滤用的是「分位数倍数法」而不是固定阈值。固定阈值在数据分布变化时容易误杀或者漏杀分位数倍数对长尾数据更鲁棒。这段是毕业设计级数据清洗里比较实用的处理逻辑写进论文里也站得住脚。3.3 字段加工与派生特征清洗完基础字段后还需要根据分析需求生成派生字段。评分、票房、类型这几个维度虽然原始数据有但直接拿来做分析比较粗糙需要加工出更结构化的信息。# 把 9.5 这种字符串评分转成 float df[rating] df[rating].astype(float) # 票房单位统一为「亿元」方便可视化展示 df[box_office_yi] df[box_office] / 100000000 # 类型字段是逗号分隔的拆分成列表以便后续按类型聚合 df[genre_list] df[genre].str.split(,) # 从上映日期提取年份和月份用于时间趋势分析 df[release_year] df[release_date].dt.year df[release_month] df[release_date].dt.month # 评分分箱将连续评分映射为 0~6、6~8、8~9、9~10 四个档位 bins [0, 6, 8, 9, 10] labels [低分, 中低分, 高分, 神作] df[rating_level] pd.cut(df[rating], binsbins, labelslabels, rightFalse)这里pd.cut()的rightFalse参数表示区间是左闭右开即[6, 8)落入「中低分」而不是(6, 8]。这个细节很容易被忽略但它直接影响分箱结果的边界归属在论文里写上「左闭右开区间」会让评审觉得你对边界有清晰认知。派生字段做完后数据分析模块就可以直接基于这套加工过的 DataFrame 做聚合计算了不需要每次分析都重新清洗一遍。4. 把数据变成会说话的图表Flask ECharts 可视化系统搭建4.1 Flask 应用骨架与路由设计整个系统用 Flask 搭建 Web 层把清洗分析好的数据通过接口吐给前端 ECharts 渲染。原文给出的功能模块包括首页可视化、时间数据分析、评分数据分析、票房数据分析、类型数据分析和词云图可视化对应的路由设计如下。from flask import Flask, render_template, jsonify import pandas as pd app Flask(__name__) # 项目启动时加载一次清洗后的数据避免每次请求都读 CSV df pd.read_csv(cleaned_movies.csv, encodingutf-8-sig) df[release_date] pd.to_datetime(df[release_date]) app.route(/) def index(): 首页展示全量数据的概要指标卡片 total_movies len(df) avg_rating round(df[rating].mean(), 2) total_box_office round(df[box_office_yi].sum(), 2) return render_template( index.html, total_moviestotal_movies, avg_ratingavg_rating, total_box_officetotal_box_office, ) app.route(/api/rating_distribution) def rating_distribution(): 评分分布接口按评分档位聚合统计 dist df[rating_level].value_counts().reindex([低分, 中低分, 高分, 神作]) return jsonify({levels: dist.index.tolist(), counts: dist.values.tolist()}) app.route(/api/box_office_trend) def box_office_trend(): 票房趋势接口按年份聚合累计票房 trend df.groupby(release_year)[box_office_yi].sum().reset_index() return jsonify({ years: trend[release_year].astype(str).tolist(), amounts: trend[box_office_yi].round(2).tolist() }) app.route(/api/genre_distribution) def genre_distribution(): 类型分布接口拆分类型字段后统计占比 genre_series df[genre_list].explode().value_counts() return jsonify({genres: genre_series.index.tolist(), counts: genre_series.values.tolist()})这段代码里值得注意的设计是「启动时加载一次数据」。如果每次接口请求都pd.read_csv()一次接口响应时间会被 IO 拖慢在毕业论文答辩演示时尤其尴尬。数据量在万级以内时内存常驻完全可行如果以后数据量涨到百万级再考虑换 SQL 查询或者加 Redis 缓存。df[genre_list].explode()是一个省事的操作——它能把列表类型的单元格拆成多行。比如一部电影类型是「剧情,爱情」用 explode 后变成两行参与聚合value_counts()就能直接算出各类型出现的频次不用手动循环拼接。这个函数在可视化场景里使用频率非常高。4.2 ECharts 前端对接接口数据与图表配置前端页面用 ECharts 接收接口数据并渲染图表。以评分分布柱状图为例前端 JavaScript 的核心逻辑如下。// 从后端拉取评分分布数据 fetch(/api/rating_distribution) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(ratingChart)); chart.setOption({ title: { text: 电影评分分布按档位 }, tooltip: {}, xAxis: { type: category, data: data.levels // 四个评分档位 }, yAxis: { type: value, name: 电影数量 }, series: [{ type: bar, data: data.counts, // 各档位电影数量 itemStyle: { color: #ff4d4f // 用红色系贴合猫眼品牌色调 } }] }); });接口返回的 JSON 结构是{levels: [...], counts: [...]}前端直接消费这两个数组。这里刻意把数据聚合放在后端做前端只负责渲染——这个取舍对毕设很重要如果让前端拿全量数据自己聚合页面会卡顿而且 JavaScript 里的聚合逻辑不如 Pandas 好写、好解释。答辩时你可以明确说「聚合计算统一在后端 Pandas 完成前端 ECharts 只做呈现职责分离」这是一个很好的评述点。ECharts 的setOption配置项里xAxis.data和series.data必须一一对应否则图表会出现错位。如果后端返回的数据顺序不稳定建议在前端做一次排序再传入。4.3 多维度分析页面票房趋势与类型占比除了评分分布票房趋势和类型分布是另外两个核心分析页面。票房趋势用折线图呈现逐年变化类型分布用饼图体现结构占比。app.route(/api/yearly_box_office) def yearly_box_office(): 年度票房趋势需要合并评分和票房数据 yearly df.groupby(release_year).agg( total_box(box_office_yi, sum), avg_rating(rating, mean), movie_count(movie_name, count) ).reset_index() return jsonify({ years: yearly[release_year].tolist(), total_box: yearly[total_box].round(2).tolist(), avg_rating: yearly[avg_rating].round(2).tolist(), movie_count: yearly[movie_count].tolist() })groupby之后的agg允许你对不同列指定不同聚合方式——对票房求和、对评分求均值、对电影名计数一次分组完成多个指标的计算。这种写法在论文里解释起来简单而且实际分析场景里用的频率极高。前端拿到这个接口的返回值后可以做一个双 Y 轴折线图左轴票房、右轴平均评分一眼看出票房和口碑的相关走势。词云图模块的实现在这套系统里也不算复杂用wordcloud库对电影类型和主演名字生成词云渲染到页面上作为辅助信息展示。词云图对中文显示有特殊要求必须指定中文字体路径否则会出现满屏乱码方块这个细节放到避坑章节细说。5. 部署路上的血泪经验四个高频坑与排查建议5.1 中文乱码爬虫和可视化两层都中招现象爬虫抓回来的电影名和演员名显示乱码或者 ECharts 图表的标题、坐标轴中文显示成方块。原因两层原因叠加导致。第一层是 requests 没有正确识别页面编码resp.text默认使用响应头里声明的编码解码猫眼页面实际编码和声明不一致时中文就炸了。第二层是前端渲染时 ECharts 本身的容器或页面没有声明 UTF-8浏览器用了错误的编码解析 JavaScript 文件。解决爬虫侧统一在请求后执行resp.encoding resp.apparent_encoding前端 HTML 的head里必须写上meta charsetUTF-8。如果 wordcloud 生成词云图乱码那是字体问题——WordCloud(font_path/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc)指定一个系统自带的中文字体路径即可。每次抓数据前先打印一条样本记录确认编码这个习惯能省掉大量返工。5.2 请求被反爬拦截返回错误页面现象爬虫稳定跑一段时间后突然拿不到数据解析结果为空或者响应内容变成验证码弹窗页。原因请求频率超过猫眼反爬策略的阈值IP 被临时封锁也可能是缺少Referer或Accept-Language头导致请求特征和正常浏览器差异太大服务器判定为爬虫。解决第一个办法是控制频率每页之间time.sleep(random.uniform(1, 3))遇到失败时指数退避重试。第二是把请求头补全参考上面代码里的 headers 配置。如果数据量要求不高可以只在凌晨低峰时段跑采集任务这个实战技巧能明显降低被封概率。5.3 MySQL 写入报错 Incorrect string value现象df.to_sql()执行时报错Incorrect string value: \xF0\x9F... for column写入直接失败。原因MySQL 连接字符串里没有指定charsetutf8mb4。MySQL 的 utf8 字符集实际只支持部分 UTF-8 字符遇到 emoji 或生僻字比如某些带特殊符号的片名就会报错。解决建库和连接都强制使用 utf8mb4——建库语句CREATE DATABASE movie_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci连接串里加?charsetutf8mb4。这个坑不踩一次很难注意到踩过之后所有 MySQL 连接我默认都带 utf8mb4。5.4 前端图表数据错位或比例失衡现象柱状图的柱子顺序和预期不符或者饼图占比显示异常。原因value_counts()返回的默认顺序是按计数降序排列不是按业务逻辑排序。比如评分档位的顺序变成了「高分、低分、神作、中低分」在前端直接渲染就错位饼图占比异常则可能是数据里有未清洗掉的重复项或者 NaN 值参与计算。解决后端聚合后强制重排索引比如前面代码里的reindex([低分, 中低分, 高分, 神作])。饼图问题回到清洗环节排查用df[df[genre].notna()]先过滤空值再统计。6. 从会看到会用协同过滤推荐模块的轻量落地与上线技巧6.1 物品协同过滤实现推荐模块是这套系统里最贴近「面向用户」的一层。原文技术选型部分提到了协同过滤算法落地时我用的是基于物品的协同过滤Item-Based CF核心思路是如果电影 A 和电影 B 被同一批用户看过且评分相近那么看过 A 的用户大概率也喜欢 B。实现时数据来源不能再依赖票房榜单需要额外采集用户的评分行为数据构建「用户-电影评分矩阵」。import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 构建用户-电影评分矩阵 # 行是用户ID列是电影ID值是评分未评分的位填 0稀疏场景下常见处理 rating_matrix df.pivot_table( indexuser_id, columnsmovie_id, valuesrating, fill_value0 # 未评分填充 0 便于余弦相似度计算 ) # 计算电影之间的相似度矩阵基于所有用户的评分向量 movie_sim cosine_similarity(rating_matrix.T) # 转置后每一行是一部电影的评分向量 np.fill_diagonal(movie_sim, 0) # 自己与自己的相似度置 0推荐时排除自身 def recommend_movies(movie_id, top_n10): 给定电影 ID返回相似度最高的 top_n 部电影 sim_scores list(enumerate(movie_sim[movie_id])) sim_scores.sort(keylambda x: x[1], reverseTrue) top_movies sim_scores[:top_n] # 把相似度换算成百分比方便前端展示「相似度 87%」之类的信息 result [] for idx, score in top_movies: result.append({ movie_id: int(idx), score: round(float(score) * 100, 1) }) return result这段代码里fill_value0是实际使用里比较常见的简化策略。原始协方差矩阵里缺失的评分用 0 填充会让相似度计算偏保守但好处是代码简单、不需要处理稀疏矩阵的数据结构。如果数据量不大千级电影、万级评分这个方案性能完全扛得住。真正的冷启动问题——新用户没有评分历史、新电影没有用户行为——需要补充基于内容的推荐策略比如按类型匹配推荐同类型高分电影在毕设场景里作为兜底方案展示很加分。6.2 上线技巧定时刷新与容错降级推荐模块上线后最大的问题是数据是静态的——猫眼榜单每天在变用户评分行为也在积累如果数据不更新推荐结果会越来越偏离实际情况。常见做法是写一个定时任务以 Cron 表达式或 Python 的schedule库在每天凌晨 2 点触发全量采集和清洗流程。采集后数据先落临时表校验通过后再替换线上正式表——这个「先写临时再原子切换」的思路能保证任何时刻页面访问到的都是完整数据不会出现读到一半的脏数据。另外要提一个很多人忽略的降级策略如果推荐模块因为数据量不足或者算法异常导致结果为空前端应该优雅降级为「同类类型高分推荐」而不是报错白屏。在 Flask 后端加一个try/except包住推荐函数失败时调genre_based_fallback()返回同类型电影列表这样系统在演示时永远不会出现难看的技术故障。从那以后我每次做类似的数据分析项目都会强制把「数据采集 → 清洗 → 存储 → 分析 → 可视化 → 推荐」这条链路完整走一遍再开始写论文。很多时候不是技术多玄乎而是链条上某一环没打通就卡住了整个流程——比如编码没兜底、清洗顺序反了、MySQL 字符集不对这些都是看似小却足以影响全局的细节。这套系统把每一个环节都给了可运行、可解释的参考实现按着它跑一遍比自己闭门造车省下至少两周时间。希望帮到你。本文还有配套的精品资源点击获取