
简介本资源是一套完整的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 请求直接返回 403User-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/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en-US;q0.8,en;q0.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?showType1采用标准卡片式布局每部影片包裹在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, limit10): # 只取前10部避免翻页 try: # 片名位于 div classchannel-detail movie-item-title 下的 a title_tag dd.find(div, class_movie-item-title).find(a) title title_tag.get_text(stripTrue) if title_tag else # 评分div classchannel-rating 下的 i 标签文本形如 9.5 rating_tag dd.find(div, class_channel-rating).find(i) rating float(rating_tag.get_text(stripTrue)) if rating_tag and rating_tag.get_text(stripTrue).replace(., ).isdigit() else 0.0 # 主演位于 div classmovie-item-info 下的 div classmovie-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(stripTrue).replace(主演, ) if len(actors_p) 1 else # 导演同上第1个 p需正则提取导演xxx director_match re.search(r导演(.?)$, actors_p[0].get_text(stripTrue)) if len(actors_p) 0 else None director director_match.group(1) if director_match else # 票房位于 div classchannel-detail movie-item-title 同级的 div classchannel-detail movie-item-number box_div dd.find(div, class_movie-item-number) box_text box_div.get_text(stripTrue) 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 classreleasetime格式如 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(stripTrue)).group(1) if time_tag else # 评论数位于 p classscore 后的 p classcomments文本如 12.3万人评分 comment_tag dd.find(p, class_comments) comment_text comment_tag.get_text(stripTrue) 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?showType1 response requests.get(url, headersheaders, timeout10) if response.status_code 200: data parse_hot_films(response.text) print(f成功解析 {len(data)} 部影片) else: print(f请求失败状态码: {response.status_code})逻辑说明limit10是防止单次请求过长实际部署时可配合time.sleep(1)控制频率box_office字段做了单位归一化处理“12.3亿” →123000000.0“2345万” →23450000.0后续分析无需再转换comments字段同样归一为整数便于做离散分桶如1000,1000-10000,10000所有.get_text(stripTrue)后都加了空值判断避免None报错。3. 把原始 HTML 数据存成结构化 CSVpandas 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)), encodingutf-8-sig, # ✅ 必选解决 Excel 中文乱码 indexFalse, # ✅ 必选不写行号保持纯数据 na_repNULL, # ✅ 必选把 None/NaN 替换为字符串 NULL避免 Excel 显示 #N/A date_format%Y-%m-%d # ✅ 必选规范日期格式防止 Excel 自动转成数字 )参数说明encodingutf-8-sigBOM 头兼容模式Windows Excel 默认识别 UTF-8-BOM而普通utf-8会被误判为 ANSIindexFalseDataFrame 默认带行索引列0,1,2…导出 CSV 时若不关掉Excel 第一列会多出一列序号干扰后续分析na_repNULL爬取过程中某些字段如小众影片导演可能为空pandas 默认存为NaNExcel 打开显示#N/A业务方看不懂设为NULL后所有空值统一为可读字符串date_format%Y-%m-%drelease_date是字符串但若后续要做时间序列分析如按月统计票房需先pd.to_datetime()转为 datetime 类型此时统一格式可避免解析失败。3.2 为什么不用 JSONCSV 在这个场景下比 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-AgentRefererIP三元组若同一 IP 在 1 分钟内发出 10 次相同 UAReferer 组合触发风控。即使你用了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 classmovie-item-number内容为空或为占位符如-真实数据藏在script标签的window.__DATA__变量里。解决改用re.search(rwindow\.__DATA__ (.*?);, html, re.DOTALL)提取 JSON 字符串再json.loads()解析。注意该方式需import json且__DATA__结构随页面改版可能变化建议同时 fallback 到 DOM 解析。4.3 现象actors字段提取出“主演张译,李晨,...”但director提取为空原因猫眼对导演和主演的 HTML 结构做了差异化处理——导演信息在p classname下主演在p classstar下且有时导演行被注释掉或缺失。原代码用actors_p[0]硬取未做标签类型判断。解决改用dd.find(p, class_name)和dd.find(p, class_star)分别定位再.get_text(stripTrue)。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], errorscoerce)再用to_csv(date_format%Y-%m-%d)。4.5 现象程序跑着跑着突然卡住CPU 占用 100%日志无报错原因requests.get()默认无限等待响应当猫眼服务器响应超慢30s或断连时线程挂起。BeautifulSoup解析超大 HTML如 Top100 页时内存暴涨未设featureslxml导致解析器退化为 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( datadf_clean, xrating, ybox_office, alpha0.6, s50, edgecolornone ) # 添加线性拟合线seaborn 自动计算 sns.regplot( datadf_clean, xrating, ybox_office, scatterFalse, colorred, 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], binsbins, rightFalse) 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], width0.2, alpha0.3, colorsteelblue) plt.xlabel(评分猫眼) plt.ylabel(票房元) plt.title(猫眼评分 vs 票房高分未必高票房但低分一定低票房) plt.grid(True, alpha0.3) plt.tight_layout() plt.savefig(output/rating_vs_boxoffice.png, dpi300, bbox_inchestight) 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(,, expandTrue).stack().reset_index(level1, dropTrue).rename(genre) df_genre_count df_genres.value_counts().reset_index(namecount) # 绘制环形图 fig px.pie( df_genre_count, namesgenre, valuescount, hole0.4, # 中心挖空形成环形 title猫眼热映影片类型分布2024Q1, labels{genre: 类型, count: 影片数} ) # 设置悬停模板显示百分比和绝对值 fig.update_traces( hovertemplateb%{label}/bbr影片数: %{value}br占比: %{percent:.1%}extra/extra ) # 保存为 HTML支持交互 fig.write_html(output/genre_donut.html) fig.show() # 在 Jupyter 中直接渲染注意hole0.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], binsnp.arange(5.0, 10.5, 0.5), labels[f{i}-{i0.5} for i in np.arange(5.0, 10.0, 0.5)], rightFalse ) # 生成交叉表 heatmap_data pd.crosstab(df_clean[month], df_clean[rating_bin]) # 绘图 plt.figure(figsize(12, 6)) sns.heatmap( heatmap_data, annotTrue, fmtd, cmapYlGnBu, cbar_kws{label: 影片数量} ) plt.title(2024年各月份上映影片评分分布热力图) plt.xlabel(评分区间) plt.ylabel(月份) plt.tight_layout() plt.savefig(output/month_rating_heatmap.png, dpi300, bbox_inchestight) 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( stopstop_after_attempt(3), # 最多重试3次 waitwait_exponential(multiplier1, min2, max10) # 指数退避2s, 4s, 8s ) def fetch_page(url): response requests.get(url, headersheaders, 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_plotlyjscdn改为directory并提前下载离线包或更简单——用include_plotlyjsrequirejsconfig{responsive: True}保证适配手机。PNG 用于微信汇报、PPT 插入HTML 用于内部看板嵌入二者缺一不可。最后说句血泪经验我最早写的猫眼脚本跑三天就崩一次不是反爬就是解析错位。后来把 retry、时间戳、离线 HTML 这三条焊死在骨架里现在这套代码在某高校课程设计服务器上无人值守跑了 11 个月自动生成日报邮件没人工干预过一次。技术不难难的是把「能跑」变成「敢托付」。希望帮到你。本文还有配套的精品资源点击获取