基于猫眼数据与SVR的票房预测系统完整链路解析
2026/9/12 9:39:29 网站建设 项目流程

简介:基于猫眼电影数据和SVR回归器的电影票房预测系统,是一份面向Python学习者的完整毕业设计/课程设计源码包,覆盖数据爬取、特征分析、票房预测全流程。项目包含数据爬取模块、预处理模块、特征工程模块及SVR回归预测模块,并附有文档说明,代码注释详细,便于新手快速上手。压缩包共18个文件,以Python脚本(.py)、缓存文件(.pyc)、字体文件(.woff)和电影数据表(.xls)为主,整体仅179KB,轻量易部署。其中woff字体文件用于处理猫眼电影反爬字符映射,xls文件为处理后的电影特征数据集,可辅助理解数据清洗思路。已有379人学习下载,适合作为大数据或人工智能方向毕业设计、期末大作业的参考项目,也可用于学习SVR在票房预测中的实际应用。

1. SVR与猫眼数据:这套票房预测系统把数分完整链路跑通了

做票房预测的公开方案不少,但大多数止步于拿现成数据集调一个回归模型,数据怎么来、字段怎么清洗、特征间有什么矛盾一概不讲。这套基于猫眼电影数据和SVR回归器的系统,是少数把完整链路暴露出来的:从猫眼接口的加密字体反爬,到多字段特征加工,再到SVR网格寻参,最后输出预测误差对比。对正在做Python毕业设计、期末大作业或者想补全「爬虫到建模」实战经验的人来说,它最大的价值不是模型多先进,而是每一步都有代码可对,出问题能定位到具体环节。系统源码覆盖catch_movie_data.pymovie_detail.pyfont.pyfindIP.pydata_feature.pydata_preprocess.pysvm_movie.py七个核心脚本,外加woff字体文件和文档说明,部署后即可跑通从抓取到预测的完整流程。

2. 猫眼数据爬取与woff字体反爬处理

2.1 猫眼榜单接口的数据结构

猫眼电影专业版的票房数据页面,加载方式不是一次性返回全量HTML,而是通过接口分段拉取。常见接口返回的是JSON格式的字符串,字段包含电影名、票房、场均人次、平均票价、上映天数等。初次抓取时最容易犯的错误是直接请求榜单页地址,拿到的HTML里只有框架和数据占位符,真实数据藏在XHR请求里。

# catch_movie_data.py 核心抓取逻辑(节选) import requests import json def fetch_maoyan_data(date_str): url = "https://piaofang.maoyan.com/rankings/year" params = {"date": date_str} headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Referer": "https://piaofang.maoyan.com/" } resp = requests.get(url, params=params, headers=headers, timeout=10) data = resp.json() return data["data"]["list"]

params中的date控制查询日期,headers里必须带Referer,否则接口会拒绝响应。resp.json()直接解析返回的JSON结构,其中data.list是票房记录的数组,每条包含movieNameboxInfoavgShowViewavgSeatView等字段。初次运行时建议先打印data.keys()确认字段层级,不同时间段接口返回结构可能略有调整。

2.2 数字加密字体woff的破解思路

票房数字在响应里不是明文。猫眼把数字做成了自定义字体,页面和接口返回的字符串里包含的是字形编码,直接拿去用会得到乱码。项目里的font.pytest.woff文件就是用来解决这个问题的。

# font.py 字体解析与数字映射(节选) from fontTools.ttLib import TTFont import re def parse_woff(woff_path): font = TTFont(woff_path) cmap = font.getBestCmap() # 从woff中提取字形编码到unicode的映射 glyph_map = {} for code, name in cmap.items(): glyph_map[code] = name return glyph_map

通用的做法是:先从接口返回的CSS或字体文件地址下载woff,然后用fontTools读取cmap表,拿到每个字形编码对应的轮廓信息。再把已知数字0到9用同样的方式渲染成轮廓,和woff里的轮廓做相似度比对,就能建立「编码到真实数字」的映射表。比对时可以先把轮廓坐标归一化,再计算欧氏距离。每次抓取前要下载当次页面对应的woff文件,因为猫眼会定期更换字体。

提示:接口里的加密数字串是一串形如的实体编码,解析前要先转成十六进制码点,再去cmap表查对应的字形ID。

2.3 IP代理池与请求频率控制

高频请求下猫眼会封IP,被封后接口直接返回验证页面。项目里的findIP.py就是代理池的采集和校验脚本。它的工作逻辑是:从免费代理源抓取候选IP,逐个用目标接口做连通性测试,把延迟低、可用性高的IP缓存到本地,供爬虫轮询使用。

# findIP.py 代理校验示例 import requests def check_proxy(proxy_ip, proxy_port): proxies = { "http": f"http://{proxy_ip}:{proxy_port}", "https": f"http://{proxy_ip}:{proxy_port}" } try: resp = requests.get( "https://piaofang.maoyan.com/rankings/year", proxies=proxies, timeout=5 ) if resp.status_code == 200: return True except Exception: return False return False

这里校验的目标地址直接用了猫眼票房接口,比用百度之类的通用地址更可靠——有些代理能访问普通网站但访问不了猫眼。我一般会在校验函数里加一个响应内容判断,如果返回体里包含verify字样,说明命中了验证码机制,这种代理要标记为不可用。抓取频率控制在单线程下每2秒一次比较安全,多线程并发时也要限制在5个线程以内。

3. 特征工程与数据预处理:从movie.xls到SVR输入

3.1 爬取数据的组织结构

抓下来的数据先落到movie.xls,再去重、补全缺失字段。这个Excel不是简单的一行一部电影,而是包含了多个sheet或者多张表结构,用电影ID做关联。一个典型的数据结构如下表,字段来自猫眼接口的不同接口段,其中「预测票房」来自文件里的实际标签,用于模型监督学习。

字段名含义数据类型示例值
movie_id电影唯一IDint1356037
movie_name电影名string流浪地球2
release_date上映日期datetime2023-01-22
first_day_box首日票房float46300.5(万)
pre_sale_box预售票房float15280.0(万)
avg_price平均票价float52.3
avg_show_view场均人次int38
avg_seat_view场均上座率float0.162
score猫眼评分float9.1
want_see想看人数int1380000
final_box最终票房(标签)float388700.0(万)

3.2 数据清洗与异常值处理

data_preprocess.py负责把Excel读进来,做类型转换和异常值过滤。票房字段从字符串转浮点数时有几种脏格式:带「万」字、千分位逗号、空格。「万」字要统一乘以10000换算成元,再除以10000保持以万为单位。这样处理是为了在数值层面统一量纲,避免SVR训练时特征尺度过大导致核函数计算异常。

# data_preprocess.py 票房字段清洗片段 import pandas as pd import numpy as np def clean_box_value(val): if isinstance(val, str): val = val.replace(",", "").replace(" ", "") if "万" in val: val = val.replace("万", "") return float(val) * 10000 / 10000 return float(val) return val def load_and_clean(excel_path): df = pd.read_excel(excel_path, sheet_name="movies") df["first_day_box"] = df["first_day_box"].apply(clean_box_value) df["pre_sale_box"] = df["pre_sale_box"].apply(clean_box_value) # 剔除首日票房和最终票房缺失的行 df = df.dropna(subset=["first_day_box", "final_box"]) df = df[df["final_box"] > 0] return df

clean_box_value里的* 10000 / 10000是原地换算,保留「万」作为统一单位,不需要额外生成新列。dropna删除的不仅是空值行,还包括被接口加密字体重合导致解析后仍然异常的数据,这些行留着会污染训练集。数值型字段用pd.to_numeric强制转类型,转换失败的就替换成该列中位数,不做删除——删行会丢失整条电影记录的其他有效信息。

3.3 特征选择与相关性分析

data_feature.py的作用是先看特征和标签的相关性,再决定哪些列进入SVR。票房预测里比较有效的特征是预售票房、首日票房、想看人数、评分、上映天数、排片场次。平均票价和场均人次这类字段容易受地区和时段干扰,单独做特征时噪声偏大,通常做衍生特征用。

# data_feature.py 相关性热力图与特征筛选 import seaborn as sns import matplotlib.pyplot as plt def plot_correlation(df): cols = ["first_day_box", "pre_sale_box", "avg_price", "avg_show_view", "score", "want_see", "final_box"] corr = df[cols].corr() plt.figure(figsize=(10, 8)) sns.heatmap(corr, annot=True, cmap="RdYlBu_r") plt.savefig("correlation.png", dpi=150) def select_features(df): feature_cols = ["first_day_box", "pre_sale_box", "score", "want_see", "avg_show_view"] X = df[feature_cols].values y = df["final_box"].values return X, y

相关性热力图的观察重点是final_box那一行。实际跑下来会发现first_day_boxpre_sale_box与最终票房的相关系数普遍在0.85以上,是强相关特征;score的相关系数在0.6左右,属于中等相关;avg_show_view相关性波动大,保留它是因为SVR对多维非线性关系有表达力,单变量相关性低不代表对模型没有贡献。

3.4 数据集切分与归一化

SVR对特征尺度敏感,尤其是使用RBF核函数时,特征范围差异过大会让距离计算被大数值特征主导。切分数据时要注意按时间排序后切分,不能用随机切分——票房数据有强烈的时间趋势,随机切分会把未来信息混进训练集,导致评估指标虚高。

# data_preprocess.py 归一化与时间序列切分 from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split def prepare_dataset(X, y): # 按时间排序后按8:2切分 split_idx = int(len(X) * 0.8) X_train, X_test = X[:split_idx], X[split_idx:] y_train, y_test = y[:split_idx], y[split_idx:] scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) return X_train_scaled, X_test_scaled, y_train, y_test, scaler

StandardScaler只用训练集来fit,然后对测试集只做transform。如果对全量数据做标准化再切分,测试集的信息会泄漏到训练过程中,模型评估结果没有参考价值。这点在课程设计答辩时经常被问,提前理解清楚,演示时能讲明白。

4. SVR回归器建模与网格寻参

4.1 SVR的基本原理与选择理由

支持向量回归(SVR)和普通线性回归的核心区别在于损失函数。线性回归追求最小化所有样本的预测误差,SVR则只惩罚落在「间隔带」之外的点,落在间隔带内的样本不产生损失。这个特性适合票房预测的场景——票房数据存在大量噪声(档期、口碑发酵、突发事件),SVR对离群点不敏感,预测结果不会因为个别爆冷或黑马电影出现大幅漂移。核函数选择上,RBF核能表达特征与票房之间的非线性关系,比线性核更贴合实际数据分布。

4.2 核心模型训练代码与参数说明

svm_movie.py是系统的预测主脚本。它读取预处理后的特征,用GridSearchCV遍历SVR参数组合,选出最佳模型后在测试集上评估。

# svm_movie.py SVR训练与参数搜索(节选) from sklearn.svm import SVR from sklearn.model_selection import GridSearchCV from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score def train_svr(X_train, y_train): param_grid = { "C": [0.1, 1, 10, 100], "gamma": [0.001, 0.01, 0.1, 1], "epsilon": [0.01, 0.1, 0.2] } svr = SVR(kernel="rbf") grid = GridSearchCV( svr, param_grid, cv=5, scoring="neg_mean_absolute_error", n_jobs=-1 ) grid.fit(X_train, y_train) return grid.best_estimator_, grid.best_params_

C是正则化参数,值越大对间隔带内样本的惩罚越重,模型越容易过拟合;gamma控制RBF核的影响半径,值越大决策边界越复杂;epsilon是间隔带宽度,值越大允许的误差越大,模型越平滑。这里scoring选择neg_mean_absolute_error而不是默认的r2,因为在票房预测场景,评估「平均差多少万」比评估「拟合优度」更直观。cv=5表示5折交叉验证,小样本下稳定性和计算量的平衡比较好。

4.3 预测结果评估与可视化

模型训练完成后,项目会把预测票房和真实票房做对比图。评估指标的计算逻辑如下:

# svm_movie.py 模型评估代码 def evaluate_model(model, X_test, y_test): y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) r2 = r2_score(y_test, y_pred) print(f"MAE: {mae:.2f} 万") print(f"RMSE: {rmse:.2f} 万") print(f"R2: {r2:.4f}") # 输出每个测试样本的预测对比 for i in range(len(y_test)): diff = abs(y_pred[i] - y_test[i]) / y_test[i] * 100 print(f"样本{i}: 实际={y_test[i]:.1f}万, " f"预测={y_pred[i]:.1f}万, 误差={diff:.1f}%") return y_pred

评估结果出来后要重点看MAE。如果MAE在1000万以内,说明模型对大体量电影的预测比较可靠;如果R2接近0或者为负,先去检查特征选择和归一化步骤,不要急着调参。误差百分比超过30%的样本通常是极端值电影——票房在1000万以下的小众片,MAE的绝对值小但相对误差大,这是SVR对稀疏区域的通病,属于正常现象。

4.4 参数寻优的坑:小数据下的过拟合

猫眼电影单年度可用的训练样本量一般在100到200条之间,这个量级下网格搜索很容易过拟合。常见问题是Cgamma同时调大后,交叉验证分数很高但测试集表现很差。解决办法有两个:一是缩小参数网格的搜索范围,C不要超过100,gamma不要小于0.001;二是把cv从5折改成3折,减少交叉验证中的方差。调试时先固定C=1epsilon=0.1,只搜索gamma,找到合理的数量级后再联合搜索,比一次性全参数搜索更可控。

5. 完整跑通预测流程与部署验证

5.1 从抓取到预测的流水线调用

项目里有几个可独立运行的脚本,正确执行顺序是:先catch_movie_data.py抓取列表页数据,再movie_detail.py获取每部电影的详情字段,font.py负责解析当次抓取需要的woff字体文件,data_preprocess.py清洗数据,data_feature.py生成特征,最后svm_movie.py完成训练和预测。实际运行中,如果只是想快速验证建模流程,可以直接用项目自带的movie.xls文件,跳过爬虫环节。这个文件已经把猫眼部分电影的数据整理好了,data_preprocess.py会直接读取和清洗。

整个链路跑通后,控制台会依次输出每条测试样本的预测票房和误差百分比,同时生成预测对比图。建议新建一个虚拟环境,安装依赖时用pip install -r requirements.txt,如果项目没有提供依赖清单,手动装pandasnumpyscikit-learnrequestsfontToolsmatplotlibseabornxlrd即可。xlrd要注意装1.2.0版本,新版本不支持.xls格式读取。

5.2 排查woff文件过期问题的验证方法

系统里带有多个woff文件(1588316706.069114.woff1588248077.992224.woff等),这是因为每次爬取时猫眼会下发新的字体文件。如果运行font.py时发现数字解析混乱,优先怀疑woff文件与当前接口不匹配。验证方法是手动请求一次接口,查看返回的字体CSS链接,和项目里已有的woff文件名做时间戳对比。时间戳差了几天或几小时通常没问题,但如果差了一周以上,基本可以断定字体被更换过。此时重新下载当次返回的woff文件,替换到项目目录即可。

5.3 一个值得迁移的技巧:woff解析失败时输出字形轮廓

这是我从这套代码里看到的一个实用思路:test.py不只是做解析测试,还把每个字符的坐标轮廓打印出来。当你怀疑编码映射对不上的时候,可以把这个测试脚本的轮廓输出和字体编辑工具(如FontCreator)里看到的轮廓做对比,确认是解析错位还是字体版本问题。调试woff时不要只盯着报错信息,多利用轮廓坐标做差分对比,定位速度快得多。

# test.py 字形轮廓输出(思路还原) from fontTools.pens.basePen import BasePen class PrintPen(BasePen): def __init__(self, glyphSet): super().__init__(glyphSet) self.lines = [] def _moveTo(self, p): self.lines.append(f"move {p[0]:.1f},{p[1]:.1f}") def _lineTo(self, p): self.lines.append(f"line {p[0]:.1f},{p[1]:.1f}") def _curveToOne(self, p1, p2, p3): self.lines.append(f"curve {p1[0]:.1f},{p1[1]:.1f} " f"{p2[0]:.1f},{p2[1]:.1f} {p3[0]:.1f},{p3[1]:.1f}") def compare_glyph(font, code): glyph = font.getGlyphName(code) pen = PrintPen(font.getGlyphSet()) font.getGlyphSet()[glyph].draw(pen) return pen.lines

这段代码用BasePen把字形的向量轮廓逐行打印出来,每个数字对应一组不同的坐标序列。对比映射表和真实数字的轮廓差异时,不需要一次性解决全部10个数字,先确认某个数字的轮廓特征,再去cmap表里反查其余码点,能显著缩短字体反爬的开发周期。这个方法在应对现代网页字体反爬时依然有效,迁移到其他站点也只是改一下字体文件路径而已。

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

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

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

立即咨询