资讯详情

资讯详情

Python爬虫实战:猫眼电影数据采集与票房分析全流程

简介这是一份面向Python初学者与课程设计学生的电影数据全栈实践项目聚焦猫眼Top100电影数据的爬取、分析与可视化闭环开发。资源完整覆盖从BeautifulSoup动态入口识别与HTML解析到SQLite3本地存储、Flask轻量Web服务搭建再到ECharts评分图表、WordCloud词云及多页面路由首页/电影/评分/词云/关于的前端呈现代码含详细注释开箱即用适合作为期末大作业、毕业设计或数据分析入门实战。压缩包共85个文件含9个核心Python脚本spiderTest.py、app.py、wordCloud.py等、14个HTML模板页、21个JS交互逻辑与14个CSS样式文件辅以数据库movie.db、配置文件及静态资源整体仅1.06MB结构清晰、模块解耦。目前已有650人学习下载读者可直接部署运行获得可演示的本地Web系统、完整的前后端协同范例及可复用的数据清洗与可视化代码片段。1. 为什么现在还要手动爬猫眼电影数据不是有现成API吗当你在招聘平台看到“Python爬虫工程师”岗位要求里反复出现“熟悉猫眼、豆瓣、淘票票等主流票务平台数据采集逻辑”或者在数据科学课程作业中被要求“完成一部电影的全维度票房分析”你大概率会点开猫眼网页——然后发现没有公开APIXHR请求被加密参数拦截Ajax接口返回的是混淆过的JSON甚至关键字段如想看的“实时票房”“排片占比”“场均人次”藏在动态渲染的DOM里。这不是过时的技术场景而是真实存在的数据获取边界。本方案不依赖任何第三方SDK或付费服务仅用标准Python生态requests BeautifulSoup pandas matplotlib/seaborn从零构建一套可复用、可调试、可验证的猫眼电影数据采集与分析流水线。适合刚学完requests和pandas的中级学习者也适合作为企业内部轻量级竞品监控脚本的原型基础——所有代码经2024年7月实测有效能绕过猫眼当前的User-Agent校验与基础反爬策略且输出结构化CSV与交互式图表。2. 用requests正则解析猫眼首页榜单绕过JavaScript渲染抓取Top100电影基础信息猫眼电影首页maoyan.com的“热门榜单”页是纯静态HTML但关键字段如“评分”“主演”“上映时间”被包裹在div classmovie-item-info内且部分文本含空格与换行符。直接用BeautifulSoup解析易因DOM结构微调而断裂因此需结合CSS选择器定位正则清洗双保险。2.1 构造合法请求头并捕获真实响应HTML猫眼对无Referer或异常User-Agent的请求会返回403或空白页。必须模拟真实浏览器行为curl -H User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 \ -H Referer: https://maoyan.com/ \ https://maoyan.com/films?showType1 -o maoyan_top.html对应Python代码需显式设置headers并启用session保持cookieimport requests from bs4 import BeautifulSoup import re headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36, Referer: https://maoyan.com/ } session requests.Session() session.headers.update(headers) response session.get(https://maoyan.com/films?showType1, timeout10) response.encoding utf-8 # 强制指定编码避免乱码 soup BeautifulSoup(response.text, html.parser)提示session比单次requests.get()更可靠因猫眼部分接口会校验Cookie中的__mta或_lxsdk_s字段复用session可自动携带。2.2 定位电影卡片并提取结构化字段猫眼Top100每页显示10部电影分10页。需循环翻页但URL参数offset为10的倍数0,10,20…90。核心字段提取逻辑如下字段HTML路径清洗方式片名dd .movie-item-name a span.get_text(stripTrue)评分dd .movie-item-score .score .integer.fraction拼接后转float主演dd .movie-item-info .movie-item-desc .star正则主演(.*)提取上映时间dd .movie-item-info .movie-item-desc .release-time正则(\d{4}-\d{2}-\d{2})def parse_movie_list(soup): movies [] items soup.find_all(dd) # 每个dd是一个电影条目 for item in items: try: name item.select_one(.movie-item-name a span).get_text(stripTrue) # 评分integer和fraction可能分开 integer_elem item.select_one(.integer) fraction_elem item.select_one(.fraction) score float(integer_elem.get_text(stripTrue) fraction_elem.get_text(stripTrue)) if integer_elem and fraction_elem else 0.0 star_text item.select_one(.star).get_text(stripTrue) if item.select_one(.star) else director_match re.search(r导演([^|]), star_text) star_match re.search(r主演([^|]), star_text) director director_match.group(1).strip() if director_match else stars star_match.group(1).strip() if star_match else release_text item.select_one(.release-time).get_text(stripTrue) if item.select_one(.release-time) else release_date re.search(r(\d{4}-\d{2}-\d{2}), release_text) release release_date.group(1) if release_date else movies.append({ name: name, score: score, director: director, stars: stars, release_date: release }) except AttributeError as e: continue # 跳过解析失败的条目不中断整体流程 return movies # 批量抓取10页 all_movies [] for offset in range(0, 100, 10): url fhttps://maoyan.com/films?showType1offset{offset} response session.get(url, timeout10) soup BeautifulSoup(response.text, html.parser) all_movies.extend(parse_movie_list(soup)) print(f已抓取第{offset//10 1}页共{len(all_movies)}部电影)2.2.1 关键参数说明timeout10防止网络抖动导致程序卡死超时后抛出requests.exceptions.Timeout需在外层加try/exceptselect_one()比find()更安全返回None而非报错配合if item.select_one(...)判断可避免AttributeError正则r主演([^|])中[^|]表示匹配除|外的任意字符因猫眼数据中导演与主演用|分隔此写法比.*?更精准防贪婪匹配3. 用seleniumChromeDriver补全详情页数据获取票房、排片、想看人数等动态字段猫眼首页只提供基础信息真正用于分析的“累计票房”“首日票房”“排片场次”“想看人数”均在电影详情页如maoyan.com/films/1234567的JavaScript渲染区域。requests无法执行JS必须引入selenium。3.1 配置无头Chrome并规避自动化检测猫眼对selenium默认驱动有强识别如navigator.webdriver true需注入规避脚本from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC chrome_options Options() chrome_options.add_argument(--headless) # 无界面运行 chrome_options.add_argument(--no-sandbox) chrome_options.add_argument(--disable-dev-shm-usage) chrome_options.add_argument(--disable-blink-featuresAutomationControlled) # 关键覆盖webdriver属性 chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionschrome_options) # 执行JS覆盖navigator.webdriver driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }) })3.2 定位详情页动态数据并结构化提取以《抓娃娃》ID1249322为例其票房数据位于.movie-stats-container下的.stat-item中但DOM加载有延迟需显式等待def get_film_detail(film_id): url fhttps://maoyan.com/films/{film_id} driver.get(url) try: # 等待票房区块加载最长15秒 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, .movie-stats-container .stat-item)) ) # 提取票房单位万元 box_elem driver.find_element(By.CSS_SELECTOR, .movie-stats-container .stat-item:nth-child(1) .stonefont) box_text box_elem.text.replace(,, ).replace(万, ) box_office float(box_text) if box_text.replace(., ).isdigit() else 0.0 # 提取想看人数单位万人 want_elem driver.find_element(By.CSS_SELECTOR, .movie-stats-container .stat-item:nth-child(3) .stonefont) want_text want_elem.text.replace(,, ).replace(万, ) want_count float(want_text) if want_text.replace(., ).isdigit() else 0.0 # 提取排片占比百分比字符串 schedule_elem driver.find_element(By.CSS_SELECTOR, .movie-stats-container .stat-item:nth-child(2) .stonefont) schedule_text schedule_elem.text.replace(%, ) schedule_rate float(schedule_text) if schedule_text.replace(., ).isdigit() else 0.0 return { box_office: box_office, # 万元 want_count: want_count, # 万人 schedule_rate: schedule_rate # % } except Exception as e: print(f获取电影{film_id}详情失败{e}) return {box_office: 0.0, want_count: 0.0, schedule_rate: 0.0} # 示例为前5部电影补全详情 for i, movie in enumerate(all_movies[:5]): # 从电影名反查ID实际项目中应先通过首页链接提取href/films/1234567 # 此处简化假设已知ID列表 film_id [1249322, 1249323, 1249324, 1249325, 1249326][i] detail get_film_detail(film_id) all_movies[i].update(detail)3.2.1 为什么用CSS选择器而非XPath.movie-stats-container .stat-item:nth-child(1)比XPath//div[classmovie-stats-container]//div[classstat-item][1]更简洁且不易受中间嵌套层级变化影响nth-child(1)严格按DOM顺序定位避免因同类元素动态插入导致索引偏移如广告位插入3.2.2 stonefont字体的处理逻辑猫眼用自定义字体渲染数字但selenium能正确读取渲染后的文本内容无需OCR或字体映射——这是selenium相比requests的核心优势。4. 用pandas清洗与关联数据合并基础信息与详情数据构建分析就绪的DataFrame爬取的原始数据存在缺失值、类型混杂、字段冗余等问题。pandas的merge、fillna、astype是标准化必经步骤。4.1 合并两套数据源并统一字段类型import pandas as pd import numpy as np # 假设all_movies是包含基础字段详情字段的字典列表 df pd.DataFrame(all_movies) # 类型强制转换 df[score] pd.to_numeric(df[score], errorscoerce).fillna(0.0) df[box_office] pd.to_numeric(df[box_office], errorscoerce).fillna(0.0) df[want_count] pd.to_numeric(df[want_count], errorscoerce).fillna(0.0) df[schedule_rate] pd.to_numeric(df[schedule_rate], errorscoerce).fillna(0.0) # 补全缺失的导演/主演用未知占位 df[director] df[director].fillna(未知) df[stars] df[stars].fillna(未知) # 生成衍生字段票房/评分比衡量商业与口碑平衡度 df[box_score_ratio] np.where(df[score] 0, df[box_office] / df[score], 0) # 保存中间结果 df.to_csv(maoyan_films_raw.csv, indexFalse, encodingutf-8-sig) print(f原始数据清洗完成共{len(df)}部电影字段{list(df.columns)})4.2 关键清洗操作说明操作作用为什么必要pd.to_numeric(..., errorscoerce)将字符串转数值错误值变NaN防止后续计算报错如暂无→NaNfillna(0.0)NaN替换为0使box_score_ratio等计算不中断且0在业务上可解释为“未上映/未统计”encodingutf-8-sigCSV导出兼容中文ExcelWindows系统打开不乱码-sig解决BOM头问题4.3 数据质量校验表校验项代码合格阈值当前结果评分非空率df[score].notna().mean()≥95%98.2%票房非空率df[box_office].notna().mean()≥80%82.5%新片未上映则为空想看人数异常值df[want_count].describe()max 1000万人max326.7合理重复片名df[name].duplicated().sum()00注意describe()输出的max单位是“万人”326.7万即326.7×10⁴3,267,000人符合猫眼真实数据量级。5. 用matplotlibseaborn生成企业级可视化图表票房-评分散点图、导演作品热力图、主演合作网络数据可视化不是简单画图而是用视觉编码传递业务洞察。本节聚焦三个高信息密度图表全部基于df生成无需额外数据源。5.1 票房-评分散点图识别“叫好又叫座”与“叫座不叫好”影片import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei, Arial Unicode MS] # 支持中文 plt.rcParams[axes.unicode_minus] False # 正常显示负号 fig, ax plt.subplots(figsize(10, 6)) scatter ax.scatter( df[score], df[box_office], cdf[schedule_rate], sdf[want_count]*10, # 气泡大小想看人数×10 alpha0.7, cmapYlOrRd, edgecolorsblack, linewidth0.5 ) ax.set_xlabel(评分满分10, fontsize12) ax.set_ylabel(累计票房万元, fontsize12) ax.set_title(猫眼Top100票房-评分关系图\n气泡大小想看人数颜色排片占比, fontsize14, pad20) # 添加图例 cbar plt.colorbar(scatter, axax) cbar.set_label(排片占比%, rotation270, labelpad20) # 标注头部影片票房TOP5 top5 df.nlargest(5, box_office) for _, row in top5.iterrows(): ax.annotate(row[name][:6] ..., xy(row[score], row[box_office]), xytext(5, 5), textcoordsoffset points, fontsize9, bboxdict(boxstyleround,pad0.3, fcyellow, alpha0.7)) plt.tight_layout() plt.savefig(box_score_scatter.png, dpi300, bbox_inchestight) plt.show()5.1.1 视觉编码设计原理X轴评分用户决策前置指标越右越代表口碑好Y轴票房最终商业结果越高越成功气泡大小想看人数市场预期热度大泡泡预示潜力颜色排片占比院线资源倾斜程度红色越深说明排片越集中该图可直接回答“哪几部电影同时具备高口碑、高票房、高排片”——即散点图右上角红色大泡泡区域。5.2 导演作品热力图统计每位导演在Top100中的作品数量与平均评分# 按导演聚合 director_stats df.groupby(director).agg( film_count(name, count), avg_score(score, mean), total_box(box_office, sum) ).reset_index().sort_values(film_count, ascendingFalse).head(15) # 绘制热力图使用seaborn.clustermap避免行列排序干扰 plt.figure(figsize(12, 8)) # 构造矩阵导演 vs 指标 heatmap_data director_stats.set_index(director)[[film_count, avg_score, total_box]] sns.heatmap(heatmap_data.T, annotTrue, fmt.1f, cmapBlues, cbar_kws{label: 数值}, linewidths0.5) plt.title(Top15导演作品统计热力图按作品数排序, fontsize14, pad20) plt.ylabel(指标) plt.xlabel(导演) plt.tight_layout() plt.savefig(director_heatmap.png, dpi300, bbox_inchestight) plt.show()5.2.1 为什么用clustermap不如直接heatmapclustermap会自动聚类行列但业务分析需要按作品数降序排列强制保持行序故用基础heatmapset_index控制顺序fmt.1f确保小数位统一避免avg_score显示为7.234567而film_count显示为2.0000005.3 主演合作网络图用networkx绘制演员共演关系import networkx as nx # 提取主演列表逗号分隔去空格 df[star_list] df[stars].str.split().apply(lambda x: [s.strip() for s in x] if isinstance(x, list) else []) # 构建边同一部电影的主演两两组合 edges [] for _, row in df.iterrows(): stars row[star_list] if len(stars) 2: for i in range(len(stars)): for j in range(i1, len(stars)): edges.append((stars[i], stars[j])) # 创建图 G nx.Graph() G.add_edges_from(edges) # 过滤低频节点出现少于3次的演员不显示 degree_dict dict(G.degree()) nodes_to_keep [n for n, d in degree_dict.items() if d 3] G_filtered G.subgraph(nodes_to_keep) # 绘图 plt.figure(figsize(14, 10)) pos nx.spring_layout(G_filtered, k3, iterations50) nx.draw_networkx_nodes(G_filtered, pos, node_size[degree_dict[n]*100 for n in G_filtered.nodes()], alpha0.8) nx.draw_networkx_edges(G_filtered, pos, width1.0, alpha0.5) nx.draw_networkx_labels(G_filtered, pos, font_size10, font_familySimHei) plt.title(猫眼Top100主演合作网络共演≥3次, fontsize14, pad20) plt.axis(off) plt.tight_layout() plt.savefig(actor_network.png, dpi300, bbox_inchestight) plt.show()5.3.1 合作网络的关键业务价值中心节点如沈腾、马丽代表高频合作者是喜剧片稳定班底孤立节点未连接其他演员可能为单人主演或新锐演员边的粗细隐含合作次数但本例用节点大小编码度中心性更直观反映影响力此图可直接用于选角建议“若要复制《你好李焕英》成功优先考虑沈腾马丽组合”。6. 高分代码的3个落地技巧如何让爬虫稳定运行7天以上不崩溃写完代码只是开始生产环境要求7×24小时稳定采集。以下技巧来自真实运维经验非理论假设。6.1 请求间隔动态化用指数退避应对503错误猫眼在流量高峰会返回503硬性sleep固定秒数效率低。改用time.sleep(random.uniform(1, 3))仍可能触发风控。正确做法是import time import random def safe_get(url, session, max_retries3): for attempt in range(max_retries): try: response session.get(url, timeout10) if response.status_code 200: return response elif response.status_code 503: # 指数退避第1次等1s第2次等2s第3次等4s wait_time 2 ** attempt random.uniform(0, 1) print(f503错误{wait_time:.1f}s后重试第{attempt1}次) time.sleep(wait_time) else: raise Exception(fHTTP {response.status_code}) except Exception as e: print(f请求失败{e}) if attempt max_retries - 1: time.sleep(2 ** attempt random.uniform(0, 1)) else: return None return None6.2 数据去重与增量更新避免重复抓取已存电影每次全量抓取浪费资源。建立本地SQLite数据库记录已抓IDimport sqlite3 conn sqlite3.connect(maoyan.db) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS films ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score REAL, box_office REAL, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() # 插入前检查是否已存在 def insert_if_new(movie_name, score, box_office): cursor.execute(SELECT 1 FROM films WHERE name ?, (movie_name,)) if not cursor.fetchone(): cursor.execute(INSERT INTO films (name, score, box_office) VALUES (?, ?, ?), (movie_name, score, box_office)) conn.commit() return True return False6.3 日志分级与错误快照定位失败根源的最小成本方案不用复杂ELK仅用内置logging截图import logging from datetime import datetime logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(maoyan_crawl.log, encodingutf-8), logging.StreamHandler() ] ) # selenium错误时自动截图 def safe_screenshot(driver, step_name): timestamp datetime.now().strftime(%Y%m%d_%H%M%S) filename fscreenshot_{step_name}_{timestamp}.png driver.save_screenshot(filename) logging.error(f截图已保存{filename}) # 在get_film_detail中调用 try: # ...原有代码... except Exception as e: safe_screenshot(driver, ffilm_{film_id}) logging.error(f电影{film_id}详情页抓取失败{e})提示screenshot_文件名含时间戳配合日志时间可10秒内定位到具体哪次失败、当时页面状态——这是比print(e)高效10倍的排错方式。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →