☰
Python爬取猫眼数据与SVR回归在票房预测中的实战方法
2026/10/12 23:17:51 网站建设 项目流程

简介:票房预测是电影行业数据分析中的典型应用场景,但由于样本量有限且特征关系复杂,传统线性回归难以捕捉非线性模式。支持向量回归(SVR)通过核函数将低维特征映射到高维空间,在中小规模数据集上表现出稳定且泛化能力强的优势。实际工程中,需结合Python爬虫从猫眼等平台采集影片基本信息与市场表现数据,并完成数据清洗、单位标准化、类别特征编码等预处理步骤。在特征工程中,合理使用目标编码、时间序列切分和相关性筛选,能有效避免数据泄露与过拟合。通过网格搜索关键参数(C、epsilon、gamma)并采用时序交叉验证,可构建具备可信度的票房预测模型。这种方法不仅适用于猫眼数据,也为影视内容排期、营销策略等业务决策提供数据支撑。

1. 用Python去爬猫眼电影数据,再交给SVR回归器做票房预测,是很多数据分析团队的常见起步方案

一部电影上映前一周,制片方拿到的预估票房是内部决策的重要依据。直接用线性回归,票房和特征之间复杂的非线性关系拟合不足;上深度学习,几千条样本规模根本不给你机会。SVR(支持向量回归)刚好卡在中间:它靠核函数把特征映射到高维空间,对中小样本回归任务非常稳。这篇文章要讲的,就是怎么用Python把猫眼电影数据爬下来,做完特征分析,再用SVR回归器搭出一套票房预测系统,从代码到参数、从坑到调优,一步步讲清楚。适合正在做数据分析项目、课程设计或想转行做预测建模的从业者。

2. 从猫眼抓数据:爬虫设计、反爬应对与数据清洗

2.1 先从票房预测倒推字段清单,别把页面能看见的全抓了

很多新手写爬虫的第一步就是打开猫眼页面,看到什么抓什么。等爬到一半才发现,真正建模要用的是字段之间的关系,以及字段的时间口径。做票房预测,至少要分清三类字段。

第一类是影片基础属性,比如片名、类型、制片地区、导演、主演、上映日期。第二类是上映后的表现,包括首日票房、首周票房、累计票房、上映天数、评分、想看人数。第三类是外部环境因素,比如档期、同档期竞争影片数量。这里有个容易被忽略的边界:上映前做预测,只能用到第一类和第三类,第二类完全不可用。你不可能在电影上映前就知道首日票房,这是很多课程设计一上来就翻车的根源。

我一般会把抓取拆成两个阶段:先抓上映前的静态属性,再按日期抓每日票房数据,最后通过电影ID关联。这样同一个ID在不同阶段累积的数据都保留下来,特征工程阶段才有得选。下面这张字段表是我常用的一份清单,建模用途和时间口径都标好了:

字段类型建模用途可用时点
片名文本名称实体上映前
类型类别独热编码上映前
导演/主演类别历史票房均值上映前
上映日期日期距离开映天数上映前
想看人数数值热度信号上映前
首日票房数值早期热度上映后
累计票房数值回归目标上映后
评分数值口碑信号上映后

这张表直接决定了爬虫的解析范围。把网页里的非结构化字段整理成上面这样的结构化数据,是爬虫环节的核心目标。数据库用sqlite3就够了,这是Python连接数据库最轻的方案,后面要换MySQL或者Oracle,只需要改连接对象,清洗和建模代码不用动。

2.2 爬虫核心代码:请求、解析、入库一步到位

定好字段后开始写爬虫。猫眼有Web端页面也有App接口,常见的做法是先请求网页版列表页拿到电影ID,再请求详情页把类型、导演、演员、评分这些字段补齐。新项目直接用requests,解析用lxml配合BeautifulSoup。这里给一个完整可跑的清单页抓取骨架:

import requests import pandas as pd import sqlite3 import time from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.maoyan.com/", } def fetch_movie_list(offset=0): """抓取猫眼电影列表页,返回电影的ID列表""" url = "https://www.maoyan.com/films?showType=2&offset={}".format(offset) try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() except requests.RequestException as e: print("[请求失败] offset={}: {}".format(offset, e)) return [] soup = BeautifulSoup(resp.text, "lxml") items = [] for a in soup.select("a.movie-poster-data"): movie_id = a.get("data-val") if movie_id: items.append({ "movie_id": movie_id, "fetched_at": time.strftime("%Y-%m-%d %H:%M:%S") }) return items def save_to_sqlite(items, db_path="movie.db"): conn = sqlite3.connect(db_path) df = pd.DataFrame(items) df.to_sql("movie_list", conn, if_exists="append", index=False) conn.close() if __name__ == "__main__": for offset in range(0, 100, 10): items = fetch_movie_list(offset) if not items: break save_to_sqlite(items) time.sleep(1.5)

这段代码的逻辑是:猫眼列表页按offset翻页,每页固定条数;返回的data-val属性就是猫眼内部电影ID,后面所有详情请求都要拿这个ID去拼URL。HEADERS里的User-Agent和Referer必须带上,空UA很容易被拦截。sleep时间从1.5秒起,如果触发限流就加大到3秒,这段操作在调试时别省。

fetch_movie_list里有个细节值得说明:列表页的数据密度太低,只有海报链接里的电影ID,所以还要再补一个详情页请求。详情页解析代码大概长这样:

def fetch_movie_detail(movie_id): url = "https://www.maoyan.com/films/{}".format(movie_id) try: resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() except requests.RequestException: return None soup = BeautifulSoup(resp.text, "lxml") # 先检查是否被反爬页拦截,避免灾难性空解析 if "验证" in resp.text: return {"movie_id": movie_id, "error": "blocked"} row = {"movie_id": movie_id} type_el = soup.select_one(".type-name") if type_el: row["genres"] = type_el.get_text(strip=True) actors = [] for name_el in soup.select(".actor-name a"): actors.append(name_el.get_text(strip=True)) row["actors"] = "/".join(actors[:3]) return row

这里有两个点要牢记。第一,猫眼页面结构改版比较频繁,CSS选择器只反映写稿时的结构,落地时如果发现选择器失效,优先检查返回的HTML是不是反爬验证页,而不是怀疑解析代码。第二,详情字段要单独存一张表,用movie_id和列表表关联,这样某个字段在部分影片上缺失时,不会影响其他记录的完整性。

在VS Code里运行之前,先确认解释器选对了项目虚拟环境,如果解释器指向了conda的base环境而项目依赖装在虚拟环境里,import bs4会直接报错,这是用VS Code配置Python开发环境时最典型的坑。

2.3 数据清洗:票房单位、日期口径、重复记录一起处理

爬下来的数据至少要过三关。

第一关是重复记录。列表页翻页偶尔会返回和上一页相同的电影,用movie_id做去重,建表时给movie_id加唯一索引是最稳的。第二关是单位统一。猫眼网页上显示的票房经常是“1.2亿”或“8600万”这种字符串,直接把字符串丢给SVR会因为类型转换失败而报错。这里有一个我一直在用的解析函数:

def parse_box_office(text): """把'1.2亿''8600万'统一换算成万元""" if not isinstance(text, str): return None text = text.strip() if "亿" in text: return float(text.replace("亿", "")) * 10000 if "万" in text: return float(text.replace("万", "")) if text == "" or text == "暂无": return None return float(text)

第三关是日期对齐。猫眼详情页给的日期格式可能是“2024-05-01”,也可能是“2024年5月1日”,统一转成datetime,并计算出“距开映天数”和“所在档期”两个新列。这两个列后面在特征分析阶段直接用得上,比在建模时现算省事。

字段缺失的处理也有讲究。导演或演员缺失,先填充“未知”再入库,而不是直接删行。SVR对缺失值敏感,删行会在小样本条件下直接损失训练数据,后期每一条样本都弥足珍贵。清洗干净后如果想导出Excel副本给业务方看,一行df.to_excel("clean_movie.xlsx")就能搞定,这一步能省去很多不必要的口舌之争。

3. 特征分析:把影片属性变成SVR能学的特征矩阵

3.1 从原始字段里能挖出哪些有效特征

清洗完的数据还在数据库里待着,SVR只认数值矩阵,所以特征分析的核心任务,是把数据库字段变成有解释力的数值特征。这个环节决定模型上限,后面SVR参数调得再好也补不回糟糕的特征。

我常用的特征体系分成三组。第一组是基础数值特征:片长、评分、想看人数、预告片播放量。第二组是类别特征:类型、制片地区、档期、是否系列片、是否IP改编,类别特征需要编码后才能进模型。第三组是衍生统计特征:导演上一部电影的票房均值、主演出演过的最高票房电影、同档期竞争影片数量。

衍生特征在票房预测里作用最大。一个导演过去三部电影的平均票房,比单纯把导演名字做成one-hot更符合SVR需求。名字one-hot在样本少时会产生大量稀疏列,而且完全没有泛化能力——新导演的名字不在训练集里就直接失效。统计特征基于历史数据计算,新导演也能有值,比如用全体导演的均值做填充。

同档期竞争影片数量这个特征要按上映日滚动统计,这是特征分析环节最容易出数据泄露的地方。某部电影上映时的竞争格局,只包含它上映日前后一段时间内开画的电影,绝不能把整年所有影片都算进去。我一般会写一个窗口函数,以当前电影上映日为锚点,统计前后7天内的开画数量。

3.2 类别特征编码实操:one-hot与target encoding怎么选

类别特征进入SVR之前有几种编码方式可选。最常见的是pandas的get_dummies直接做one-hot,简单粗暴:

def encode_categorical(df, category_cols): df_encoded = pd.get_dummies(df, columns=category_cols, dummy_na=False) return df_encoded

get_dummies用起来方便,但有两个现实问题。第一,高基数的类别会产生大量稀疏列。演员列如果有300个不同名字,one-hot后就是300列,而训练集只有几百条样本,SVR瞬间陷入高维稀疏困境。第二,对未来数据不友好:测试集里出现新演员,训练好的模型会因列数不一致而报错。

所以我更常使用目标编码,把类别转成该类别历史上票房目标列的均值:

def target_encode(train_df, col, target="total_box_office"): """目标编码:用类别在训练集上的票房均值作为新特征""" mapping = train_df.groupby(col)[target].mean() train_df[col + "_target"] = train_df[col].map(mapping) return train_df, mapping train_df, actor_mapping = target_encode(train_df, "actor1") test_df["actor1_target"] = test_df["actor1"].map(actor_mapping) test_df["actor1_target"] = test_df["actor1_target"].fillna( train_df["actor1_target"].mean() )

目标编码的坑是容易过拟合,解决办法是加平滑系数,或者只在类别出现次数大于等于5时生效,否则回归到全局均值。在样本量不到2000条的票房数据上,我会限制目标编码只对高频类别生效,低频类别一律用全体均值填充。

日期和档期这类有序特征,更建议直接转成连续数值。档期离春节越近,热度越高,本质上是有序变量,硬做one-hot会丢掉顺序关系。SVR计算样本距离时,one-hot对每一维平等对待,顺序信息被彻底抹平,模型也就学不到“元旦档比普通周末更能拉票房”这层规律。

3.3 相关性检查与特征筛选:别把无关噪声喂给模型

特征做完后别急着开训,先做一轮相关性检查,把明显重复或强相关的特征去掉。强相关特征在SVR里不会像线性回归那样引发严重共线性问题,但会稀释样本在特征空间中的密度,让模型在同等样本量下学得更吃力。

常用的检查套路是计算皮尔逊相关系数矩阵,把相关系数绝对值大于0.9的特征对挑出来。最典型的是首周票房和累计票房,二者相关系数常常在0.95以上,这时候只保留首周票房作为上映后阶段的代表特征就够了。

import pandas as pd import numpy as np def check_correlation(feature_df, threshold=0.9): corr = feature_df.corr().abs() upper = corr.where(np.triu(np.ones(corr.shape), k=1).astype(bool)) to_drop = [col for col in upper.columns if any(upper[col] > threshold)] return to_drop

跑完这个函数后,要人工过一遍待删列,确认重复信息来自哪个业务字段再决定删哪个。“首日票房”和“首周票房”高度相关,但业务上首周票房更受口碑影响,保留哪个取决于你的预测时点。

除了相关系数,我还会看每个特征与票房目标的单变量相关性,把绝对值低于0.05的特征列去掉。这个操作不是为了提升SVR拟合能力,而是为了缩短训练时间、降低过拟合风险。特征分析做完后,把最终特征列固定成一个清单文件,后续训练和部署都按这份清单读取,避免实验阶段反复改特征列导致结果不可比。绘制特征相关性热力图时记得控制横坐标密度,票房数据跨度大,直接画图会出现横坐标太密集的问题,设置plt.xticks(rotation=45)和适当步长能缓解。

4. SVR回归器的参数选择与训练验证流程

4.1 C、epsilon、gamma:SVR三个核心参数的直观理解

开始跑模型前,先确认环境里装好了scikit-learn,也就是常见的python安装sklearn库这一步。没装的话一行pip install scikit-learn就搞定,装完再import sklearn检查版本。

SVR做的事情,是在特征空间里找一条回归曲线,让大部分样本点落在以该曲线为中心、宽度为2倍epsilon的管道内。管道宽度决定对误差的容忍程度,三个参数直接影响最终模型行为,必须理解了再调。

C是惩罚系数,控制对管道外样本的惩罚力度。C越大,模型越努力拟合每一个样本点,代价是过拟合;C太小,模型过度平滑,连明显的趋势都抓不住。epsilon是管道宽度,epsilon越大,允许误差越大,模型越平缓,但会丢失细节模式。gamma只在RBF核下有意义,它控制单个样本的影响半径,gamma越小,决策边界越平滑;gamma越大,样本点附近的变化越剧烈。

票房数据属于典型的中间样本量、噪声偏大的回归问题。经验取值方向是:kernel优先用RBF,C从10起步,epsilon在0.01到0.1之间试,gamma不要用默认的auto,直接设定为1除以特征数。这个基准组合能保证第一轮训练不会跑偏,后续再按验证结果微调。

4.2 时序切分验证:票房预测不能做随机打散

这一类项目里最致命的错误,是用train_test_split默认的随机切分来评估模型。票房数据有强烈的时间属性,2023年的电影和2024年的电影在特征分布上并不独立。如果把打散后的样本放进验证集,模型相当于提前看到了部分未来信息,验证分数虚高,一部署就原形毕露。

正确做法是按时序切分:训练集用前80%时间段的电影,验证集用后20%时间段的电影,并且保证训练集里所有电影的上映日期都早于验证集里最早的上映日期。

from sklearn.svm import SVR from sklearn.metrics import mean_absolute_error, r2_score from sklearn.preprocessing import StandardScaler df_sorted = df.sort_values("release_date").reset_index(drop=True) cut_idx = int(len(df_sorted) * 0.8) train_df = df_sorted.iloc[:cut_idx] test_df = df_sorted.iloc[cut_idx:] feature_cols = ["genres_target", "actor1_target", "director_target", "wish_count", "release_days", "screen_count"] X_train = train_df[feature_cols].values y_train = train_df["total_box_office"].values X_test = test_df[feature_cols].values y_test = test_df["total_box_office"].values scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) model = SVR(kernel="rbf", C=10.0, epsilon=0.05, gamma="scale") model.fit(X_train_scaled, y_train) y_pred = model.predict(X_test_scaled) print("MAE: {:.2f} 万元".format(mean_absolute_error(y_test, y_pred))) print("R2: {:.3f}".format(r2_score(y_test, y_pred)))

这是整套项目里最重要的一段代码。StandardScaler有个隐蔽的坑:只能在训练集上fit,在验证集上只做transform。如果对整个数据集先标准化再切分,验证集的均值和方差已经包含了测试集信息,SVR在这种场景下的表现同样虚高。数据处理里所有“先整体、后切分”的操作都要警惕,这条规则同样适用于后面的目标编码。

4.3 网格搜索与评估指标:怎么找参数,怎么确认性能

时序切分确定后,用GridSearchCV做参数搜索。注意这里的交叉验证也要做时序切分,直接使用默认KFold会造成同样的数据泄露。sklearn提供了TimeSeriesSplit,返回按时间顺序递增的训练集和验证集索引,正好适合票房数据。

from sklearn.model_selection import TimeSeriesSplit, GridSearchCV tscv = TimeSeriesSplit(n_splits=5) param_grid = { "C": [1, 10, 50, 100], "epsilon": [0.01, 0.05, 0.1, 0.5], "gamma": ["scale", 0.01, 0.1], } gs = GridSearchCV( SVR(kernel="rbf"), param_grid, cv=tscv, scoring="neg_mean_absolute_error", n_jobs=-1, ) gs.fit(X_train_scaled, y_train) print("best params:", gs.best_params_) best_model = gs.best_estimator_

评估指标别只看R²。R²在票房这种数值范围很大的回归问题上很容易到0.9以上,但绝对误差可能仍然高达几百万。我一般同时打印MAE(平均绝对误差,单位万元)和MAPE(平均百分比误差)。对票房项目来说,MAPE在20%以内算可用,超过30%说明特征或参数有问题。

网格搜索结果不要盲信。GridSearchCV选出的参数是在验证折上平均表现最好的,受折数影响明显。真正部署时回头看,往往会发现一个更平滑的参数组合反而更稳。比如搜索出来是C=100,换到C=30之后,滚动预测时误差波动明显更小。这是SVR项目里最玄学但也最常见的情况——验证集分数高不等于实战表现好。

4.4 预测结果落盘与误差分析:模型不只是输出一个数

模型训练完后,我习惯把验证集预测结果和真实值拼成一个DataFrame,按误差绝对值排序,重点检查误差最大的20条样本长什么样。这一步能快速发现问题:误差最大的往往集中于极端票房影片,比如大制作特效片票房超出模型预期,或者小众文艺片票房远低于热度信号暗示的水平。

result_df = pd.DataFrame({ "movie_id": test_df["movie_id"].values, "actual": y_test, "predict": y_pred, "error": np.abs(y_test - y_pred) }).sort_values("error", ascending=False) print(result_df.head(20))

这一步的价值在于区分系统性偏差和个别异常。如果误差最大的20条全是同档期影片,大概率是档期特征没做好;如果全是导演或演员知名度极高的片子,说明热度类特征需要加权。看完误差分布再回头调特征,比直接调SVR参数更有效。

5. 避坑指南:猫眼票房预测项目里的5个高频翻车点

5.1 反爬导致字段静默缺失,模型训练直接报错

现象:详情页请求返回后,电影类型、导演、演员字段全部为空,请求状态码却是200,程序没有抛任何异常,后续建模时特征矩阵出现大量NaN。

原因:猫眼返回了反爬验证页面,页面结构和正常详情页完全不同,CSS选择器匹配不到内容。最迷惑人的是状态码仍然正常,报错被Python静默吞掉了。

解决:解析详情页前先检查页面里有没有“验证”类文本,一旦命中就丢弃该请求并重试。重试时轮换User-Agent,加上随机延时,代码前面fetch_movie_detail里的if "验证" in resp.text这个分支,就是干这件事的。

5.2 票房单位“亿”和“万”混用,误差凭空放大一个数量级

现象:模型训练完,预测值比真实值高出一个数量级,或者MAE大得离谱,让整个项目看起来毫无价值。

原因:爬下来的票房数据一部分是“3.2亿”,另一部分是“5800万”,没统一单位就丢给SVR。模型把3.2当成3.2元、把5800当成5800元去学,两个数量级的数值混在一起,SVR的损失函数直接被大数值样本主导。

解决:清洗阶段强制统一,字符串进库前先过parse_box_office,输出统一以万元为单位。入库后随机抽查几条,确认没有“亿”“万”残留。这一步看着小,翻车率极高,我见过太多项目在这里耗掉半天调试时间。

5.3 随机切分造成时间泄露,测试集指标虚高

现象:用train_test_split默认随机切分,验证集MAE很低,R²漂亮得像示范案例,部署到真实预测后误差暴增。

原因:随机切分让同一部电影或同一档期的电影同时出现在训练集和测试集。SVR学到了档期和时间热度模式,但这些模式在真正的未来数据上并不稳定,模型其实偷偷记住了验证集里的时间上下文。

解决:统一使用按上映日期排序后的时序切分。特征工程里的统计型特征也必须只用训练集计算,再把映射应用到测试集,这一点在4.2里已经强调过。每次换特征后都检查一遍切分逻辑,不要想当然。

5.4 上映前和上映后的特征混训,模型两头不讨好

现象:同一套特征和参数,预测上映首周票房效果还行,预测上映前票房效果极差,完全没法在生产环境里用。

原因:上映前后的特征空间差异很大。上映前只有想看人数、预售、导演历史表现;上映后有了首日票房、口碑、评论数。用一套特征矩阵训练一个SVR,等于逼模型在两种空间里同时找映射关系,结果两边都学不精。

解决:把预测拆成两个阶段,分别训练模型。第一阶段只使用上映前可用特征,预测首日或首周票房;第二阶段加入累计票房、评分等上映后特征,预测最终累计票房。两个模型的特征列不同,参数也分开搜索,这样每个模型的分布假设才干净。

5.5 SVR在小样本上过拟合,训练R²高而预测失效

现象:训练集R²到0.98,测试集R²只有0.3,模型完全是记忆而不是泛化。

原因:样本量不到千条时,把C调得过大,gamma用auto,RBF核在高维特征空间里找了条贴着每个样本点的曲线。SVR在高维空间里的表达能力很强,样本不够时这种能力就变成了死记硬背。

解决:缩小C的搜索范围,从0.1、1、10试起,gamma固定为1除以特征数。同时监控训练集和测试集的分数差距,差距过大就降低C或者增大epsilon。宁可让模型看起来“笨一点”,也不能让它把训练集背下来。过拟合在票房数据上不是理论问题,是每天都在发生的现实问题。

6. 让预测结果真正可用:滚动预测与误差带

模型训练完,离“能用的预测系统”还差两步:一是随着新数据到来持续更新,二是给预测结果一个可信区间,否则业务方看见一个具体数字就当真,后面错得离谱。

滚动预测是我在票房项目里最常用的做法。把上映后数据按周切成窗口,每过一周就用最新数据重新训练一次SVR,预测下一个周期。如果你的样本量还不够,可以只更新特征统计值而不重新训练。这就像写python量化交易策略不能对着同一段历史数据反复调参一样,票房预测的回测也要留出真正没参与过选择的未来窗口,否则指标全是自欺欺人。

误差带的做法是:SVR本身输出一个点估计,但可以用验证集上的残差分布来构建区间。把验证集每一条样本的预测误差存下来,取95%分位数作为波动范围,预测结果表达成区间而非单点。业务上,区间比单点有用得多,制片方更容易接受“预计5到8亿”而不是“7亿”。

import numpy as np residuals = y_test - y_pred lo = -np.percentile(residuals, 97.5) hi = np.percentile(residuals, 97.5) pred_new = best_model.predict(new_sample_scaled)[0] interval = (pred_new + lo, pred_new + hi) print("预测票房区间: {:.0f} ~ {:.0f} 万元".format(interval[0], interval[1]))

误差带发布前要解释清楚含义,让使用者理解95%区间不是保证范围,而是模型不确定性的量化表达。这个方案落地过程中,我最深的教训是:不要迷信R²,不要迷信GridSearchCV自动挑出的参数,更不要在上映前后用同一套特征。每当急着给模型“加料”,多塞一列one-hot或者把C调到100,测试集的表现反而更差。最后我总会回到最朴素的检查清单:数据口径有没有对齐、特征有没有泄露、切分有没有按时间。希望帮到你。

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

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

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

立即咨询