简介一份面向Python初中级学习者的猫眼电影数据实战源码以猫眼网站为对象完整演示网页抓取、反爬处理、MySQL存储及多维度票房与评分分析流程。包内共14个Python脚本压缩包约13KB脚本角色清晰maoyan.py与font_change.py负责请求与字体反爬maoyan_mysql_1.py、maoyan_mysql_2.py完成数据入库与增量更新movie_score_top10.py、movie_box_office_top10.py分别输出评分榜与票房榜movie_type.py、movie_country_top10.py、movie_month_box_office.py等覆盖类型、国家、月度等分析维度。目前已有176人学习下载适合作为课程设计或练手项目。结合代码可掌握requests解析、MySQL操作、常见反爬策略以及数据聚合展示方法还能基于已有模块扩展新分析维度是提升Python综合能力的实用素材。1. 用 Python 拆解猫眼电影数据的完整链路所谓「猫眼电影网站大数据分析」落到实际工程里通常是从猫眼网页拿票房、评分、上映日期等公开字段清洗之后做排序、分组、趋势统计最后输出成图表或报表。标题里的「TOP」两个字说明核心分析目标是两个电影评分 TOP 和电影票房 TOP。前者偏向口碑排序后者偏向市场表现排序两者结合能回答一个常见业务问题——高分电影是否一定高票房高票房是否一定口碑好。对刚入门 Python 数据分析的人来说这组数据足够干净、结构足够典型既能练爬虫又不至于被反爬折磨到放弃对有经验的人来说也可以用同样的思路验证自己在数据清洗、去重、类型转换和可视化上的工程习惯。下文不依赖任何封装好的「猫眼专用库」只用 requests、pandas、matplotlib 这些通用组件从分析目标倒推每一步要做什么。2. 先确定数据口径与采集策略评分 TOP 和票房 TOP 依赖哪些字段2.1 猫眼页面上评分与票房数据的基本结构打开猫眼电影榜单页能看到的字段至少包含电影名称、主演、上映时间、评分通常显示一位小数、票房单位是万或亿、评分人数。这里有一个关键陷阱页面上的评分是「观众评分」由猫眼用户购票后评价聚合而来和豆瓣评分不是同一套体系票房字段在列表页往往显示为「累计票房」或「实时票房」单位不统一。分析前必须把「评分数值」和「票房数值」从展示字符串里抽出来并转成可计算的数值类型否则排序时会出现「9.9 8.0」这种字符串比较导致的错误结论。对评分 TOP 和票房 TOP 的查询意图来说真正必需的核心字段只有四个title、score、box_office、release_date。其他字段如主演、想看人数可以抓下来扩充分析维度但不是排序必需。采集策略我一般这样定先抓固定榜单页拿到全集再按需求做字段清理如果目标是历史票房 TOP需要额外抓历史票房榜因为实时票房的当日排名和累计票房排名差异很大。猫眼的榜单页通常使用异步接口返回 JSON直接请求 HTML 再解析也是一种办法但接口方式更稳定。2.2 三种采集方式的取舍接口、HTML 解析、第三方库常见做法是优先找页面背后的 XHR 接口。打开浏览器开发者工具切换到 Network 面板刷新榜单页找到返回电影列表的 JSON 请求记下它的 URL 和请求头。这个请求通常比较简单直接构造requests.get就能拿到 JSON 数据。import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://maoyan.com/board } url https://maoyan.com/ajax/xxx resp requests.get(url, headersheaders, timeout10) data resp.json() print(data.keys())这段代码先定义请求头和目标接口地址timeout10防止请求卡死拿到响应后直接调用.json()解析。这里没有给出完整接口地址因为不同时期猫眼前端接口路径会变实际开发时你需要从 Network 面板自己确认。关键在于理解这个过程接口返回的数据结构通常比 HTML 干净字段名是英文短横线风格或驼峰风格解析成本低。如果接口加了签名参数再退回去用requests抓 HTML配合parsel或BeautifulSoup解析。第三方库如maoyan之类的封装不建议用一是维护滞后二是你学不到数据从哪来到哪去的完整链路。2.3 采集时直接处理数据口径问题抓下来的 JSON 或者 HTML 里的字段通常是给前端展示用的不是给数据分析用的。列举几个必须当场处理的点评分可能是字符串9.7也可能缺失缺失值在排行里要单独决定怎么处理票房可能是45.82亿、23.5万或者统一单位需要拆出数值和单位上映日期可能是2023-03-15也可能带星期几存在不规整格式部分电影没有评分猫眼页面显示为「暂无评分」这类记录如果进入排序必须写成函数统一过滤或填充。我建议采集后立刻落到 pandas 的 DataFrame不要在 Python 列表里反复倒腾。DataFrame 的dtype检查能帮你尽早发现「评分数值列被读成了 object 类型」这类问题。import pandas as pd df pd.DataFrame(data) # data 是接口返回的列表 print(df.dtypes)如果score列显示object说明里面混着字符串需要后续做转换。票房列同理。采集阶段只负责把原始数据带回来清洗和结构化放在下一步但「字段口径」这个意识必须从第一行代码就开始有否则等到分析阶段再回头查排错成本会翻倍。3. 数据清洗与统一度量评分排序、票房单位换算与日期归一化的标准操作3.1 清洗流程先转类型再处理缺失最后排序猫眼数据进入DataFrame后第一件事是看每一列的类型和缺失情况不要急着排序。用一个信息总览函数把整体情况拉出来。def inspect_frame(df: pd.DataFrame) - pd.DataFrame: summary pd.DataFrame({ col: df.columns, dtype: df.dtypes.astype(str), null: df.isna().sum().values, sample: [df[col].dropna().iloc[0] if df[col].notna().any() else for col in df.columns] }) return summary print(inspect_frame(df))这个函数生成三列信息字段名、类型、空值数外加每列第一个非空值作为口径参考。看到sample里的字符串你就能判断9.9是三位字符串、45.82亿是数值加单位后续解析函数就有依据。缺失值处理上评分缺失的电影在「评分 TOP」场景里应该剔除票房缺失在票房榜场景里同样剔除但在分析「高分低票房」这类交叉问题时缺失值可以保留成 NaN最后分组聚合时用dropnaFalse单独统计。3.2 评分的数值化与票房单位统一评分的转换比较简单直接用pd.to_numeric加上errorscoerce无法转换的变成 NaN。票房转换需要写一个函数把字符串里的数字和单位拆开统一换算成「亿元」这个标准度量。单位变换必须谨慎亿、万、还有可能直接给纯数字单位字段缺失时默认当作「元」是不合理的通常要看页面展示逻辑。def parse_box_office(value) - float: if pd.isna(value): return float(nan) s str(value).strip() if s.endswith(亿): return float(s[:-1].replace(,, )) if s.endswith(万): return float(s[:-1].replace(,, )) / 10000 try: return float(s) / 100000000 except ValueError: return float(nan) df[box_office_yi] df[box_office].apply(parse_box_office)逻辑说明函数接收原始票房字符串先判断尾部单位亿直接转浮点万转浮点后除以 10000 变成亿纯数字假设单位是元除以 1 亿做转换。这样处理后整个表里票房统一成「以亿为单位」的浮点数排序和柱状图都直接可用。需要注意如果页面本身显示的就是「亿元」且有两位小数「除以 100000000」那一步就会把数值缩小一亿倍这种错误要结合数据源口径判断。3.3 日期归一化与年份维度提取分析电影票房和评分时年份是一个非常重要的维度。上映日期一旦统一成datetime64类型按年份分组就变得很简单。df[release_date] pd.to_datetime(df[release_date], errorscoerce) df[release_year] df[release_date].dt.year这里errorscoerce会把无法解析的日期变成NaT后续按年份聚合时自动忽略。如果你的数据量大可以再取月份做季度分析但 TOP 分析一般到年份就够。日期归一化还有一个好处做「上映首周票房」这类窗口计算时日期是前提条件。3.4 排序与去重的坑排序看似只是sort_values一行但有两个坑必须处理。第一个坑评分相同时票房高的应该排在前面所以排序条件要是多列。第二个坑同一部电影因为采集了多个榜单可能出现重复行去重前必须想好保留哪条。常见做法是把titlerelease_date作为去重键。df df.drop_duplicates(subset[title, release_date], keepfirst) top_score df.dropna(subset[score]).sort_values( by[score, box_office_yi], ascending[False, False] ).head(20) top_box df.dropna(subset[box_office_yi]).sort_values( by[box_office_yi, score], ascending[False, False] ).head(20)逻辑说明先去重再分别做评分 TOP 和票房 TOP。评分 TOP 按评分降序同分时按票房降序票房 TOP 按票房降序同票房时按评分降序。dropna是为了把缺失项排除在排序之外。这里多列排序的ascending参数要传一个列表长度必须和by的列数一致否则 pandas 会报 TypeError。3.5 清洗结果质量检查清洗完别急着画图先做三件事检查排序后前 20 名是不是耳熟能详的电影检查describe看数值分布抽查几条记录的原始字段和转换字段对比。特别是抽查随便挑三五行肉眼比对box_office和box_office_yi能直接发现单位换算错误。下面这段代码打印异常大或异常小的数值。print(df[box_office_yi].describe()) print(df[df[box_office_yi] 100][[title, box_office, box_office_yi]]) print(df[df[box_office_yi] 0.01][[title, box_office, box_office_yi]])describe输出四分位数如果最大值超过 100 亿检查是不是单位换算漏了如果最小值太小检查是不是万被当成元。这个步骤非常重要清洗逻辑正确不代表数据源里没有脏值。4. 分析建模与可视化评分 TOP 与票房 TOP 的交叉分析、年度趋势和差值诊断4.1 评分 TOP 与票房 TOP 的交叉含义两个 TOP 榜单单独看意义有限交叉分析才能回答「口碑和市场是否一致」。把两个榜单按电影名称对齐如果一部电影同时出现在评分 TOP 20 和票房 TOP 20 里说明它叫好又叫座只出现在评分榜说明叫好不叫座只出现在票房榜说明叫座不叫好。用集合运算或者merge都能做这个分析。集合运算适合快速出结论merge适合保留完整字段做进一步统计。score_set set(top_score[title]) box_set set(top_box[title]) both score_set box_set only_score score_set - box_set only_box box_set - score_set print(同时进入双榜, both) print(仅评分榜, only_score) print(仅票房榜, only_box)逻辑说明把两个 TOP 列表的标题转换为集合直接求交集和差集。这里有个隐含问题同一部电影在不同榜单里的标题可能带「」或空格差异比如「你好李焕英」和「你好李焕英 »」这种展示差异会在集合运算里被当成两个名字。稳妥做法是先对标题做「规范化」去掉空格、冒号、特殊符号。import re def normalize_title(t: str) - str: t re.sub(r[:·\-—\s], , str(t)) return t.lower() df[title_norm] df[title].apply(normalize_title)4.2 年度票房与评分的双轴趋势图交叉分析之外年度维度能看出整体市场变化。按年份把票房和评分分别聚合用双轴图展示左侧轴是年度总票房右侧轴是年度平均评分两个趋势线放在一张图里能直观看到某一年电影数量少但口碑高或者票房高但口碑均值下降。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False year_stats df.groupby(release_year).agg( total_box(box_office_yi, sum), avg_score(score, mean), movie_count(title, count) ).reset_index() year_stats year_stats.dropna(subset[release_year]) fig, ax1 plt.subplots(figsize(12, 6)) ax1.bar(year_stats[release_year].astype(int), year_stats[total_box], color#2b6cb0, alpha0.7, label年度总票房(亿)) ax1.set_xlabel(上映年份) ax1.set_ylabel(总票房(亿)) ax2 ax1.twinx() ax2.plot(year_stats[release_year].astype(int), year_stats[avg_score], color#c53030, markero, label平均评分) ax2.set_ylabel(平均评分) plt.title(猫眼电影年度票房与平均评分趋势) plt.show()要点说明groupby后agg同时生成总票房、平均评分和电影数量三个维度双轴图用twinx()共享 x 轴字体设置必须在绘制前完成否则中文标签在 Linux 服务器上会显示成方块axes.unicode_minus是为了让负号正常显示。图中如果平均评分曲线明显平稳而总票房波动剧烈说明口碑和市场的相关性其实不强这正是数据分析要给出的结论。4.3 评分分布与票房分箱分析想看评分和票房的整体关联把票房分成几档统计每档的平均分和电影数量这个分析对「票房高的电影评分是否更高」给出量化答案。bins [0, 1, 5, 10, 30, 100] labels_list [0-1亿, 1-5亿, 5-10亿, 10-30亿, 30亿以上] df[box_bin] pd.cut(df[box_office_yi], binsbins, labelslabels_list) bin_stats df.dropna(subset[box_bin]).groupby(box_bin).agg( count(title, count), avg_score(score, mean) ).reset_index() print(bin_stats)逻辑说明pd.cut把连续的票房数值切成区间bins是区间边界labels对应各区间名称。分组后每个票房档位得到电影数量和该档平均评分。这个结果能直接看出30 亿以上票房的电影平均分是否显著高于 5 亿以下的档位。如果各档平均分差异不大说明评分和票房在统计上不相关。这里需要注意bins列表长度比labels_list多 1cut默认左开右闭最低档从 0 开始刚好能覆盖所有非负值。4.4 用散点图定位异常与离群点双 TOP 分析通常还要回答「哪些是高票房 低评分」的异常情况。散点图的 x 轴是票房y 轴是评分每个点是一部电影两个维度都达到阈值的点会出现在左上或右上区域。fig, ax plt.subplots(figsize(12, 6)) x df[box_office_yi] y df[score] ax.scatter(x, y, alpha0.5, s30) ax.axhline(y9.0, colorgray, linestyle--, linewidth1) ax.axvline(x20, colorgray, linestyle--, linewidth1) ax.set_xlabel(票房(亿元)) ax.set_ylabel(评分) plt.title(票房与评分散点图虚线为阈值参考) plt.show()阈值线axhline(9.0)和axvline(20)是人为划定的观察线不是统计结论。散点图的价值在于一眼看到聚集和离群比如 20 亿票房以上、评分低于 9 的点这些就是「高票房低口碑」的候选对象。用 matplotlib 的annotate可以把判别出来的电影名标在点上但标注文字重叠问题很麻烦我是直接把离群点单独输出成表格。4.5 相关性与交叉表验证量化验证评分和票房的相关性用spearman相关系数比pearson更稳健因为票房数据往往长尾分布不满足正态假设。from scipy.stats import spearmanr valid df.dropna(subset[score, box_office_yi]) corr, pvalue spearmanr(valid[score], valid[box_office_yi]) print(fSpearman 相关系数: {corr:.3f}, p-value: {pvalue:.3g})spearmanr对两个变量的单调关系敏感不要求线性p 值小于 0.05 才说明相关性显著。大多数情况下这个系数会在 0 到 0.3 之间波动说明评分和票房整体相关性弱但高票房头部电影的评分确实不低。这个结论和散点图是互相印证的。如果输出 CSV 报告把相关系数、双榜交叉集合、年度统计三块内容拼成一个文件。with pd.ExcelWriter(maoyan_top_analysis.xlsx, engineopenpyxl) as writer: top_score.to_excel(writer, sheet_name评分TOP, indexFalse) top_box.to_excel(writer, sheet_name票房TOP, indexFalse) bin_stats.to_excel(writer, sheet_name票房分箱, indexFalse) year_stats.to_excel(writer, sheet_name年度趋势, indexFalse)ExcelWriter把多个 DataFrame 写到同一个工作簿的不同 sheetopenpyxl是默认引擎之一如果环境里没有会报 ImportError可以先pip install openpyxl。5. 最后收口把「高评分低票房」和「高票房低评分」识别成可复用的诊断脚本到这一步两个 TOP 榜单和交叉分析都做了但离「可交付」还差一步把分析逻辑固化成脚本。常见做法是写一个find_stock_and_board_mismatches函数输入清洗后的 DataFrame输出两类电影清单一类是评分高但票房未达阈值另一类是票房高但评分低于阈值。这种脚本的价值在于你下次换一个数据源、换一份新数据只要字段名不变就能直接复跑。def diagnose_mismatches(df, score_threshold9.0, box_threshold10.0): score_best df[df[score] score_threshold] box_best df[df[box_office_yi] box_threshold] high_score_low_box score_best[score_best[box_office_yi] box_threshold] high_box_low_score box_best[box_best[score] score_threshold] return high_score_low_box, high_box_low_score hsl, hbl diagnose_mismatches(df) print(高评分低票房数量, len(hsl)) print(hsl[[title, score, box_office_yi]].head(10)) print(高票房低评分数量, len(hbl)) print(hbl[[title, score, box_office_yi]].head(10))这个诊断函数的阈值是硬编码默认参数实际使用时应该结合业务场景调整。比如你想要「口碑潜力片」的候选阈值设在 9.2 分以上、票房 5 亿以下更合理想找「商业烂片」的典型阈值设在票房 15 亿以上、评分 8.5 以下。阈值的选取逻辑本身就是数据分析结论的一部分不是凭空拍的。验证脚本的稳定性有个办法把清洗函数、排序函数、诊断函数连起来跑一遍把打印结果和直接从 DataFrame 手工查的几条记录对比确认无误后把整个流程封装成main()接受 CSV 路径作为参数输出诊断报告。这样后面换新数据一行命令就能输出完整分析。防止采集阶段或字段变化导致的隐性错误运行时打印中间结果的 shape 和dtypes。比如清洗完的df如果行数比原始数据少很多说明去重条件太激进box_office_yi列如果有大量 NaN说明单位解析函数有漏网格式。把这些检查项放进脚本里用assert保证关键字段不为空失败时抛异常而不是带着脏数据继续跑。assert df[box_office_yi].notna().sum() 0, 票房字段解析失败请检查单位格式 assert df[score].notna().sum() 100, 评分数据量不足检查采集是否完整最终交付物可以是一个 CSV 文件加两张图一张评分 TOP 横向条形图、一张票房 TOP 横向条形图外加一张双轴趋势图。画横向条形图时注意中文排序pandas 的plot.barh默认 y 轴从下往上需要invert_yaxis让第一名在上方。这些小细节决定了报告是否像一个真正的数据分析产出而不是一堆代码输出的草稿。本文还有配套的精品资源点击获取