☰
Python爬虫实战:猫眼电影Top100数据采集与可视化分析
2026/10/18 23:03:27 网站建设 项目流程

简介:面向需要完成期末大作业或课程设计的Python学习者,这份项目以猫眼电影数据为切入点,集成了从网络爬取、数据解析、存储管理到可视化展示的完整技术链路。相比零散教程,它提供了一个可直接运行的综合性案例,帮助理解真实项目中多个模块如何串联协作。压缩包共42个文件,核心为11个Python脚本和9个pyc编译文件,另有5个HTML页面、5个JavaScript脚本及1个SQL数据文件,配合配置文件、项目配置与readme文档,整体体积仅428KB,便于快速下载部署。代码注释详细,从请求头伪装、数据清洗到图表渲染均有体现,也保留清晰的目录结构方便定位修改。目前已有1984人学习下载,适合希望对照完整项目掌握爬虫与分析流程,并在此基础上做功能扩展的开发者。

1. 从榜单页到可视化大屏:这门期末大作业到底在练什么

很多人第一次看到“基于Python实现猫眼电影数据爬取+数据分析+数据可视化”这个题目,第一反应是找个别人的代码跑一遍,改改参数交上去。但真正动手后你会发现,这门期末大作业的核心从来不是“爬虫写得有多快”,而是你能不能把一条完整的数据流水线跑通:从网页上把半结构化的电影信息抠出来,清洗成规整的表格,再从中提炼出“哪个类型的电影评分最高”“票房和评分到底相不相关”这类结论。它练的是你同时驾驭 requests、正则表达式、pandas 和 pyecharts 的能力,以及遇到反爬、脏数据、中文字体乱码时能不能自己排查。本文就用一条可复现的路线,带你从零把猫眼电影 Top100 榜单爬下来、存进 CSV、完成数据清洗,最后生成一组能直接放进答辩 PPT 的交互图表。全程不依赖 selenium 这类重工具,尽量用最轻量的方案把每个环节讲透。


2. 准备工作:环境搭建与依赖选型,以及为什么不用 selenium

2.1 三个核心库的职责边界

这个项目涉及的库并不多,但每个库的定位必须清晰。requests 负责拿 HTML 页面,它的超时设置、请求头伪装直接决定你能否稳定抓取;正则表达式 re 负责从 HTML 里“抠”出结构化字段,因为猫眼榜单页的数据就嵌在 HTML 标签中,用 XPath 或 BeautifulSoup 也能做,但期末大作业的代码篇幅有限,正则是最直观且不容易被版本更新影响的选择;pandas 负责数据清洗与分析,它能帮你处理缺失值、转换数据类型、做分组聚合;pyecharts 负责可视化,它生成的 HTML 图表可以直接双击打开,不需要额外部署 Web 服务。

这里有一个很关键的选型取舍——为什么不用 selenium?因为 selenium 需要下载浏览器驱动,还要启动一个完整的浏览器进程,在答辩现场一旦驱动版本和浏览器不匹配就直接翻车。而 requests 加正则的方案,只需要 Python 环境和三个标准库就能跑通,环境复现成本几乎为零。至于反爬,猫眼对 requests 的拦截力度远低于其他平台,只要设置合理的 User-Agent 和 Cookie,短时间内抓取 Top100 是稳定的,这一点后面还会单独讲。

2.2 最小可运行环境与验证命令

我一般建议用一个干净的虚拟环境来跑这个项目,避免系统 Python 环境被污染。下面的命令在 Windows 和 macOS 上通用,主要做三件事:创建独立环境、安装依赖、验证安装是否成功。

python -m venv movie_env source movie_env/bin/activate # Windows 下改为 movie_env\Scripts\activate pip install requests pandas pyecharts python -c "import requests, pandas, pyecharts; print('环境就绪')"

这段命令的顺序有讲究:先创建虚拟环境,再激活,然后一次性安装全部依赖。把三个库写在同一行 pip install 里,pip 会自动解析依赖树,比逐个安装更快。“环境就绪”这行输出是给你的第一个“验收信号”,如果它正常打印,说明后续所有代码都有运行基础。如果这一步报错,九成是 pip 源的问题,可以换用国内镜像源,命令是pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests pandas pyecharts。

2.3 确认网页结构与数据字段

打开猫眼电影榜单页,按下 F12 进入开发者工具,你会发现每条电影记录都包在一个<dd>标签里。这个标签内包含片名、主演、上映时间、评分等关键字段,其中评分被拆成了整数和小数两部分,分别放在<i class="integer">和<i class="fraction">中。这种结构的价值在于:正则表达式可以直接按dd块的模式去匹配,而不是一行行地匹配全部 HTML。

建议你把页面往下滚动,确认榜单是分页的,每页 10 条,共 10 页,URL 上的 offset 参数从 0 开始,每页加 10。这个分页规律是后面循环抓取的核心,也是期末答辩时老师最喜欢追问的细节——你要能说清楚“offset=10 代表第二页”而不是“代表第十条数据”。


3. 用 requests + 正则解析 Top100 榜单页:最小可运行爬虫

3.1 构造带翻页的请求函数

爬虫的第一步不是写解析代码,而是先确保持续能拿到 HTML。猫眼榜单页对请求头的校验不算严格,但如果你不设置 User-Agent,返回的页面很可能被重定向到验证页。这里我习惯把请求头抽成一个字典,每次请求都带上,同时设置 3 秒超时和 3 次重试。

import requests import re import pandas as pd import time HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://maoyan.com/board/1' } def fetch_page(offset): url = f'https://maoyan.com/board/1?offset={offset}' for attempt in range(3): try: resp = requests.get(url, headers=HEADERS, timeout=5) resp.raise_for_status() resp.encoding = 'utf-8' return resp.text except Exception as e: print(f'第 {offset} 页请求失败,重试 {attempt + 1}/3: {e}') time.sleep(2) return None

这个函数把“请求”和“重试”绑定在一起,调用方只需要传入 offset 就能拿到 HTML 文本。参数里有三个细节值得留意:URL 中的board/1代表榜单类型为 Top100,不要随意改动;resp.encoding = 'utf-8'是必须的,因为猫眼页面用的是 UTF-8 编码,Python 的 requests 有时会误判为 ISO-8859-1;重试间隔设为 2 秒,既能避开瞬时反爬,又不会让总耗时膨胀到不可接受。

3.2 用正则从 HTML 中提取电影字段

拿到 HTML 后,解析的核心是“按块匹配”。先用正则把整个页面的所有<dd>块提取出来,再对每个块分别提取片名、主演、上映时间、评分。这样做的好处是避免正则的贪婪匹配把多个 dd 块混在一起,同时让错误定位更容易——如果某个字段匹配不到,你只需要检查那一条记录而不是整个页面。

def parse_page(html): pattern = re.compile(r'<dd>(.*?)</dd>', re.S) items = pattern.findall(html) movies = [] for item in items: title_match = re.search(r'title="([^"]+)"', item) star_match = re.search(r'<p class="star">\s*(.*?)\s*</p>', item, re.S) time_match = re.search(r'<p class="releasetime">(.*?)</p>', item, re.S) integer_match = re.search(r'<i class="integer">(\d+)</i>', item) fraction_match = re.search(r'<i class="fraction">\.(\d+)</i>', item) if not all([title_match, star_match, time_match, integer_match, fraction_match]): continue movies.append({ '片名': title_match.group(1), '主演': star_match.group(1).strip().replace('主演:', ''), '上映时间': time_match.group(1).strip().replace('上映时间:', ''), '评分': int(integer_match.group(1)) + float('0.' + fraction_match.group(1)) }) return movies

这段代码的关键在于re.S标志,它让.能匹配换行符,否则<p class="star">和主演名字之间一旦有换行,整个匹配就会失败。评分拼接的逻辑是先把整数部分拿出来,再把小数部分转成0.x的浮点数,二者相加得到完整评分。这里的float('0.' + fraction_match.group(1))必须用字符串拼接而不是直接做除法,因为小数部分可能是 9,直接除以 10 会得到 0.9,但如果小数部分是个位数且带前导零(比如 05),直接强转就会出问题。

3.3 主循环与 CSV 落盘

解析函数写好后,主循环的工作就非常机械了:遍历 10 页,每页请求一次、解析一次、把结果追加到列表,最后一次性转成 DataFrame 并保存为 CSV。这里我把“每页之间的等待”放在请求函数外部,方便统一控制频率。

all_movies = [] for offset in range(0, 100, 10): html = fetch_page(offset) if html: movies = parse_page(html) all_movies.extend(movies) print(f'已抓取 offset={offset},累计 {len(all_movies)} 条') else: print(f'offset={offset} 抓取失败,跳过') time.sleep(1) df = pd.DataFrame(all_movies) df.drop_duplicates(subset='片名', inplace=True) df.to_csv('maoyan_top100.csv', index=False, encoding='utf-8-sig') print(f'共保存 {len(df)} 条数据到 maoyan_top100.csv')

encoding='utf-8-sig'是一个细节中的细节。如果在这里写成'utf-8',生成的 CSV 文件用 Excel 打开时中文会显示成乱码,因为 Excel 默认按 GBK 编码解析无 BOM 文件。drop_duplicates(subset='片名')是为了防止页面在极端情况下出现重复数据,这一行的存在能避免后面统计“哪年上映电影最多”时出现数值虚高。运行完这段代码,你就拥有了本次大作业的原始数据原料。


4. 数据清洗与字段扩展:把“能跑”变成“能分析”

4.1 数据维度与缺失值检查

爬下来的 CSV 一共有四个字段:片名、主演、上映时间、评分。四个字段对应四个分析维度,但这远远不够。比如你想分析“哪个类型的电影评分最高”,现在的数据里没有“类型”字段;你想分析“哪一年的电影平均评分最高”,现在的“上映时间”存储的是完整的日期字符串,直接按年份分组是不可能的。这就是期末大作业拉开差距的地方——很多人爬完数据就直接画图,导致图表里只有评分分布一种,缺乏业务解读深度。

我建议第一步先做缺失值检查,用df.isnull().sum()看哪些字段有空值。猫眼榜单页的“主演”字段有时只有“主演:某某”,有时是“主演:某某,某某”,甚至偶尔出现空主演的情况。如果空值数量少,直接删除这几行不影响整体分析;如果空值多了,就要考虑是否在正则解析时漏掉了某些特殊情况,比如复映影片的上映时间字段写的是“重映”而不是日期。

4.2 从上映时间中拆分出年份与月份

时间字段拆分是这场大作业里性价比最高的一步。它不需要额外请求接口,只靠 pandas 的字符串方法就能完成,却能让你的分析维度瞬间翻倍。拆分逻辑是先把“上映时间:”前缀去掉,再按正则提取开头的四个数字作为年份,并判断这部影片的上映月份。

df['年份'] = df['上映时间'].str.extract(r'(\d{4})') df['月份'] = df['上映时间'].str.extract(r'-(\d{1,2})-') df['年份'] = df['年份'].astype(int) df['月份'] = df['月份'].astype(int)

这里的astype(int)是必须的,因为str.extract返回的是字符串类型,不转成 int 会导致后面df.groupby('年份')['评分'].mean()时排序错乱。月份提取同样重要,它能支撑“暑期档电影评分是否更高”这类结论,而暑期档的划分条件是月份在 6 到 8 月之间。如果你只想做基础作业,拆分年份就够了;想拿高分,月份分析是最好的加分项。

4.3 主演字段切分与类型占比统计

“主演”字段的原始形如“张国荣,张丰毅,巩俐”,逗号分割后可以统计“哪一位演员上榜次数最多”。这时用 pandas 的str.split配合explode可以把一行拆成多行,再做value_counts统计个人上榜次数。值得提醒的是,极少数影片的主演字段里有“主演:”前缀,正则解析时已经用replace('主演:', '')清掉了,但如果你的数据是手动整理的,这里需要再次检查。

actor_series = df['主演'].str.split(',', expand=False).explode() actor_series = actor_series.str.strip() actor_series = actor_series[actor_series != ''] top_actors = actor_series.value_counts()

这段代码的核心是explode,它把列表型单元格拆成多行,比如一部三人主演的电影会变成三行。str.strip()是为了去掉字符串里可能残留的空格,因为真实页面中的数据项之间可能有空格,不清理的话“张国荣”和“ 张国荣”会被当成两个人。value_counts()返回的是一个 Series,直接就是“演员:次数”的统计结果。

4.4 数据质量复核与抽样打印

清洗完字段后,不要急着做可视化,先抽样打印前五行数据,用肉眼确认年份、月份、评分三个字段的值都是合法数字。这一步虽然看起来“不够技术”,但它能拦截掉大量低级错误,比如年份解析出 NaN、评分大于 10 的异常值。我的习惯是同时打印df.dtypes和df.describe(),前者看字段类型,后者看评分的最大值是否在 9.8 左右。

异常值的处理原则是“先看数量,再决定删还是改”。如果异常值只有一两条,直接删掉不影响整体分布;如果异常值比例超过 10%,就要怀疑是正则解析时匹配到了错误标签,得回到解析函数检查正则模式。这一节做完,你的数据就从“能看”变成了“能讲”。


5. 可视化设计与反向排查:哪些图能讲出真正的故事

5.1 选型方案:pyecharts 的三种图表怎么搭配

可视化是这个项目里最直接能看出工作量差异的地方。我的建议是准备三张图,每一张对应一个不同的分析结论:评分分布直方图用来回答“Top100 电影的分数集中在哪个区间”;演员上榜次数条形图用来回答“哪些演员最常出现在高票房电影中”;年份趋势折线图用来回答“这些年上映的电影数量是增是减”。三张图分别覆盖分布、排名、趋势三种分析类型,比单纯做一张炫酷的大屏更有说服力。

pyecharts 的优势在于它是中文社区最成熟的可视化库,生成的 HTML 文件自带交互提示,答辩时鼠标悬停就能看到具体数值。它的安装命令在前面已经包含在pip install pyecharts里,不需要额外装 Node.js 或 Web 服务。渲染方式统一用chart.render('chart.html'),它会生成一个完整的 HTML 页面,用浏览器直接打开即可。

5.2 评分分布与类型占比的绘图实现

绘制评分分布直方图时,我先把评分四舍五入到整数,再按分值分桶聚合。这里要注意round(0.5)的银行家舍入问题——Python 的 round 对 0.5 会舍入到偶数,导致 9.5 变成 9 而不是 10。为了避免这个坑,我改用int(score + 0.5)做四舍五入,这样可以保证评分 9.5 分进入 10 分桶。

from pyecharts.charts import Bar from pyecharts import options as opts df['评分_取整'] = df['评分'].map(lambda x: int(x + 0.5)) score_dist = df['评分_取整'].value_counts().sort_index() bar = ( Bar() .add_xaxis(score_dist.index.tolist()) .add_yaxis('电影数量', score_dist.values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title='猫眼Top100电影评分分布'), xaxis_opts=opts.AxisOpts(name='评分'), yaxis_opts=opts.AxisOpts(name='电影数量') ) ) bar.render('score_distribution.html')

value_counts().sort_index()按评分从低到高排序,保证横轴从左到右是 1 到 10 的自然顺序,而不是随便排。如果你的图横轴顺序是乱的,多半是这里没有加 sort_index。渲染出的 HTML 文件名不要用中文,避免某些浏览器在本地文件场景下打不开。

5.3 中文字体与坐标轴显示异常的处理

用 pyecharts 出图后,最容易碰到的问题是图表内部的中文标签变成方块。原因是 pyecharts 渲染出的 HTML 在本地浏览器中默认依赖操作系统中文字体栈,而某些精简版系统或未安装中文语言包的虚拟机里没有可用中文字体。常见的解决方法是给页面补充字体设置,在 pyecharts 中可以通过set_global_opts里的textstyle_opts指定字体族。

.set_global_opts( title_opts=opts.TitleOpts(title='猫眼Top100电影评分分布'), textstyle_opts=opts.TextStyleOpts(font_family='Microsoft YaHei') )

把font_family指定为Microsoft YaHei后,Windows 环境下基本不会出现乱码。如果你用的是 macOS,可以改成PingFang SC。这是期末大作业里一个特别容易被忽视的展示细节——图做出来了但字全是方块,答辩效果会大打折扣。如果你把图表嵌进 Jupyter Notebook 预览,还需要额外执行chart.render_notebook(),不过我们这里统一用 render 生成 HTML,不依赖 Notebook 环境。

5.4 数据可视化阶段常见的三个逻辑误区

第一个误区是把所有字段塞进一张图,比如把片名、演员、评分、年份全部放在柱状图上,导致横轴标签拥挤到无法阅读。正确的做法是一张图只讲一个结论,最多两个维度对比。第二个误区是不做数据排序就画条形图,导致条形图的高矮毫无规律。第三个误区是忽略了“样本量太小”的问题,比如某年份只有一部电影上榜,直接得出“这一年电影质量很差”的结论就过于草率。图表是给人看的,不是给算法跑的,这是可视化阶段最重要的心态。

5.5 期末答辩流程中如何把图表嵌进 PPT 演示

pyecharts 输出的 HTML 文件不能直接复制进 PPT,你有两种选择:一是直接演示 HTML 页面,展示交互悬停效果;二是用截图工具把图表区域截成 PNG 图片放进 PPT。我倾向于推荐前者,因为交互效果本身就是工作量证明——老师看到鼠标悬停弹出数值,比看到一张静态图更有说服力。如果选择截图,注意把浏览器窗口缩放比例调整为 100%,避免截出来的图文字模糊。另外,所有 HTML 文件和 CSV 文件最好放在同一个目录下,答辩前提前检查一次文件路径,防止换到教室电脑后图表空白。


6. 猫眼大作业的五大翻车点:现象、原因与解决

6.1 请求被拒绝,或返回空白页面

现象:运行爬虫时,print 显示出 HTML 内容长度为 0,或请求状态码不是 200。对象原因:requests 默认的 User-Agent 是 Python-requests 的标识,猫眼服务器可以轻易识别并拒绝服务;另外如果请求频率太快,也可能触发临时封禁。解决方法:设置完整的请求头,其中 User-Agent 必须伪装成真实浏览器;同时每次请求之间至少睡眠 1 秒。如果仍然被拒,可以尝试从浏览器开发者工具中复制一份完整的 Cookie 字符串填入请求头,不要直接访问登录接口,那不是期末作业需要的复杂度。

6.2 正则匹配为空或只匹配到一条数据

现象:parse_page()返回的 movies 列表长度为 0,或者只剩最后一条数据。原因:最常见的错误是忘了在正则末尾加re.S,导致<p class="star">内的主演字段因为换行而匹配失败;另一个原因是把<dd>匹配写成贪婪模式,导致整个页面只匹配出一个超大块。解决:先打印len(re.findall(r'<dd>(.*?)</dd>', html, re.S)),如果数量不是 10,说明页面结构有变化,需要回到开发者工具里查看当前dd块的具体 class 名称。猫眼偶尔会调整页面结构,这是不可控因素,学会调试正则比背代码更重要。

6.3 CSV 用 Excel 打开时中文乱码

现象:CSV 文件用记事本打开正常,用 Excel 打开后所有中文全部变成类似“ç”的乱码。原因:Python 默认保存的 UTF-8 编码不带 BOM(字节序标记),而 Excel 按照系统 ANSI 编码(中文系统是 GBK)去解码,真实原因就是编解码不一致。解决:保存时把encoding参数改成utf-8-sig,这个编码会在文件开头写入 BOM 头,Excel 看到 BOM 后会自动切换为 UTF-8 解码方式。如果你已经保存了不带 BOM 的文件,可以重新运行一次保存命令覆盖,不要手动改后缀。

6.4 评分出现 10.9 这样的非法值

现象:描述性统计中,评分最大值超过了 10。原因:正则解析时把整数部分和小数部分都正确提取了,但中间多了一个空格或换行,导致float('0.' + fraction)时带入了异常字符,比如float('0. 9')会直接报错或产生错误值。另一种情况是页面中有其他匹配到<i class="integer">9</i>但后面跟的不是小数,比如预告片评分。解决:在解析函数中加入一层数值校验,if score > 10: continue,同时把评分写入列表前打印一条警告。这一步能保证最终写进 CSV 的数据一定是合法范围。

6.5 图表中文乱码与空白提示

现象:生成的 HTML 图表能打开,但标题和坐标轴全部是小方块,或者数据全部为空。原因:中文字体缺失是最常见的,其次是 pyecharts 在极旧版本中与 Python 3.10 以上存在不兼容问题。解决:先尝试更新 pyecharts 到最新版,pip install pyecharts -U;再检查代码中是否引用了不存在的字段名,比如df['评分']写成了df['分数']——pandas 的 KeyError 会在运行时报错,但某些场景下你用了df.get()就会静默返回空值。最稳妥的调试方法是把图表数据源单独打印一遍,确认它非空再传给图表。


7. 进阶玩法:把评分与票房两个维度合并,做出答辩时的“记忆点”

如果你想让这份期末大作业在答辩时让人眼前一亮,不要在“评分分布”这种基础分析上止步。最值得做的进阶玩法是“类型 × 年份 × 评分”的交叉聚合,它不需要新增爬虫,只用现有数据就能完成,却能让你的分析从“描述”升级到“洞察”。

第一步,从片名入手给每部电影打上“类型标签”。猫眼的片名往往自带线索,比如名字里含“战”“狼”的多为动作片,含“爱”“恋”的多为爱情片,但这只是粗粒度方法,容易误判。更稳妥的方案是把 Top100 的片名与一个短小的关键词映射表做匹配,这个映射表可以手动维护,也可以在答辩时说明它的局限性。常见做法是先分析“每部电影评分的方差”,找出评分差异最大的类型,作为下一步深挖的对象。

第二步,用df.groupby(['年份', '评分_取整']).size()生成一个透视表,观察“年份”和“评分”两列的联合分布。如果某一年评分 9 分以上的电影数量明显多于其他年份,说明这一年上榜影片的整体质量偏高。透视表结果可以转成 Excel 表格放进附录,答辩时直接打开给老师看。

第三步,也是最有“记忆点”的一步:把“演员上榜次数”和该演员参演电影的“平均评分”做散点图,横轴是参演次数,纵轴是平均评分。这张图能直观看出“哪些演员参演高评分电影更多”,如果数据量太小,散点图只有十几个点,也完全可以接受,因为 Top100 本身就决定了样本必然稀疏。这个结论不需要多复杂,只要能在答辩时讲清楚“我发现了什么、依据是什么、数据在哪”就够了。

值得一提的是,我在完成这类期末大作业时,习惯把每个分析图表的数据源都单独导出成 CSV。比如把score_distribution的中间结果存成score_dist.csv,这样老师追问时你能随手打开文件展示数据,而不是回到 Jupyter Notebook 里重新运行一遍。这个习惯不仅能帮你应付答辩,也是在模拟真实项目中的“数据可追溯”要求——别人拿着你的 CSV 就能复现你的图,这是工程化思维的一种体现。

最后给你一个我自己的教训:不要在答辩前两天才开始写代码。推荐的节奏是第一天完成爬虫和 CSV 落盘,第二天完成清洗和可视化,第三天整理结论和 PPT。如果爬虫当天没跑通,后面时间会非常紧张。这个项目最美好的地方在于每个环节都可以单独验证,只要有耐心一步步排查,结果一定能跑出来。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询