简介基于Python构建的猫眼电影数据爬取、分析与可视化综合项目主要面向Python学习者、数据分析入门者及需要完成期末大作业的高校学生。项目以猫眼电影为素材完整覆盖数据爬取、清洗存储、统计分析及可视化展示全流程能够直接运行并产出分析结论。压缩包共42个文件以Python源码为主包含py/pyc脚本文件、前端页面html/js、SQL数据脚本、配置文档及readme说明整体大小仅428KB结构紧凑且目录层级清晰。截至目前已有1984人学习下载适合作为课程设计或期末项目的参考模板。项目带有详细的代码注释与开发文档基础薄弱者也能轻松跟上同时模块化设计便于二次开发可替换数据源或调整展示维度。从接口、模型到视图的完整代码组织也能帮助读者快速理解Web数据项目的基本架构是一份源自期末大作业且获得97分高分评价的优质参考。1. 期末大作业选题猫眼电影爬虫、分析和可视化一次全打通期末大作业选Python方向最怕的是技术点很多但凑不成一个完整项目。猫眼电影Top100榜单是一个很适合收口的练手数据源网页版是静态HTML不用抓包模拟App接口字段包含片名、主演、上映时间、评分信息密度刚好够做分析和可视化反爬强度对高频请求有反馈又能用UA和延时绕开足够向老师展示爬虫基本功。最难的不是把requests请求发出去而是数据到手后才发现需要清洗、类型转换、处理乱码这些步骤加起来才算完成度高的作业。下面按我实际做这类作业的顺序展开先把猫眼Top100榜单爬下来再清洗成干净的DataFrame做几个能讲出故事的分析维度最后给出一套避坑清单和答辩加分技巧。适合已经学完Python基础、想用requests、pandas和matplotlib交一份能现场演示大作业的同学。不需要IP池不需要分布式方案一台电脑、一个IDE、几次sleep就能跑通。2. 抓取猫眼榜单数据确定页面结构、写正则、处理反爬2.1 打开页面先看HTML爬虫里最不该跳过的步骤写爬虫最容易翻车的点是拿到网上代码直接跑跑不通就说“被反爬了”。实际上很多爬虫跑不通是因为页面结构和你用的选择器不匹配。先说猫眼榜单页的特征比较好用的Top100榜单页是服务端渲染的页面打开后在浏览器里右键“查看网页源代码”能在HTML里直接看到电影条目不需要去XHR接口里翻JSON。每个电影条目在HTML里是一个节点里面包含排名、片名、主演、上映时间、评分。为什么强调先看源码因为猫眼页面改过版不同时间段打开标签class可能不一样。正确做法是先把榜单页保存成本地HTML然后用编辑器检查一个电影条目的结构再决定正则怎么写。常见页面版本里字段和页面标注特征对应关系如下字段页面特征说明排名board-index榜单序号从1到100片名a标签的title属性同时可以取到电影详情页链接主演class为star的段落多个人名用顿号分隔上映时间class为releasetime的段落文本以“上映时间”开头评分integer与fraction两个标签整数部分和小数部分被拆开我一般会写一个临时小脚本把第一页响应存下来避免在终端里只看到半屏HTMLimport requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } url https://maoyan.com/board/4?offset0 resp requests.get(url, headersheaders, timeout5) with open(maoyan_page.html, w, encodingutf-8) as f: f.write(resp.text) print(resp.status_code, len(resp.text))这段代码的逻辑很简单先请求第一页把响应保存为HTML。resp.text是requests解码后的文本写文件时再指定utf-8后面用编辑器打开中文不会乱。很多同学在这里直接打印HTML结果终端缓冲区太小只能看到半屏内容无法判断结构。把响应存下来再打开是最稳妥的做法。参数说明timeout5表示超过5秒没有响应就抛异常避免程序一直挂起UA是模拟Chrome浏览器的标识不带这个字段请求部分站点会直接返回403。第一次请求成功后再打开maoyan_page.html看结构确认每个字段在什么标签里。2.2 用正则抓取Top100榜单一条正则吃下六个字段确认页面结构之后就可以写解析函数。这里用正则而不是BeautifulSoup不是说BeautifulSoup不好而是很多课程在爬虫章节刚好教到正则而且榜单页HTML结构比较规整一条正则匹配多个字段是可行的。下面是一种经过验证的写法注意正则里每个括号都要和字段一一对应import re import time import requests HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } movie_pattern re.compile( rdd.*?board-index-\d(\d)/i.*? ra href/films/\d title(.*?).*? rp classstar(?:主演)?(.*?)/p.*? rp classreleasetime(?:上映时间)?(.*?)/p.*? ri classinteger(.*?)/ii classfraction(.*?)/i, re.S ) def fetch_html(offset): url fhttps://maoyan.com/board/4?offset{offset} resp requests.get(url, headersHEADERS, timeout5) resp.raise_for_status() return resp.text def parse_movies(html): movies [] for item in movie_pattern.findall(html): movies.append({ rank: int(item[0]), title: item[1].strip(), actors: item[2].strip(), release_date: item[3].strip(), score_int: item[4], score_frac: item[5] }) return movies这段代码里有几个关键点。第一正则里的和.?之间没有class属性只要能定位到条目块即可不关心有没有额外属性。第二a href/films/\d用\d匹配电影ID如果页面改版后a标签的属性顺序变了这段正则就会失效这也是2.1里强调先存HTML的原因。第三re.S让.能匹配换行符因为内部跨多行不加上这个标志.?到不了下一行。参数说明findall返回的是元组列表item[0]到item[5]分别对应正则里的六个括号rank用int()转成整数方便后续排序strip()把字符串前后的空格清掉。fetch_html里的raise_for_status()让请求遇到4xx、5xx时主动抛异常而不是默默返回一个错误页面否则后面的正则匹配到空列表排查起来很费劲。循环抓取10页的代码放在主函数里if __name__ __main__: all_movies [] for page in range(10): offset page * 10 html fetch_html(offset) movies parse_movies(html) all_movies.extend(movies) print(f第{page 1}页完成累计{len(all_movies)}条) time.sleep(1)页面逻辑Top100榜单按offset分页每页10条第1页offset0第2页offset10一直到第9页offset90。time.sleep(1)是每抓完一页休息1秒既减少对目标站点的压力也降低被反爬的概率。如果只抓100条这个速度大概10秒左右对期末作业完全够用。2.3 反爬处理先从UA和Referer开始说到反爬很多同学第一反应是上代理IP池其实对猫眼榜单这个场景完全没必要。最常见的反爬反馈是请求返回200但响应正文里找不到电影条目或者直接被重定向到安全验证页面。这个时候先把UA补上再把Referer带上往往就能解决。Referer表示请求是从哪个页面跳转过来的爬虫里加上这个头更像浏览器行为HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Referer: https://maoyan.com/board }如果加了这两项仍然返回空数据需要确认响应是不是验证页。快速定位的办法是打印响应前200个字符html fetch_html(0) print(html[:200])看到里面出现“校验”“验证”之类的字眼就说明被反爬拦住了。此时不要硬着头皮调正则先降低请求频率把time.sleep改到2秒再等一两分钟重新跑。如果还是被拦检查是否本机IP被临时限流换个网络环境重试即可。不需要把问题上升到对抗层面期末作业的关键是把流程走通而不是证明能爬多少量。这里还有一个容易被忽略的点requests默认不带浏览器的Accept-Language头某些站点会根据语言头返回不同版本页面。稳妥起见可以把浏览器常用头都复制过来但别在代码里硬编码一堆静态头看起来反而像自动化痕迹过重。我一般只带UA和Referer保持克制这也是一种低姿态抓取。3. 数据清洗与存储把抓下来的字符串变成能分析的DataFrame3.1 先把列表转成DataFrame并检查数据质量爬下来的all_movies是一个字典列表直接交给pandas就能变成DataFrame。但很多同学在这一步直接做表然后马上画图结果图很怪。必须先摸清数据质量。首选动作是import pandas as pd df pd.DataFrame(all_movies) print(df.head()) print(df.info()) print(df.describe())df.info()能看到每一列的非空数量和数据类型df.describe()能看到rank字段的分布。此时还要确认是不是真的有100条记录、有没有重复电影。如果条数不够先回到爬虫部分检查是否每页都抓到了10条。数据质量检查不是一个额外的好习惯而是画图前必要的体检。一个常见的意外是刚抓下来的df里score_int和score_frac都是字符串画图时pandas直接把它们当分类变量处理。类型转换放3.2这里先处理重复值df.drop_duplicates(subsettitle, inplaceTrue) print(df.shape)以片名作为去重键。如果去重后数量少于100说明爬虫阶段有重复请求或者页面本身重复。此时需要回看2.2的输出日志确认是解析导致重复还是页面数据本身重复。3.2 字段清洗合并评分、转换日期、切分演员# 合并且转换评分解析失败的变成NaN score_str df[score_int].fillna() . df[score_frac].fillna() df[score] pd.to_numeric(score_str, errorscoerce) # 上映时间转成datetime非法的变成NaT df[release_date] pd.to_datetime(df[release_date], errorscoerce) df df.dropna(subset[score, release_date]) # 从日期里提取年份后面年份分析要用 df[year] df[release_date].dt.year # 演员字段去掉首尾空格并转成列表 df[actors] df[actors].str.strip() df[actor_list] df[actors].str.split(、)这段代码有几个需要注意的参数。第一个是pd.to_numeric和errorscoerce它把“9.”“5”这种字符串转成9.5遇到无法解析的值变成NaN后续再dropna。这样做比直接astype(float)安全因为astype遇到空字符串会直接报错而to_numeric不会。第二个是to_datetime里的errorscoerce即使页面里某个上映时间是空文本程序也不会中断。关于演员字段的拆分我用中文顿号作为分隔符这是榜单主演字段的默认格式。def split 如果某个条目只有一个演员split后也能生成含一个元素的列表。这里不要用英文逗号分割因为逗号分隔后会有多余空格还得再strip一次。3.3 存储到CSV和SQLite让作业在答辩时随时可复现数据清洗完就要落盘。存储目标有两个一是生成一份随时可查看的CSV方便在pandas之外用Excel人工核对二是存SQLite答辩时不管爬虫还能不能跑通数据都还在。代码df.to_csv(maoyan_top100.csv, indexFalse, encodingutf-8-sig) import sqlite3 conn sqlite3.connect(maoyan.db) df.to_sql(movie, conn, if_existsreplace, indexFalse) conn.close()重点说encode参数。很多同学第一次用encodingutf-8然后用Excel打开CSV发现中文乱码开始怀疑爬虫编码写错了。实际上CSV是UTF-8编码Excel默认按GBK去解码所以要用utf-8-sig。utf-8-sig会在文件开头加入BOM标记Excel看到BOM就知道这是UTF-8编码。这个参数对期末作业很重要因为老师大概率会直接用Excel打开你的CSV检查。SQLite存储说明to_sql要求sqlite3连接表名为movieif_existsreplace表示表已存在就覆盖适合重复运行。需要把数据读回来时用pd.read_csv(maoyan_top100.csv)或pd.read_sql(select * from movie, sqlite3.connect(maoyan.db))。到这里一个干净的数据集就齐了。4. 数据分析从三个维度把Top100榜单讲成一个故事4.1 评分分布高分是常态还是少数猫眼Top100榜单本身是一个高分榜单所以评分分布和全量电影不一样。分析时不能只给一个平均值还要把分布形状讲清楚。用直方图最直观import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] False df[score].plot(kindhist, bins20, edgecolorwhite) plt.title(猫眼Top100电影评分分布) plt.xlabel(评分) plt.ylabel(电影数量) plt.show()bins20表示评分区间分成20段。如果分数集中在9分上下直方图会出现尖峰如果分散分布就平缓。关键是要结合describe()补充文字说明比如均值和中位数、最低和最高分。不要只贴图答辩时要能说出“榜单里的电影大部分落在哪个区间”这句话。为了让结论更清晰可以再做一个分档统计df[score_bin] pd.cut(df[score], bins[0, 7, 8, 9, 10], labels[7分, 7-8分, 8-9分, 9分以上]) print(df[score_bin].value_counts())pd.cut把连续分数切成几档value_counts()统计每档数量。这个表格放在PPT里比直方图更好解释。参数说明bins边界左开右闭0到7分一档7到8分一档9分以上一档。如果榜单里大量电影在9分以上那说明Top100的筛选标准确实高。4.2 年份维度榜单里老片和新片各占多少分析时用year字段做统计。代码year_count df[year].value_counts().sort_index() year_count.plot(kindbar, figsize(12, 4)) plt.title(榜单电影上映年份分布) plt.xlabel(年份) plt.ylabel(数量) plt.show()value_counts()统计每个年份出现的次数sort_index()按年份排序这样横轴才是连续的时间轴。如果榜单跨度特别大比如从1990到2024大多数年份只有一两部柱状图比较碎。这种情况可以按五年区间聚合df[year_bin] (df[year] // 5) * 5 year_bin_count df[year_bin].value_counts().sort_index() year_bin_count.plot(kindbar) print(year_bin_count)(year // 5) * 5先把年份除以5取整再乘回5得到所在五年段的起始年份。比如1993年落在1990-1994这一段结果就是1990。这种分箱方法比手动写if判断简洁调粒度也方便想每10年一段把5改成10即可。分析时要写清边界榜单是当前热度排行的经典片单不代表某一年电影质量突然变高。在报告和PPT里要写清楚这是榜单样本的分布不是整体市场趋势。4.3 演员维度谁的名字在Top100里出现最多主演字段是“主演A、B、C”格式清洗时已经拆成了actor_list现在需要展开成一行一个演员actor_series df[actor_list].explode() actor_count actor_series.str.strip().value_counts().head(15) print(actor_count)explode()是pandas把列表元素拆到多行的函数拆完后一个包含三个演员的电影会变成三行。value_counts()统计每位演员出现次数head(15)取前15名。由于不同演员可能同名这个统计只能做粗粒度展示不能作为严谨的学术结论但作为期末作业的可视化维度足够。画图actor_count.sort_values().plot(kindbarh, figsize(8, 8)) plt.title(演员上榜次数Top15) plt.xlabel(上榜次数) plt.ylabel(演员) plt.show()用barh画横向条形图演员名字在纵轴不会因为名字太长导致横轴标签重叠。sort_values()先把数据升序最长的条形在下方视觉上更像一个排行榜。再补一个评分与年份的关系作为分析部分的收尾print(df[[year, score]].corr()) df.plot.scatter(xyear, yscore) plt.title(年份与评分散点图) plt.show()corr()输出相关系数矩阵如果相关系数接近0说明年份和评分没有明显线性关系。这种情况下结论就是“榜单中高分电影不受年份局限老片和新片都有高分”这也是一个可以立得住的结论评分分布和年份分布可以共同支撑它。5. 避坑指南爬虫、清洗、可视化里最容易翻车的5个问题5.1 正则匹配不到数据页面结构被改版现象运行脚本后all_movies一直是空列表或者抓到的条数远少于100。原因榜单页改版后内部的class名、标签层级发生变化另一个常见原因是反爬返回了验证页解析函数拿到的html里根本没有电影条目。解决把响应存成本地文件看实际结构。如果class名变了修改正则中的关键词如果html里只剩几行验证码调整请求头并降低频率。注意正则匹配不到时不要反复调正则里的量词先打印len(html)看长度长度只有几千字节多半是验证页。定位代码if len(movies) 0: with open(error.html, w, encodingutf-8) as f: f.write(html) print(匹配为空请检查error.html)5.2 评分拆成两段后转float报错现象清洗时执行astype(float)直接报错ValueError提示字符串无法转换。原因score_int或score_frac里有空值。正则匹配不到小数部分时字符串就变成“9.”这种残缺格式。如果某一行两个都为空拼接出来就是“.”转float必然失败。解决优先用pd.to_numeric(x, errorscoerce)而不是astype(float)。转完后再查看NaN数量score_str df[score_int].fillna() . df[score_frac].fillna() df[score] pd.to_numeric(score_str, errorscoerce) print(df[score].isna().sum())看到NaN之后决定是删行还是填默认值。对Top100榜单评分我建议直接删除缺失评分的行因为评分是核心字段硬填一个值会干扰后续所有图表。5.3 CSV打开后中文乱码现象pandas里数据正常Excel打开CSV全是乱码。原因写入用了utf-8Excel按GBK解码。这是编码不一致问题不是数据坏了。解决to_csv时用encodingutf-8-sig。已经生成的乱码文件不能靠Excel手动切换编码完整挽救只能重新导出。这条属于血泪经验建议第一次就写成utf-8-sig。验证读取也没问题df_check pd.read_csv(maoyan_top100.csv, encodingutf-8-sig) print(df_check.head())5.4 matplotlib中文显示成方块现象图标题、坐标轴标签都是方块英文正常。原因matplotlib默认字体里没有中文字符需要配置中文字体。Windows上SimHei通常可用macOS上可以用PingFang SC。解决在绘图脚本顶部统一设置plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False如果列表里的字体都没装先用print(matplotlib.font_manager.fontManager.ttflist)查看系统可用字体挑一个支持中文的填进去。axes.unicode_minus必须设为False否则负号显示成方块。配置要放在import matplotlib之后所有绘图之前否则有的图已经生成了才设置无效。5.5 只有100条数据分析结论被质疑现象答辩时老师问“你统计的演员上榜次数能代表猫眼所有电影吗”。原因Top100榜单是有偏样本它是按热度或评分选出的百强不代表全量。解决在报告标题、PPT、结论里都写明“研究对象为猫眼Top100榜单”而不是“猫眼电影整体”。想扩大样本可以爬取多个榜单合并比如“正在热映榜”“人气榜”字段结构类似合并后做对比分析。这个动作不复杂但能让爬虫和分析部分更有说服力。6. 三个加分技巧让期末大作业从“能跑”变成“值得看”6.1 加日志和重试爬虫中断不再“从头再来”抓100条数据只要10秒但如果以后抓多个榜单中途断掉会很痛苦。给请求函数加一个重试包装import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(message)s) def safe_fetch(offset, retry3): for i in range(retry): try: html fetch_html(offset) logging.info(foffset {offset} 请求成功) return html except Exception as e: logging.warning(foffset {offset} 第{i1}次失败: {e}) time.sleep(2) return None每次请求成功都留一条日志失败时知道是从哪个offset断了。这个习惯比代码本身更重要。6.2 用pyecharts产出交互式HTML图表答辩现场用网页图表比静态图片更吸引人。pyecharts基于ECharts几行就能生成一个带数据提示的条形图from pyecharts.charts import Bar from pyecharts import options as opts top10 df.nlargest(10, score) bar (Bar() .add_xaxis(top10[title].tolist()) .add_yaxis(评分, top10[score].tolist()) .set_global_opts(title_optsopts.TitleOpts(titleTop10高分电影)) .set_series_opts(label_optsopts.LabelOpts(is_showTrue))) bar.render(top10.html)浏览器打开top10.html鼠标悬停能看到每部电影评分这种交互感在答辩演示时很加分。前提是环境里已经pip install pyecharts。6.3 报告里放一张可读表格数据分析报告里不建议贴print()输出。可以生成一个排序后的表格放进PPT或者附录summary df[[rank, title, score, year]].sort_values(score, ascendingFalse) print(summary.head(20).to_string(indexFalse))我的习惯是最后几天不再动爬虫代码把所有数据和分析结果固定住只调整结论的表达。做这类项目时吃过亏数据抓下来没检查质量就去画图出来的图自己都解释不了。现在无论多小的大作业我都会先跑df.info()和df.describe()相当于给数据做个体检。希望帮到你。本文还有配套的精品资源点击获取