简介:基于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两个标签 | 整数部分和小数部分被拆开 |
我一般会写一个临时小脚本,把第一页响应存下来,避免在终端里只看到半屏HTML:
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' } url = 'https://maoyan.com/board/4?offset=0' resp = requests.get(url, headers=headers, timeout=5) with open('maoyan_page.html', 'w', encoding='utf-8') as f: f.write(resp.text) print(resp.status_code, len(resp.text))这段代码的逻辑很简单:先请求第一页,把响应保存为HTML。resp.text是requests解码后的文本,写文件时再指定utf-8,后面用编辑器打开中文不会乱。很多同学在这里直接打印HTML,结果终端缓冲区太小,只能看到半屏内容,无法判断结构。把响应存下来再打开,是最稳妥的做法。
参数说明:timeout=5表示超过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( r'<dd>.*?board-index-\d+">(\d+)</i>.*?' r'<a href="/films/\d+" title="(.*?)".*?' r'<p class="star">(?:主演:)?(.*?)</p>.*?' r'<p class="releasetime">(?:上映时间:)?(.*?)</p>.*?' r'<i class="integer">(.*?)</i><i class="fraction">(.*?)</i>', re.S ) def fetch_html(offset): url = f'https://maoyan.com/board/4?offset={offset}' resp = requests.get(url, headers=HEADERS, timeout=5) 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这段代码里有几个关键点。第一,正则里的
参数说明: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页offset=0,第2页offset=10,一直到第9页offset=90。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. 数据清洗与存储:把抓下来的字符串变成能分析的DataFrame
3.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(subset='title', inplace=True) 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, errors='coerce') # 上映时间转成datetime,非法的变成NaT df['release_date'] = pd.to_datetime(df['release_date'], errors='coerce') 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和errors='coerce',它把“9.”+“5”这种字符串转成9.5,遇到无法解析的值变成NaN,后续再dropna。这样做比直接astype(float)安全,因为astype遇到空字符串会直接报错,而to_numeric不会。第二个是to_datetime里的errors='coerce',即使页面里某个上映时间是空文本,程序也不会中断。
关于演员字段的拆分,我用中文顿号作为分隔符,这是榜单主演字段的默认格式。def split 如果某个条目只有一个演员,split后也能生成含一个元素的列表。这里不要用英文逗号分割,因为逗号分隔后会有多余空格,还得再strip一次。
3.3 存储到CSV和SQLite:让作业在答辩时随时可复现
数据清洗完就要落盘。存储目标有两个:一是生成一份随时可查看的CSV,方便在pandas之外用Excel人工核对;二是存SQLite,答辩时不管爬虫还能不能跑通,数据都还在。代码:
df.to_csv('maoyan_top100.csv', index=False, encoding='utf-8-sig') import sqlite3 conn = sqlite3.connect('maoyan.db') df.to_sql('movie', conn, if_exists='replace', index=False) conn.close()重点说encode参数。很多同学第一次用encoding='utf-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连接,表名为movie;if_exists='replace'表示表已存在就覆盖,适合重复运行。需要把数据读回来时,用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(kind='hist', bins=20, edgecolor='white') plt.title('猫眼Top100电影评分分布') plt.xlabel('评分') plt.ylabel('电影数量') plt.show()bins=20表示评分区间分成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(kind='bar', 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(kind='bar') 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(kind='barh', figsize=(8, 8)) plt.title('演员上榜次数Top15') plt.xlabel('上榜次数') plt.ylabel('演员') plt.show()用barh画横向条形图,演员名字在纵轴,不会因为名字太长导致横轴标签重叠。sort_values()先把数据升序,最长的条形在下方,视觉上更像一个排行榜。
再补一个评分与年份的关系,作为分析部分的收尾:
print(df[['year', 'score']].corr()) df.plot.scatter(x='year', y='score') plt.title('年份与评分散点图') plt.show()corr()输出相关系数矩阵,如果相关系数接近0,说明年份和评分没有明显线性关系。这种情况下结论就是“榜单中高分电影不受年份局限,老片和新片都有高分”,这也是一个可以立得住的结论,评分分布和年份分布可以共同支撑它。
5. 避坑指南:爬虫、清洗、可视化里最容易翻车的5个问题
5.1 正则匹配不到数据,页面结构被改版
现象:运行脚本后all_movies一直是空列表,或者抓到的条数远少于100。
原因:榜单页改版后,
解决:把响应存成本地文件看实际结构。如果class名变了,修改正则中的关键词;如果html里只剩几行验证码,调整请求头并降低频率。注意正则匹配不到时不要反复调正则里的量词,先打印len(html)看长度,长度只有几千字节多半是验证页。定位代码:
if len(movies) == 0: with open('error.html', 'w', encoding='utf-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, errors='coerce')而不是astype(float)。转完后再查看NaN数量:
score_str = df['score_int'].fillna('') + '.' + df['score_frac'].fillna('') df['score'] = pd.to_numeric(score_str, errors='coerce') print(df['score'].isna().sum())看到NaN之后决定是删行还是填默认值。对Top100榜单评分,我建议直接删除缺失评分的行,因为评分是核心字段,硬填一个值会干扰后续所有图表。
5.3 CSV打开后中文乱码
现象:pandas里数据正常,Excel打开CSV全是乱码。
原因:写入用了utf-8,Excel按GBK解码。这是编码不一致问题,不是数据坏了。
解决:to_csv时用encoding='utf-8-sig'。已经生成的乱码文件不能靠Excel手动切换编码完整挽救,只能重新导出。这条属于血泪经验,建议第一次就写成utf-8-sig。验证读取也没问题:
df_check = pd.read_csv('maoyan_top100.csv', encoding='utf-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(level=logging.INFO, format='%(asctime)s - %(message)s') def safe_fetch(offset, retry=3): for i in range(retry): try: html = fetch_html(offset) logging.info(f'offset {offset} 请求成功') return html except Exception as e: logging.warning(f'offset {offset} 第{i+1}次失败: {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_opts=opts.TitleOpts(title='Top10高分电影')) .set_series_opts(label_opts=opts.LabelOpts(is_show=True))) bar.render('top10.html')浏览器打开top10.html,鼠标悬停能看到每部电影评分,这种交互感在答辩演示时很加分。前提是环境里已经pip install pyecharts。
6.3 报告里放一张可读表格
数据分析报告里不建议贴print()输出。可以生成一个排序后的表格,放进PPT或者附录:
summary = df[['rank', 'title', 'score', 'year']].sort_values('score', ascending=False) print(summary.head(20).to_string(index=False))我的习惯是最后几天不再动爬虫代码,把所有数据和分析结果固定住,只调整结论的表达。做这类项目时吃过亏:数据抓下来没检查质量就去画图,出来的图自己都解释不了。现在无论多小的大作业,我都会先跑df.info()和df.describe(),相当于给数据做个体检。希望帮到你。
本文还有配套的精品资源,点击获取