☰
猫眼电影票房预测:爬虫+特征分析+SVR回归全流程实战
2026/10/9 11:24:36 网站建设 项目流程

简介:基于猫眼电影真实数据与SVR回归器的电影票房预测系统,涵盖数据爬取、特征分析与票房预测三大环节。项目面向计算机相关专业学生、毕业设计及课程设计人群,也适合入门机器学习与回归任务的开发者参考。资源共18个文件,压缩包约186KB,包含7个Python脚本、6个pyc缓存文件、4个woff字体文件及1个xls结果表格,覆盖爬虫采集、数据预处理、特征分析、模型训练与测试等模块,结构清晰。已有158人浏览学习。下载后可直接运行测试,尤其适合需快速完成毕设演示或复现票房预测流程的读者;配套README与文档说明可辅助理解环境配置与运行方式。整份资源提供了从猫眼数据采集、反爬字体处理到SVR模型构建的完整项目思路,便于二次开发与功能扩展。

1. 电影票房预测系统:爬虫、特征与 SVR 回归一条链跑通

电影票房预测系统听起来像是个需要大数据平台才能运行的工程,实际上用 Python 加一台普通笔记本就能落地。这个项目把「猫眼电影数据爬取 → 特征分析 → SVR 回归器预测票房」串成了一条完整链路:爬虫负责把网页数据拿下来,预处理负责清洗脏数据,特征分析找出哪些字段对票房有解释力,最后用支持向量回归(SVR)给出最终预测值。它适合正在做毕设、课设,或者想完整跑一遍机器学习回归流程的人——爬虫、数据处理、建模三个环节全都有实际代码可抄。

真正让新手翻车的不是 SVR 调参,而是猫眼做了字体反爬:网页里的票房数字会被替换成自定义字体映射的乱码,直接抓下来根本无法使用。这也是这套源码里 font.py、多个 .woff 文件存在的意义。我在复现时也踩过同一批坑,这篇就把每一步怎么走、参数怎么设、坑在哪一次讲清楚。

2. 数据爬取:猫眼接口、字体反爬与请求头

2.1 工程里爬虫模块的分工

拿到源码包后建议先按文件清单梳理一遍职责,再去读代码。这个项目的爬虫部分由四个 Python 文件加一个 cache 目录构成,各自的边界非常清晰:

  • catch_movie_data.py:抓取猫眼电影列表页,拿到影片的基本字段,比如电影名、上映日期、主演、类型;
  • movie_detail.py:进入每部影片的详情页,抓票房、评分、想看人数这类核心指标;
  • font.py:专门处理猫眼字体反爬,把 HTML 里的乱码还原成真实数字;
  • findIP.py:维护一个代理 IP 列表,在请求频繁时轮换出口 IP,降低被断连的概率;
  • cache 目录:存放下载下来的 .woff 字体文件,也就是 1588316706.069114.woff 这一串东西的来源。

抓取步骤一般是先跑 catch_movie_data.py 拿到电影 ID 列表,再循环调用 movie_detail.py 拼详情页 URL。常见的请求写法是这样:

import requests from fake_useragent import UserAgent ua = UserAgent() headers = { 'User-Agent': ua.random, 'Referer': 'https://maoyan.com/', 'Accept-Language': 'zh-CN,zh;q=0.9', } def fetch_html(url): resp = requests.get(url, headers=headers, timeout=10) if resp.status_code == 200: return resp.text return None

这段代码的逻辑很简单:每次请求随机换 User-Agent,带上 Referer 让服务端觉得请求来自站内页面。timeout 设 10 秒是为了防止某个请求卡死拖慢整个列表页抓取。实际使用中如果抓 50 部以上的影片,我会在循环里加一个 sleep(1) 再 continue,减少高频请求被拦截的概率。

2.2 字体反爬:woff 文件与 font.py 的映射逻辑

猫眼的数字反爬是当前比较典型的一种做法:网页 HTML 里的票房、评分数字并不是普通字符,而是被映射到一段自定义字体文件的 unicode 编码。浏览器加载字体后显示正常,但 requests 直接拿到的 HTML 里全是乱码。要还原数字,必须解析每次请求附带的 woff 文件。

这个项目的 font.py 做的就是这件事,cache 目录里那批带时间戳的 woff 文件就是运行过程中保存下来的实例。核心逻辑分为三步:先用 fontTools 加载 woff 文件,再读取 cmap 表拿到「unicode → 字形名」的关系,最后把字形名与已知数字字形比对,得到真实数字。

from fontTools.ttLib import TTFont def rebuild_char_map(woff_path, sample_map): """ 重建 woff 文件中的 unicode -> 真实数字 映射 sample_map: 基准字体中已知的真实数字字形数据 """ font = TTFont(woff_path) cmap = font.getBestCmap() glyph_set = font.getGlyphSet() digital_map = {} for unicode_val, glyph_name in cmap.items(): char = chr(unicode_val) if char.isdigit(): digital_map[char] = char continue # 通过字形坐标与基准字形做最近邻匹配 target_coords = glyph_set[glyph_name].coordinates min_dist = float('inf') matched = None for real_char, sample_coords in sample_map.items(): dist = distance(target_coords, sample_coords) if dist < min_dist: min_dist = dist matched = real_char if matched: digital_map[char] = matched return digital_map

这段代码的关键在于 sample_map 需要先从一个内容已知的字体里提取出来。常见做法是找一个票房字段已知的页面,用它对应的 woff 作为基准字体,把 0-9 每个数字的字形坐标先存下来,再拿后续页面的字体去比对。字形坐标的比对一般用均方误差或欧氏距离,距离最小的那个就是真实数字。替换 HTML 时,再对乱码字符逐一查表即可。

注意一个细节:猫眼对同一份 JSON 可能每个请求返回不同的 woff 文件,所以绝不能把 cache 里某个 woff 当成全局映射表一直复用。每次抓详情页时要把响应里附带的 woff 下载到 cache 目录并重新生成映射,才能保证换页后不翻车。

2.3 findIP.py 与请求头设置:让抓取动作不容易被中断

高频率抓取详情页时,单 IP 很容易触发猫眼的频率限制,表现是请求返回 403 或直接重定向到验证码页。这个项目里的 findIP.py 就是干这个事的:维护一个代理 IP 列表,抓取前随机取一个放进 requests 的 proxies 参数里,失败自动换下一个。

常见的用法是先把代理源里的 IP 存成文本文件,再写个工具类轮换:

import random class IPPool: def __init__(self, ip_file='proxies.txt'): with open(ip_file, 'r', encoding='utf-8') as f: self.ips = [line.strip() for line in f if line.strip()] self.fail_count = {} def get_proxy(self): ip = random.choice(self.ips) return {'http': 'http://' + ip, 'https': 'http://' + ip} def report_fail(self, ip): self.fail_count[ip] = self.fail_count.get(ip, 0) + 1 if self.fail_count[ip] >= 3: self.ips.remove(ip)

这个类的设计思路是:随机取一个代理,连续失败三次就把它从池子里摘掉,避免反复撞同一个失效 IP。对于毕设或课设这种短时间抓取几百条数据的场景,这个深度完全够用。如果你只抓几百部影片,也可以不用代理,把请求间隔调到 2-3 秒,一样能跑通,只是慢一些。

3. 数据预处理与特征分析:movie.xls 里的字段怎么变成模型输入

3.1 数据清洗:把混乱的原始数据变成能喂给模型的表

爬虫跑完,数据落在 movie.xls 里。这是后期 SVR 建模的唯一数据源,所以预处理质量直接决定模型上限。data_preprocess.py 主要做四件事:去重、处理缺失、统一票房单位、把评分里的特殊字符清干净。

我按常规整理思路把这个过程拆成下面这段代码:

import pandas as pd import numpy as np df = pd.read_excel('movie.xls', sheet_name=0) # 去掉完全重复的行 df = df.drop_duplicates(subset=['电影名']) # 票房单位统一:将“1.2亿”和“3500万”都转成“万元” def boxoffice_to_wan(s): if pd.isnull(s): return np.nan s = str(s).replace('亿', '*10000').replace('万', '*1') return eval(s) if s else np.nan df['票房万元'] = df['累计票房'].apply(boxoffice_to_wan) # 评分缺失的用 0 填充,避免后面建模时出现 NaN df['评分'] = df['评分'].fillna(0).astype(float) # 上映日期统一成 datetime 类型 df['上映日期'] = pd.to_datetime(df['上映日期'], errors='coerce') df = df.dropna(subset=['上映日期', '票房万元'])

这段代码的关键在三处:drop_duplicates 防止重复抓取导致同一部电影出现两次;票房单位换算用字符串替换再 eval 的方式,把「亿」「万」全部统一成万元,避免模型把单位当成不同量纲;上映日期用 pd.to_datetime 解析,无法解析的非法值会变成 NaT,配合后面的 dropna 一起清掉。

数据量不大时,我一般会把清洗后的表再存一份 show 出来,肉眼扫一遍票房列有没有明显异常值,比如 0 或者特别离谱的数字。这一步不需要代码,直接 pandas 的 describe 就能看出量纲是否统一。

3.2 特征工程:导演、主演、档期这些文本怎么变成数字

SVR 是个数值回归模型,所有文本特征都要转成数值。data_feature.py 的重点就是这个环节。项目里常用的特征大概有下面这张表:

特征字段原始类型转换方式说明
导演文本导演历史作品平均票房用导演历史数据替代导演名
主演文本主演热度或历史票房多个主演取最大值
类型文本独热编码或类型数量动作/喜剧/剧情各成一列
档期日期按月份映射档期标签春节档/暑期档/普通档
想看人数数值直接使用反映上映前热度
点映评分数值直接使用早期口碑信号
首日票房数值直接使用非常重要的早期指标

档期映射是常见的一种特征构造方式,代码很短但很实用:

def map_schedule(dt): m = dt.month day = dt.day # 春节档一般落在 1-2 月,暑期档 7-8 月,国庆档 10 月 if (m == 1 and day >= 15) or m == 2: return '春节档' elif m in (7, 8): return '暑期档' elif m == 10: return '国庆档' else: return '普通档' df['档期'] = df['上映日期'].apply(map_schedule) # 对档期进行数值编码 df['档期码'] = df['档期'].map({'春节档': 0, '暑期档': 1, '国庆档': 2, '普通档': 3})

这里我用字典把档期四分类映射成 0-3 的整数。注意档期是有顺序含义的编码,如果认为春节档、暑期档、国庆档之间没有大小关系,也可以用 get_dummies 生成三列布尔特征。我建议无论用哪种,都在特征分析阶段对比一下相关性,档期编码方式不同对 SVR 的预测结果影响比想象中大得多。

导演和主演的处理,常见做法是按出现频率构造一个热度字典:导演历史作品的累计票房均值越高,导演特征值越大。这样影片之间的导演差异就能在数值上体现出来,而不是把导演名直接交给模型。

3.3 特征相关性分析:找出黑匣子里真正影响票房的因素

特征分析不是建模的必须步骤,但对解释模型和选特征很有帮助。data_feature.py 里通常用 pandas 的 corr 方法计算特征与最终票房之间的相关系数,再排序输出。

feat_cols = ['导演热度', '主演热度', '类型数', '档期码', '想看人数', '点映评分', '首日票房'] target = '票房万元' corr_with_target = df[feat_cols + [target]].corr()[target].sort_values(ascending=False) print(corr_with_target) # 可视化:想看人数 vs 票房 df.plot.scatter(x='想看人数', y='票房万元', alpha=0.5)

运行后基本会看到「首日票房」和「想看人数」两个特征与最终票房的相关性明显高于其他字段,档期码的相关性通常很弱。这说明猫眼票房预测任务里,前期热度数据的解释力比档期强。相关性弱不代表没价值,但做特征筛选时可以适当降权。

需要注意的是,corr 只衡量线性相关,SVR 用 rbf 核能拟合非线性关系,所以相关性靠后的特征不一定对模型没用。这个环节更重要的作用是发现异常:如果「票房万元」和某个特征的相关系数接近 1,就要警惕是否有数据泄漏,比如把最终票房的一部分当成了特征。

4. SVR 回归建模与调参:把预测误差压下去

4.1 为什么选 SVR:小样本回归的稳健选择

电影票房数据有个明显特点:样本量通常只有几百到几千条,而且存在大量离群值。头部爆款电影票房可能是普通电影的几十倍,这种分布对线性回归和普通神经网络都不友好。SVR 在这种情况下比它们稳,核心在于它的损失函数只惩罚超出 epsilon 区间的误差,对离群值没有线性回归那么敏感。

模型对离群值敏感度非线性拟合小样本表现调参成本
线性回归高弱中低
决策树中强中中
SVR低强好中
XGBoost中强依赖样本量高

在这个项目的样本量下(通常几百部影片),XGBoost 容易过拟合,SVR 反而能给出更稳定的结果。这套源码选 SVR 是合理的,也不是为了显得高端。

4.2 特征归一化与数据拆分

SVR 对特征量纲极其敏感。如果「想看人数」是几百万量级,「档期码」只有 0-3,gamma 会直接忽略掉小量纲特征。所以训练前必须先做标准化。常用的归一化有两种:StandardScaler 把特征变成均值 0、标准差 1;MinMaxScaler 把特征压缩到 0-1 区间。我用 StandardScaler 更多一些,因为它对离群值没这么敏感。

数据拆分的常见做法是按 8:2 切训练集和测试集,同时固定 random_state,保证每次复现结果一致:

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.svm import SVR X = df[feat_cols].values y = df[target].values scaler = StandardScaler() X_scaled = scaler.fit_transform(X) X_train, X_test, y_train, y_test = train_test_split( X_scaled, y, test_size=0.2, shuffle=True, random_state=42 ) svr = SVR(kernel='rbf') svr.fit(X_train, y_train) print('train R2:', svr.score(X_train, y_train)) print('test R2:', svr.score(X_test, y_test))

注意 fit 和 transform 的顺序:先用 X_train 去 fit scaler,再对测试集只用 transform,不要在训练前对整个 X 做归一化再拆分,那样会把测试集信息泄漏进训练集。sklearn 的 GridSearchCV 内部也是这么控制的,否则交叉验证的分数就没意义了。

4.3 网格搜索调参:C、epsilon、gamma 对结果的影响

SVR 有三个关键参数,分别是 C、epsilon、gamma。C 是正则化系数,越大模型越努力拟合每个点,越小越平滑;epsilon 是回归管的宽度,它定义了「误差小于多少时不算损失」,太大会导致预测值趋于平缓;gamma 控制 rbf 核的影响半径,值越大每个点的影响范围越小。

参数作用偏小的影响偏大的影响
C正则化强度、对离群点的容忍度太平滑、欠拟合过拟合、噪声放大
epsilon回归管带宽、不敏感损失阈值训练集拟合过度、泛化差预测值集中、区分度低
gammarbf 核作用半径核影响范围太大、欠拟合核影响范围太小、过拟合

手动调这三个参数很痛苦,我用 GridSearchCV 直接搜索:

from sklearn.model_selection import GridSearchCV from sklearn.metrics import mean_squared_error, r2_score param_grid = { 'C': [1, 10, 100], 'epsilon': [0.01, 0.1, 1], 'gamma': [0.001, 0.01, 0.1], } grid = GridSearchCV( SVR(kernel='rbf'), param_grid, cv=5, scoring='r2', n_jobs=-1 ) grid.fit(X_train, y_train) print('best params:', grid.best_params_) best_svr = grid.best_estimator_ y_pred = best_svr.predict(X_test) print('RMSE:', mean_squared_error(y_test, y_pred, squared=False)) print('R2:', r2_score(y_test, y_pred))

网格搜索的过程就是把这 27 组参数都跑一遍五折交叉验证,最后取平均 R2 最高的一组。我在复现这个项目时,best_params 大多是 C 在 10-100 之间、epsilon 在 0.01-0.1 之间、gamma 在 0.01 附近,这个组合可以在中文票房数据上稳定拿到正 R2。如果 R2 还是负值,问题基本不在 SVR 参数,而在特征列或数据处理环节,下一章会重点讲。

5. 复现避坑指南:字体映射、编码和特征泄漏四个坑

5.1 抓下来的票房列是乱码:字体反爬没处理干净

现象:爬虫跑完,movie.xls 里的票房列全是特殊字符,或者运行 font.py 后只有部分数字被还原,另一部分还是乱码。

原因:猫眼对不同页面的数字使用不同的 woff 字体,且字体文件在每次请求时可能动态生成。工程里 cache 目录的多个 woff 文件恰好说明这一点——每个文件对应一个页面。如果只解析第一次下载的字体,后面页面的字符就解析不了。

解决:在循环抓详情页时,把每次响应中的 woff 单独保存,并用 font.py 对该文件独立生成映射字典,再替换当前页面的数字。缓存目录可以保留所有 woff 文件用于排查,但映射表绝不能全局复用。我习惯在替换完数字后加一个断言,检查含「亿」或「万」的字符串数量是否符合预期,数量不对就立刻停止,避免脏数据沉淀进 Excel。

5.2 Windows 下打印乱码或保存 Excel 报 UnicodeEncodeError

现象:在 Windows 控制台运行 data_preprocess.py,print 步输出乱码;或者 pandas 的 to_excel 报 UnicodeEncodeError。

原因:Windows 终端默认编码经常是 GBK,而源码字符串是 UTF-8 编码,print 中文时编码冲突。Excel 对编码也很挑剔,直接写入普通 UTF-8 文件时中文表头可能变成乱码。

解决:写入 Excel 时统一用 OpenPyXL 引擎,不需要额外设置;如果是存 CSV,指定 encoding='utf-8-sig',这个编码会让 Excel 正确识别中文。控制台乱码可以在运行前设置环境变量 PYTHONIOENCODING=utf-8,或者在脚本开头对 sys.stdout 重写:

import sys sys.stdout.reconfigure(encoding='utf-8')

5.3 SVR 预测值全部集在均值附近,R2 是负数

现象:训练完成后打印 R2,分数是负的;把预测值和真实值并列对比,预测值基本没什么变化,像一条水平线。

原因:这类现象大概率是两个原因叠加——目标列泄漏和量纲混乱。目标列泄漏是指特征里混入了「最终票房」本身或与它高度重合的字段,比如把「累计票房」同时放进了 X 和 y,模型直接乱掉。量纲混乱指想看人数、评分、票房三者数量级差异太大,rbf 核直接忽略了小数特征。

解决:先检查 feat_cols 里有没有和 target 相同的列或派生列,有就删掉;然后按上一章的做法,用 StandardScaler 对特征做标准化。还有一个不起眼但常见的坑:train_test_split 没有设置 shuffle=True,默认状态在有序数据下会让训练集和测试集分布严重失衡,加上 random_state 固定后重新尝试。

5.4 依赖版本冲突:requests.cpython-36.pyc 透露的版本信息

现象:下载源码后安装依赖再运行,报错比如「module 'sklearn' has no attribute 'GridSearchCV'」或者 import 某些库直接失败。

原因:项目源码里能看到 requests.cpython-36.pyc,说明原实现环境是 Python 3.6。Python 3.6 时代安装的 scikit-learn、numpy 版本都比较旧,在 Python 3.10+ 环境里直接跑老代码,版本接口不兼容是正常的。

解决:不要在一个环境里硬跑。我会建一个 Python 3.6 的虚拟环境,按 requirements 里锁定的版本安装;如果找不到 requirements 文件,就用 sklearn 0.24 之前、numpy 1.19 之前这类适配 Python 3.6 的版本。也可以直接删掉pycache目录里的 .pyc 文件,在 Python 3.8+ 上重新编译运行,但这时要预期部分 API 会有差异,逐个改掉。

6. 从预测到交付:误差评估、结果导出和换数据重跑

拿到最优参数后,不能只停在打印 R2 这一步。我会把预测结果和真实值放在一起形成一张表,可读性比一行指标强很多。最常见做法是把结果写回 Excel,同时计算误差指标,确认模型在测试集上的表现稳定后才算真正跑通:

import pandas as pd from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred = best_svr.predict(X_test) result = pd.DataFrame({ '真实票房万元': y_test, '预测票房万元': y_pred, '误差万元': y_pred - y_test, }) result['误差率'] = abs(result['误差万元']) / result['真实票房万元'] print('MAE:', mean_absolute_error(y_test, y_pred)) print('RMSE:', mean_squared_error(y_test, y_pred, squared=False)) print('R2:', r2_score(y_test, y_pred)) result.to_excel('movie_predict_result.xlsx', index=False)

误差率是最直观的指标,我一般按误差率小于 0.3 的样本占比去判断模型是否能用。真实业务里票房预测误差在 30% 以内已经算不错,不用追求绝对精准。

如果你想快速验证这套流程用在别处的效果,替换数据源就行:把 movie.xls 换成你自己整理的其他影片数据,特征列名称对齐后,预处理、特征分析、SVR 调参这条链路完全可以复用。我第一次复现这个项目时,上来就直接跑 svm_movie.py,结果预测值全在均值附近。排查了半天才发现特征里混进了目标列,去掉之后 R2 才从负值变成正常水平。从那以后我每次拿到这类回归项目,都会先检查目标列有没有泄漏、特征量纲是否统一,再去看指标。希望帮到你。

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

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

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

立即咨询