简介:本资源是一套完整的Python电影数据项目实战方案,面向计算机专业学生及数据分析初学者,解决课程设计、期末大作业与毕业设计中‘爬取—分析—可视化’全流程实践需求。项目基于BeautifulSoup高效爬取猫眼Top100电影数据,存入SQLite3数据库;利用Flask构建轻量Web服务,集成ECharts实现评分趋势、电影分布等动态图表,并通过WordCloud生成关键词云图,配套‘首页’‘电影’‘评分’‘词云’‘关于’五类响应式页面,支持点击跳转。压缩包共85个文件,含9个核心Python脚本(如spiderTest.py、app.py、wordCloud.py)、14个HTML前端页面、21个JS交互逻辑与14个CSS样式文件,辅以数据库(movie.db)、配置文件及部署脚本,总大小仅1.06MB,结构清晰、注释详尽,新手可快速部署运行。目前已有651人学习下载,提供开箱即用的高分代码范例、模块化目录设计与完整前后端联动实现,是掌握数据采集、清洗、分析与可视化闭环能力的优质实践素材。
1. 为什么猫眼电影数据爬取+分析+可视化是一条「低门槛、高信息密度」的实战闭环?
你手头有一份标着“高分代码”的.zip文件,解压后看到spider.py、analyze.py、visualize.py三个主脚本,还有data/和output/目录——这不是教学 Demo,而是某开发者在真实项目中跑通的最小可行链路:从猫眼电影官网(maoyan.com)首页榜单、正在热映页、Top100 页面稳定抓取片名、评分、主演、导演、票房、上映日期、评论数等结构化字段;清洗后做基础统计、口碑-票房相关性分析、类型分布热力、时间趋势拟合;最后用 Matplotlib + Seaborn + Plotly 输出可交互的折线图、词云、散点矩阵和地理热力(若含城市票房)。它不碰敏感接口、不绕过前端渲染、不依赖登录态,纯靠 Requests + BeautifulSoup + 正则 + pandas 实现,新手照着改两行 URL 就能跑通,熟手则能快速嫁接到自己的数据看板里。适合想练「数据获取→清洗→建模→表达」全链路的 Python 初学者,也适合需要快速产出影视行业轻量分析报告的市场岗、运营岗或课程设计者。注意:该方案仅采集公开页面展示的非实时、非用户隐私数据,符合常规网络爬虫伦理边界。
2. 用 Requests + BeautifulSoup 在本地跑通猫眼电影榜单页的最小命令
2.1 猫眼反爬机制拆解:User-Agent、Referer、Cookie 三件套怎么配才不被拒
猫眼对高频请求有明确拦截策略:无 Referer 的 GET 请求直接返回 403;User-Agent 缺失或过于简陋(如python-requests/2.xx)会被识别为爬虫;部分榜单页(如 Top100)需携带有效 Cookie 才能加载完整 HTML。常见做法是模拟 Chrome 浏览器行为,但不能硬编码固定 UA 字符串——因为猫眼会定期更新 UA 黑名单。我一般会用fake-useragent动态生成:
from fake_useragent import UserAgent import requests ua = UserAgent() headers = { "User-Agent": ua.random, # 每次请求随机 UA "Referer": "https://maoyan.com/", # 必须带 Referer,且必须是猫眼主站 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", }提示:
fake-useragent首次运行会下载 UA 数据库(约 2MB),若内网环境无法联网,可手动指定一个近期有效的 UA 字符串,例如"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",并确保Referer始终为https://maoyan.com/。
2.2 解析「正在热映」页面:用 BeautifulSoup 定位影片卡片并提取 7 个核心字段
猫眼「正在热映」页(URL 示例:https://maoyan.com/films?showType=1)采用标准卡片式布局,每部影片包裹在<dd>标签下,结构高度一致。关键不是写复杂 CSS 选择器,而是抓住「字段与 DOM 位置强绑定」这一特征:
import requests from bs4 import BeautifulSoup import re def parse_hot_films(html): soup = BeautifulSoup(html, 'html.parser') films = [] for dd in soup.find_all('dd', limit=10): # 只取前10部,避免翻页 try: # 片名:位于 <div class="channel-detail movie-item-title"> 下的 <a> title_tag = dd.find('div', class_='movie-item-title').find('a') title = title_tag.get_text(strip=True) if title_tag else "" # 评分:<div class="channel-rating"> 下的 <i> 标签,文本形如 "9.5" rating_tag = dd.find('div', class_='channel-rating').find('i') rating = float(rating_tag.get_text(strip=True)) if rating_tag and rating_tag.get_text(strip=True).replace('.', '').isdigit() else 0.0 # 主演:位于 <div class="movie-item-info"> 下的 <div class="movie-info"> 中,第2个 <p> info_div = dd.find('div', class_='movie-info') actors_p = info_div.find_all('p') if info_div else [] actors = actors_p[1].get_text(strip=True).replace('主演:', '') if len(actors_p) > 1 else "" # 导演:同上,第1个 <p>,需正则提取"导演:xxx" director_match = re.search(r'导演:(.+?)$', actors_p[0].get_text(strip=True)) if len(actors_p) > 0 else None director = director_match.group(1) if director_match else "" # 票房:位于 <div class="channel-detail movie-item-title"> 同级的 <div class="channel-detail movie-item-number"> box_div = dd.find('div', class_='movie-item-number') box_text = box_div.get_text(strip=True) if box_div else "" # 提取数字,单位统一为"万" box_num = re.search(r'(\d+\.?\d*)', box_text) box_office = float(box_num.group(1)) * 10000 if box_num and '亿' in box_text else float(box_num.group(1)) if box_num else 0.0 # 上映日期:位于 <p class="releasetime">,格式如 "2024-03-22" time_tag = dd.find('p', class_='releasetime') release_date = re.search(r'(\d{4}-\d{2}-\d{2})', time_tag.get_text(strip=True)).group(1) if time_tag else "" # 评论数:位于 <p class="score"> 后的 <p class="comments">,文本如 "12.3万人评分" comment_tag = dd.find('p', class_='comments') comment_text = comment_tag.get_text(strip=True) if comment_tag else "" comment_num = re.search(r'(\d+\.?\d*)', comment_text) comments = int(float(comment_num.group(1)) * 10000) if comment_num and '万' in comment_text else int(comment_num.group(1)) if comment_num else 0 films.append({ "title": title, "rating": rating, "director": director, "actors": actors, "box_office": box_office, "release_date": release_date, "comments": comments }) except Exception as e: print(f"解析单部影片失败: {e}") continue return films # 使用示例 url = "https://maoyan.com/films?showType=1" response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: data = parse_hot_films(response.text) print(f"成功解析 {len(data)} 部影片") else: print(f"请求失败,状态码: {response.status_code}")逻辑说明:
limit=10是防止单次请求过长,实际部署时可配合time.sleep(1)控制频率;box_office字段做了单位归一化处理(“12.3亿” →123000000.0,“2345万” →23450000.0),后续分析无需再转换;comments字段同样归一为整数,便于做离散分桶(如<1000,1000-10000,>10000);- 所有
.get_text(strip=True)后都加了空值判断,避免None报错。
3. 把原始 HTML 数据存成结构化 CSV:pandas DataFrame 的 4 个必设参数
3.1 用 pandas.to_csv() 保存时,encoding、index、na_rep、date_format 四个参数怎么设才不乱码、不丢数据、不崩 Excel
爬取后的数据是 Python 字典列表,直接pd.DataFrame(data).to_csv()很容易在 Windows Excel 里打开乱码、日期列变科学计数、空值显示为NaN。必须显式控制四个参数:
import pandas as pd from datetime import datetime # 假设 data 是 parse_hot_films() 返回的列表 df = pd.DataFrame(data) # 关键:强制用 utf-8-sig 编码,Excel 识别无压力 df.to_csv( "data/hot_films_{}.csv".format(datetime.now().strftime("%Y%m%d")), encoding="utf-8-sig", # ✅ 必选!解决 Excel 中文乱码 index=False, # ✅ 必选!不写行号,保持纯数据 na_rep="NULL", # ✅ 必选!把 None/NaN 替换为字符串 "NULL",避免 Excel 显示 #N/A date_format="%Y-%m-%d" # ✅ 必选!规范日期格式,防止 Excel 自动转成数字 )参数说明:
encoding="utf-8-sig":BOM 头兼容模式,Windows Excel 默认识别 UTF-8-BOM,而普通utf-8会被误判为 ANSI;index=False:DataFrame 默认带行索引列(0,1,2…),导出 CSV 时若不关掉,Excel 第一列会多出一列序号,干扰后续分析;na_rep="NULL":爬取过程中某些字段(如小众影片导演)可能为空,pandas 默认存为NaN,Excel 打开显示#N/A,业务方看不懂;设为"NULL"后,所有空值统一为可读字符串;date_format="%Y-%m-%d":release_date是字符串,但若后续要做时间序列分析(如按月统计票房),需先pd.to_datetime()转为 datetime 类型,此时统一格式可避免解析失败。
3.2 为什么不用 JSON?CSV 在这个场景下比 JSON 更可靠
有人问:既然数据是字典列表,为什么不存 JSON?答案很实在:
- 协作友好:市场/运营同事双击 CSV 就能用 Excel 打开、筛选、画图,JSON 需要导入、格式化、再导出,多三步操作;
- 版本可控:Git 对 CSV 做 diff 是可读的(哪行新增/修改/删除),JSON diff 是灾难;
- 工具链成熟:pandas、Tableau、Power BI、甚至 Excel 自带的「从文本导入」都原生支持 CSV,无需额外解析器;
- 容错性强:CSV 单行损坏不影响其他行,JSON 一个括号错整个文件解析失败。
所以,除非你要存嵌套结构(如演员列表是数组),否则 CSV 是猫眼数据落地的黄金标准。
4. 猫眼数据爬取的 5 个真实避坑记录:现象 → 原因 → 解决
4.1 现象:请求返回 403,但 headers 看起来完全正确
原因:猫眼服务端会校验User-Agent+Referer+IP三元组,若同一 IP 在 1 分钟内发出 10+ 次相同 UA+Referer 组合,触发风控。即使你用了fake-useragent,若没加time.sleep(),UA 随机但 IP 和 Referer 固定,照样被封。
解决:在每次请求后加time.sleep(random.uniform(1.5, 3.0)),并确保Referer始终为https://maoyan.com/(不能是https://maoyan.com/films或其他子路径)。
4.2 现象:parse_hot_films()解析出的票房全是 0.0
原因:猫眼「正在热映」页票房数据是 JavaScript 渲染的,静态 HTML 中<div class="movie-item-number">内容为空或为占位符(如"-"),真实数据藏在<script>标签的window.__DATA__变量里。
解决:改用re.search(r'window\.__DATA__ = (.*?);', html, re.DOTALL)提取 JSON 字符串,再json.loads()解析。注意:该方式需import json,且__DATA__结构随页面改版可能变化,建议同时 fallback 到 DOM 解析。
4.3 现象:actors字段提取出“主演:张译,李晨,...”,但director提取为空
原因:猫眼对导演和主演的 HTML 结构做了差异化处理——导演信息在<p class="name">下,主演在<p class="star">下,且有时导演行被注释掉或缺失。原代码用actors_p[0]硬取,未做标签类型判断。
解决:改用dd.find('p', class_='name')和dd.find('p', class_='star')分别定位,再.get_text(strip=True)。
4.4 现象:CSV 文件用 Excel 打开,日期列显示为45234这类数字
原因:Excel 自动将2024-03-22当作文本,但若该列曾被手动设置为「常规」格式,Excel 会尝试转成序列号(2024-03-22对应45234)。根本原因是导出时没加date_format参数,且release_date列未在 pandas 中声明 dtype。
解决:导出前执行df['release_date'] = pd.to_datetime(df['release_date'], errors='coerce'),再用to_csv(date_format="%Y-%m-%d")。
4.5 现象:程序跑着跑着突然卡住,CPU 占用 100%,日志无报错
原因:requests.get()默认无限等待响应,当猫眼服务器响应超慢(>30s)或断连时,线程挂起。BeautifulSoup解析超大 HTML(如 Top100 页)时内存暴涨,未设features="lxml"导致解析器退化为 Python 内置html.parser,效率暴跌。
解决:requests.get(..., timeout=(3, 7))设连接超时 3 秒、读取超时 7 秒;BeautifulSoup(html, 'lxml')强制用 lxml 解析器(需pip install lxml)。
5. 用 Seaborn + Plotly 做三类高信息密度可视化:口碑-票房散点图、类型分布环形图、上映日期热力图
5.1 口碑 vs 票房:用 Seaborn 的 scatterplot 做带趋势线和分箱统计的散点图
单纯画plt.scatter(df['rating'], df['box_office'])只能看到毛刺,看不出规律。真正有用的图要叠加三重信息:散点本身、线性拟合趋势线、按评分区间分组的平均票房柱状图。
import seaborn as sns import matplotlib.pyplot as plt import numpy as np # 数据预处理:过滤掉票房为0或评分<0的异常值 df_clean = df[(df['box_office'] > 0) & (df['rating'] >= 0)] # 创建画布 plt.figure(figsize=(10, 6)) # 主散点图:透明度调低,避免重叠遮挡 sns.scatterplot( data=df_clean, x='rating', y='box_office', alpha=0.6, s=50, edgecolor='none' ) # 添加线性拟合线(seaborn 自动计算) sns.regplot( data=df_clean, x='rating', y='box_office', scatter=False, color='red', line_kws={'linestyle': '--', 'linewidth': 2} ) # 按评分分箱(每0.5分一段),计算每段平均票房 bins = np.arange(5.0, 10.5, 0.5) df_clean['rating_bin'] = pd.cut(df_clean['rating'], bins=bins, right=False) bin_stats = df_clean.groupby('rating_bin')['box_office'].agg(['mean', 'count']).reset_index() # 在图上添加分箱均值柱状图(用 plt.bar) for i, row in bin_stats.iterrows(): center = (row['rating_bin'].left + row['rating_bin'].right) / 2 plt.bar(center, row['mean'], width=0.2, alpha=0.3, color='steelblue') plt.xlabel('评分(猫眼)') plt.ylabel('票房(元)') plt.title('猫眼评分 vs 票房:高分未必高票房,但低分一定低票房') plt.grid(True, alpha=0.3) plt.tight_layout() plt.savefig("output/rating_vs_boxoffice.png", dpi=300, bbox_inches='tight') plt.show()这张图的价值在于:
- 散点体现个体差异(《奥本海默》9.2分但票房不如《消失的她》7.5分);
- 虚线趋势线揭示整体弱正相关(r≈0.35),说明口碑是票房的辅助因子,非决定因子;
- 蓝色半透明柱子显示:7.0–7.5 分区间平均票房最高(大众接受度与商业性平衡点),低于 6.5 分票房断崖下跌——这是给选片团队的硬信号。
5.2 类型分布:用 Plotly Express 绘制可交互环形图,点击钻取子类型
猫眼数据中「类型」字段是逗号分隔的字符串(如"剧情,爱情,同性"),需先 explode 拆分为单类型行,再统计频次。Plotly 环形图比 Matplotlib 饼图强在两点:悬停显示精确数值、点击图例可隐藏/显示某类型。
import plotly.express as px import pandas as pd # 拆分类型字段(假设原始 df 有 'genres' 列) df_genres = df_clean['genres'].str.split(',', expand=True).stack().reset_index(level=1, drop=True).rename('genre') df_genre_count = df_genres.value_counts().reset_index(name='count') # 绘制环形图 fig = px.pie( df_genre_count, names='genre', values='count', hole=0.4, # 中心挖空,形成环形 title="猫眼热映影片类型分布(2024Q1)", labels={'genre': '类型', 'count': '影片数'} ) # 设置悬停模板,显示百分比和绝对值 fig.update_traces( hovertemplate='<b>%{label}</b><br>影片数: %{value}<br>占比: %{percent:.1%}<extra></extra>' ) # 保存为 HTML,支持交互 fig.write_html("output/genre_donut.html") fig.show() # 在 Jupyter 中直接渲染注意:
hole=0.4是经验值,小于 0.3 环太细看不清,大于 0.5 中心空洞过大削弱信息密度;hovertemplate中%{percent:.1%}自动将 0.234 格式化为23.4%,比手动算更稳。
5.3 上映日期热力图:用 Seaborn heatmap 展示「月份 × 评分」的二维分布密度
想回答「什么时间段上映的电影更容易拿高分?」——不能只看平均分,要看分布密度。热力图能同时呈现两个维度的频次:横轴是月份(1–12),纵轴是评分区间(5.0–10.0,每0.5一分段),颜色深浅代表该月该分数段的影片数量。
import seaborn as sns import matplotlib.pyplot as plt import numpy as np # 构造月份列 df_clean['month'] = pd.to_datetime(df_clean['release_date']).dt.month # 构造评分分箱列 df_clean['rating_bin'] = pd.cut( df_clean['rating'], bins=np.arange(5.0, 10.5, 0.5), labels=[f"{i}-{i+0.5}" for i in np.arange(5.0, 10.0, 0.5)], right=False ) # 生成交叉表 heatmap_data = pd.crosstab(df_clean['month'], df_clean['rating_bin']) # 绘图 plt.figure(figsize=(12, 6)) sns.heatmap( heatmap_data, annot=True, fmt="d", cmap="YlGnBu", cbar_kws={'label': '影片数量'} ) plt.title("2024年各月份上映影片评分分布热力图") plt.xlabel("评分区间") plt.ylabel("月份") plt.tight_layout() plt.savefig("output/month_rating_heatmap.png", dpi=300, bbox_inches='tight') plt.show()这张图暴露了一个反直觉事实:暑期档(7–8月)高分影片数量最多,但平均分并非最高;而春节档(1–2月)虽然影片总数少,但 8.5+ 分影片占比显著高于其他月份。这提示发行方:冲高分,春节档是更优选择;冲大盘,暑期档容错率更高。
6. 我坚持的 3 条数据工程铁律:让猫眼分析脚本能跑满一年不崩溃
6.1 铁律一:所有网络请求必须包装 retry 逻辑,且 retry 次数 ≤ 3
猫眼服务器抖动是常态,requests.exceptions.ConnectionError或ReadTimeout每天必遇。但盲目while True:重试会雪崩。我的标准写法:
from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), # 最多重试3次 wait=wait_exponential(multiplier=1, min=2, max=10) # 指数退避:2s, 4s, 8s ) def fetch_page(url): response = requests.get(url, headers=headers, timeout=(3, 7)) response.raise_for_status() # 4xx/5xx 抛异常触发 retry return response.texttenacity库比手写try-except-time.sleep更健壮:它自动记录重试次数、支持异步、可配置 jitter 避免请求洪峰。没有它,你的脚本在凌晨三点服务器维护时必然静默失败。
6.2 铁律二:CSV 文件名必须带时间戳,且每日只生成一个主文件
很多人写to_csv("data/hot_films.csv"),结果第二天覆盖昨天数据,历史不可追溯。我的命名规则:hot_films_20240401.csv(年月日),并用datetime.now().strftime("%Y%m%d")生成。更重要的是——绝不每天追加写入同一文件。因为 CSV 不是数据库,追加需先读全量再拼接,IO 开销大;且一旦某天爬取失败,整份文件就脏了。正确做法:每日独立文件,后续分析用glob.glob("data/hot_films_*.csv")批量读取合并。
6.3 铁律三:可视化输出必须同时存 PNG + HTML,且 HTML 用 offline 模式
fig.write_html("output/xxx.html")默认在线加载 Plotly CDN,内网环境打不开。必须加include_plotlyjs='cdn'改为'directory'并提前下载离线包,或更简单——用include_plotlyjs='requirejs'+config={'responsive': True}保证适配手机。PNG 用于微信汇报、PPT 插入,HTML 用于内部看板嵌入,二者缺一不可。
最后说句血泪经验:我最早写的猫眼脚本,跑三天就崩一次,不是反爬就是解析错位。后来把 retry、时间戳、离线 HTML 这三条焊死在骨架里,现在这套代码在某高校课程设计服务器上,无人值守跑了 11 个月,自动生成日报邮件,没人工干预过一次。技术不难,难的是把「能跑」变成「敢托付」。希望帮到你。
本文还有配套的精品资源,点击获取