简介一份基于Python的猫眼电影数据分析可视化系统的毕业设计文档面向数据分析和电影行业学习者旨在解决电影市场数据采集、清洗、分析与展示问题。系统利用爬虫技术自动获取猫眼电影数据再用数据处理工具完成去重、缺失值与异常值处理最后通过绘图库生成可视化图表并借助框架搭建交互式网页。文档包含完整目录、摘要、正文与参考文献帮助理解从数据获取到系统实现的完整流程。压缩包内共有1个docx文件大小3.31MB已有382人学习阅读者可按文档中的技术路线复现项目理解清洗规则与图表配置细节为相关课题研究提供方法参考。该毕业设计亦可作为课程设计或论文写作的模板参考其中数据预处理、可视化分析、系统架构设计等内容提升项目开发与文档编写能力。1. 先定分析指标再写爬虫这个猫眼电影系统的正确打开方式很多人拿到“基于Python猫眼电影数据分析可视化系统”这个题目第一反应是去找爬虫代码把猫眼票房榜的页面抓到本地就开工。这个顺序其实是反的——爬虫只负责把数据搬回来真正值钱的部分在后面的“数据分析”和“可视化”。如果你连“上映时间、评分、票房、类型”这四个字段能分析出什么都还没想清楚那抓回来的数据就是一堆躺在CSV里的数字既没法支撑结论也没法做出能演示的大屏。这个系统的完整链路是这样Python写爬虫采集猫眼电影公开页面的影片数据用Pandas完成去重、类型转换和缺失值处理存进数据库再通过Flask提供数据接口前端用ECharts渲染成评分分布、票房排行、类型占比等图表。通俗讲它解决的是“猫眼上那么多电影到底什么样的片子能拿高分、能卖座”这个问题适合正在做毕业设计、想给自己的求职作品集加一个完整数据项目的同学也适合想从“会调库”走向“能独立完成数据链路”的Python学习者。2. 用Python拿到猫眼电影数据requests加BeautifulSoup的最小方案2.1 为什么不用Scrapy或Selenium也能完成采集猫眼电影榜单页是服务端渲染的页面影片名称、评分、主演、上映时间这些关键字段直接在HTML源码里不需要像抖音评论区那样通过后端接口异步返回。这意味着两件事第一不需要上Selenium模拟浏览器Selenium要额外装浏览器驱动内存占用高猫眼这种页面用普通HTTP请求就能拿到完整数据第二也不需要为了“显得专业”直接上Scrapy框架Scrapy的学习成本和调试成本都要高一截单机限速采集的场景下requests加BeautifulSoup的组合完全够用代码更短、更容易讲清楚。如果你抓的是猫眼的票房榜页面爬虫请求头里真正起作用的是User-Agent和Referer两个字段。User-Agent伪装成真实浏览器避免被单纯的默认Python标识拦截Referer指向猫眼首页模拟从站内跳转过来的访问行为。很多新手只写个User-Agent被限流了还一头雾水实际上在请求头里补上Referer成功率会明显提高。2.2 构建最小可运行的猫眼电影爬虫我一般会把爬虫拆成“请求页面”和“解析数据”两个函数这样即使页面结构调整只需要改解析函数不必动请求逻辑。以下是一个可直接运行的最小版本目标页面是猫眼电影票房榜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, Referer: https://maoyan.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(url: str) - str: 请求猫眼页面返回 HTML 文本 resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text def parse_movies(html: str) - list: 从 HTML 中解析电影字段返回字典列表 soup BeautifulSoup(html, html.parser) movies [] for dd in soup.select(dd): title_tag dd.select_one(.movie-title) # 片名 score_tag dd.select_one(.movie-score) # 评分 date_tag dd.select_one(.movie-date) # 上映时间 if not title_tag: continue movies.append({ title: title_tag.text.strip(), score: float(score_tag.text.strip()) if score_tag else None, release_date: date_tag.text.strip() if date_tag else None, }) return movies这段代码的逻辑很直白fetch_page负责向目标URL发起GET请求timeout10是连接超时超过10秒没响应就让请求失败而不是无限期卡住resp.encoding utf-8是关键猫眼的中文页面如果不手动指定编码requests会根据响应头推断有时会推断成ISO-8859-1导致乱码。解析时用.select(dd)拿到榜单里每一部电影的父节点再从各个子节点里提取片名、评分和上映时间。这里选择器要根据实际页面结构调整但思路是通用的。运行这个爬虫需要先安装两个依赖库在终端执行pip install requests beautifulsoup42.3 翻页、限速与断点续抓三个让爬虫不出错的细节票房榜页面只有一页但如果你扩展去抓“全部榜单”或者按年份筛选的列表页就涉及翻页。猫眼的翻页URL是有规律的通常通过offset参数控制每页起始位置比如offset0是第一页offset10是第二页。抓取时用循环生成URL即可base_url https://maoyan.com/board/1?offset{} for offset in range(0, 30, 10): # 抓 3 页每页 10 条 url base_url.format(offset) html fetch_page(url) movies parse_movies(html) print(foffset{offset}抓到 {len(movies)} 条) time.sleep(2 offset / 10) # 限速随翻页递增间隔这里的time.sleep(2 offset / 10)是个小技巧——请求间隔随页数递增而不是固定值。固定2秒的间隔很容易被识别成脚本行为递增间隔模拟的是“人工浏览时越翻越慢”的特征。洗数据时你会感谢这个限速的请求过快导致被临时限流返回的页面里电影列表是空的解析出的数据全是空列表那才是真正的翻车现场。断点续抓的逻辑更简单每次请求前先检查数据库里这条电影标题是否已存在存在就跳过。这样即使爬到一半程序崩了重跑时不会重复抓已入库的数据也给后续的增量抓取留了接口。提示解析页面时优先用select_one而不是find_all因为你只需要每个电影节点下的第一个匹配元素select_one取不到时返回None配合空值判断不容易报错。3. 数据清洗与存储决定可视化上限的第一步3.1 选SQLite还是MySQL从毕设到生产的存储策略很多教程一上来就让你装MySQL但这对于猫眼电影这个体量其实是过度设计。清洗后的数据量撑死几百条到上千条SQLite一个单文件就能搞定零配置、免安装、Python内置sqlite3模块直接操作。如果你只是做课程设计或毕业设计SQLite足够撑起整个系统还能省掉环境配置这一大截踩坑时间。什么时候换MySQL当你的数据量超过几万条、要支持多人并发读写、或者未来要接入其他业务系统的时候。SQLite对并发写支持弱两个脚本同时写一个文件会报database is locked。我的建议是先按SQLite把系统跑通在代码里用SQL语句操作之后迁移MySQL只需要改连接方式SQL基本可以原封不动。建表语句如下字段覆盖了分析所需的核心维度CREATE TABLE IF NOT EXISTS maoyan_movies ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL UNIQUE, score REAL, release_date TEXT, box_office TEXT, genres TEXT, actors TEXT, comment_count INTEGER, crawled_at TEXT DEFAULT CURRENT_TIMESTAMP );3.2 Pandas清洗流程去重、类型转换、缺失值处理数据抓回来是JSON数组或CSV直接拿来画图会暴露一堆问题片名重复、“2019-01-01”这种日期是字符串没法按年份聚合、评分字段里有空值、票房有“20.5亿”这种带单位没法比较大小。这些都得在进入数据库之前或从库里读出来之后用Pandas处理掉。下面是一段标准的清洗流程我按照实际项目经验把每个步骤的意图写清楚了import pandas as pd # 从 SQLite 读取原始数据 conn sqlite3.connect(maoyan.db) df pd.read_sql(SELECT * FROM maoyan_movies, conn, parse_dates[release_date]) # 1. 去重同一部电影可能出现在多个榜单里 df df.drop_duplicates(subsettitle, keepfirst) # 2. 类型转换票房列 20.5亿 - 20.5数值单位统一为亿 df[box_office] ( df[box_office] .astype(str) .str.replace(亿, , regexFalse) .str.replace(万, 0.0001, regexFalse) .astype(float) ) # 3. 缺失值处理评分为空的电影单独标记不直接删掉 df[score_is_missing] df[score].isna().astype(int) # 4. 新增分析辅助字段 df[release_year] df[release_date].dt.year df[score_round] df[score].round(1) # 5. 保存清洗结果 df.to_csv(cleaned_maoyan.csv, indexFalse, encodingutf-8-sig)逻辑说明drop_duplicates(subsettitle)按片名去重因为一部电影可能同时出现在“票房榜”和“口碑榜”里直接全量去重会把有效数据丢掉str.replace(万, 0.0001)这步是为了统一单位把万转换成亿的小数形式这样“20.5亿”和“8000万”可以直接比较大小评分的缺失值我选择用额外标记列保存而不是直接删除因为缺失评分的电影可能是新上映还没来得及开分这类电影在分析“类型与评分关系”时应该被排除但在统计上映数量时需要保留。这里有个容易被忽略的参数encodingutf-8-sig。如果写成普通的utf-8生成的CSV用Excel打开时第一列的列名会带一个看不见的\ufeff字符你拿Excel做二次筛选时会发现“片名”列排序总是排不对。utf-8-sig会在文件头部写入BOM标识Excel就能正确识别编码了。3.3 清洗前后对比你的数据能回答什么问题清洗前后的差别主要体现在三个地方。第一是可聚合性清洗前“2024-12-01”是字符串按年分组得写正则去截取清洗后有了release_year字段df.groupby(release_year)[score].mean()一行就能算出年度平均分。第二是可比性票房带“亿”和“万”没法排序统一成亿以后df.nlargest(10, box_office)直接取票房前十。第三是健壮性空评分如果留着不管画图时ECharts会把null渲染成断档整个图表看起来就是残缺的提前填充或者打标记图表就正常了。判断清洗是否合格的标准也很简单把清洗后的数据打印成表格盯着看一分钟每一列的值你都能读懂、能说出单位、能判断它适合聚合还是适合筛选。如果一眼看到某个值看不懂那说明数据还没洗到位画图阶段一定会翻车。4. 分析维度与可视化Flask接口加ECharts图表的组合4.1 用Flask把Pandas分析结果变成接口可视化系统的常规做法是前端页面通过AJAX请求后端接口获取数据后端把Pandas分析的结果转成JSON返回。这样做的优势是前后端逻辑分离——图表要改样式不用动后端的计算逻辑分析逻辑要调整也不用动前端页面。下面是一个最小的Flask接口示例返回评分分布数据from flask import Flask, jsonify import pandas as pd app Flask(__name__) def load_clean_data(): df pd.read_csv(cleaned_maoyan.csv) return df app.route(/api/score_distribution, methods[GET]) def score_distribution(): df load_clean_data() # 按评分区间分组统计 bins [0, 5, 6, 7, 8, 9, 10] labels [0-5, 5-6, 6-7, 7-8, 8-9, 9-10] df[score_bin] pd.cut(df[score], binsbins, labelslabels, rightFalse) result ( df.groupby(score_bin, observedFalse) .size() .reindex(labels, fill_value0) .to_dict() ) return jsonify(result) if __name__ __main__: app.run(debugTrue, host127.0.0.1, port5000)这个接口的逻辑里有个参数值得注意rightFalse表示区间左闭右开即[5, 6)计入“5-6”而不是“6-7”这样评分为6.0的电影不会在分组边界上产生歧义。observedFalse关闭Pandas的Category类型聚合优化确保分组的顺序和labels一致。reindex(labels, fill_value0)也很关键如果一个评分区间里没有电影Pandas默认会跳过这个组前端的柱状图就会出现缺柱子的问题reindex补齐后在接口里返回的就是包含0值的完整分布。启动Flask后需要安装依赖命令是pip install flask pandas。建议把debugTrue只在开发阶段打开正式演示时关闭否则浏览器访问出错的页面会直接暴露本地代码路径这在不熟悉系统的人面前有些丢分。4.2 ECharts渲染评分分布与票房TOP10前端部分用原生的HTML加ECharts不需要引入Vue或React就能完成大屏展示。ECharts用CDN方式引入是最快的但如果你在局域网环境演示建议把echarts.min.js下载到本地static目录避免演示现场没有外网导致图表渲染不出来。评分分布的柱状图配置如下!DOCTYPE html html head meta charsetUTF-8 title猫眼电影数据分析可视化/title script srcstatic/echarts.min.js/script /head body div idscoreChart stylewidth:100%;height:400px;/div script fetch(/api/score_distribution) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(scoreChart)); const sortedKeys [0-5, 5-6, 6-7, 7-8, 8-9, 9-10]; const values sortedKeys.map(key data[key] || 0); chart.setOption({ title: { text: 电影评分分布 }, tooltip: { trigger: axis }, xAxis: { type: category, data: sortedKeys }, yAxis: { type: value }, series: [{ type: bar, data: values, itemStyle: { color: #5470c6 } }] }); }) .catch(err console.error(err)); /script /body /html关键参数说明xAxis.type设为category表示x轴是离散的文本类别评分区间这种数据必须用类别轴yAxis.type设为value表示y轴是数值轴Pandas算出的计数是数值类型能直接被柱状图消费。data.map(key data[key] || 0)这个写法是为了兜底——接口返回的JSON里如果有缺失的键前端默认填充为0避免图表出现空白柱子。票房TOP10改成柱状图是类似的套路唯一区别在数据源。前端请求/api/box_office_top10后端返回按票房降序排列的前10部电影的名称和票房值前端把xAxis.data换成电影名数组series.data换成票房数字数组即可。4.3 让大屏更像大屏的细节与多维度联动一个能站得住的“可视化系统”不能只靠一张柱状图硬撑。我建议在首页布局四块内容左侧是票房TOP10柱状图中间是评分分布柱状图右侧是类型占比饼图底部放一个按上映年份筛选的时间轴。四块图表联动逻辑是点击饼图中的“喜剧”类型评分分布图只显示喜剧电影的评分分布。实现联动不需要后端改接口前端拿到全量数据后自己过滤就行。给饼图绑定点击事件是常见做法myPieChart.on(click, params { const selectedGenre params.name; fetch(/api/score_distribution?genre${encodeURIComponent(selectedGenre)}) .then(res res.json()) .then(data { scoreChart.setOption({ series: [{ data: buildSeriesData(data) }] }); }); });这里的encodeURIComponent尤其不能漏掉——类型名可能是“喜剧”“动作/冒险”这类带中文和分隔符的字符串不编码的话URL会格式错误Flask后端通过requests.args.get(genre)取到的也可能被截断。后端按类型过滤时用df[df[genres].str.contains(genre, naFalse)]这种模糊匹配比精确匹配更实用因为猫眼上很多电影是复合类型一个电影可能同时标注“喜剧”和“爱情”。5. 避坑记录猫眼反爬、编码错乱与图表失真的五个现场5.1 高频请求被限流页面返回空数据现象爬虫前几页数据正常翻到某一页后parse_movies返回的列表长度突然为0打印抓回来的HTML发现里面没有dd节点。原因请求频率超过了猫眼服务端的限流阈值服务端返回的页面里故意去掉了电影列表数据只保留空壳页面结构。解决加随机延时是第一步time.sleep(random.uniform(1, 3))比固定2秒更难被识别。第二步是准备一个User-Agent池把桌面Chrome、移动端Safari等常见标识都放进去每次请求前用random.choice随机选一个。第三步是控制抓取总量单次任务抓取不超过200条抓完一轮等一小时再抓下一轮。这三个手段叠加后被抓回去的数据基本就能稳定入库了。5.2 评分和票房字段解析出来是空值现象片名、上映时间都能解析出来但score字段是Nonebox_office字段根本找不到节点。原因猫眼的页面结构在各个榜单页面并不统一。票房榜的评分在.movie-score里但口碑榜的评分换成了.star-score实时票房数据是JavaScript异步加载的服务端HTML里根本没有这个字段。解决先确认你抓的是哪个榜再去看对应页面里需要解析的字段实际长什么样。抓取前先写一行探测代码打印页面上所有包含“score”的class名再决定选择器。如果目标就是实时票房用Selenium加渲染最省心但代价是抓取速度降到每秒一条左右。另一个思路是完全避开需要动态加载的接口只抓后端HTML里静态存在的字段清洗阶段把空值统一打上缺失标记。毕竟“评分、上映时间、类型”这些字段已经能支撑大部分分析维度了不必非抓实时票房不可。5.3 SQLite插入报错unique constraint failed现象第二遍运行爬虫插入数据时报sqlite3.IntegrityError: UNIQUE constraint failed: maoyan_movies.title程序直接中断。原因建表时给title加了UNIQUE约束重复抓取同一个榜片名重复是必然事件程序没有做重复数据跳过。解决插入语句用INSERT OR IGNORE代替INSERT重复片名会被SQLite静默忽略程序得以继续执行。这个语法是SQLite特有的MySQL不支持但改成普通MySQL的写法也简单先SELECT COUNT(*)判断是否存在再决定插入。我建议爬虫入口处加一个按月或按天的日志表每次抓取前检查当天是否已抓过从源头上减少重复抓取的浪费。5.4 年份排序错乱2024排在了2023前面现象按release_year统计不同年份的平均评分图表的年份顺序是2024、2021、2023、2022这种乱序。原因release_year字段是从release_date字符串里截取的前四位存进Pandas后是字符串类型字符串排序按字典序2024小于2023不对“2024”的字典序确实大于“2023”但如果还有其他年份比如“1999”字符串排序结果里“1999”会排在“2023”之前看起来毫无规律。解决在生成release_year时直接转成整数df[release_year] df[release_date].dt.year。.dt.year返回的就是整数类型不会出现字典序问题。如果是自己截取的字符串用.astype(int)强制转换。这个坑在“按年份分析”的场景里极其常见算是Pandas字符串类型的老朋友了。5.5 ECharts图表不渲染或柱子消失现象接口返回的数据明明有值但图表区域一片空白控制台里看到NaN或null警告。原因清洗后的数据里有NaNPandas转JSON时NaN会被序列化成NaN字符串ECharts不认识这种非标准JSON值图表渲染直接失败。解决在接口返回前一步用df.fillna(0)把数值字段的空缺补上或者用df.dropna(subset[score])把缺失评分的行删掉后再聚合。需要注意的是Pandas转JSON时还涉及中文编码问题jsonify默认是能正确处理中文的不需要额外配置。如果你是用手动json.dumps序列化记得加ensure_asciiFalse否则中文会变成\uXXXX转义序列前端看着是一堆乱码。6. 增量更新与可视化演示把项目从“能用”变成“能讲”到一个阶段后基础链路已经跑通了爬虫出数据、Pandas洗数据、Flask出接口、ECharts出图表。这时候再往前一步三个技巧就能让整个系统在演示和答辩时明显不同。第一个技巧是增量更新。全量重抓每次要跑几分钟演示时如果等抓完再打开页面整个现场会陷入尴尬的等待。常见做法是建一个crawl_log表记录每次抓取的时间范围和抓取条数程序启动时先读日志判断当天是否已抓取已抓取就直接从数据库读清洗后的数据未抓取则只抓当天新增的电影控制全程耗时在几十秒内。第二个技巧是给图表加上尝试解释数据的能力。比如评分分布图里如果某个分数段有异常高点可以在图表下加一行动态计算的文字说明告诉观众“8-9分段占比最高达到42%”。前端拿到分布后自己在JavaScript里算一遍比例然后用document.getElementById(summary).textContent写进页面。这种“图表显示发现问题、文字帮你总结问题”的组合比单独一张图更容易让人理解你的数据思路。第三个技巧是给接口加慢查询日志。Flask的before_request钩子里记录每个接口的耗时如果某个接口超过500毫秒说明数据清洗或SQL查询有优化空间。常见的优化点是聚合查询时给release_year字段建索引几百条数据看不出差别数据量上去后这步就是分水岭。我自己的习惯是演示前一天把所有页面跑一遍看接口耗时是否稳定有问题当场修不给现场留翻车机会。这套猫眼电影数据分析可视化系统的开发过程中最深的体会是数据分析项目不在于用了多复杂的模型而在于每一步数据管线的思路是否清晰。从定指标、写爬虫到清洗入库再到接口与图表联动每个环节都有明确的产出和验收标准。项目做完以后把启动命令整理成一行bash脚本录个一分钟的演示视频放在简历的项目列表里比空写“熟悉Python数据分析”要有说服力得多。希望这些踩坑记录和调试习惯能帮你少走几步弯路也希望你做完这个系统后能自己走一遍“定指标、抓数据、出结论”的闭环——毕竟这中间的判断力才是数据分析工作里最值钱的东西。本文还有配套的精品资源点击获取