基于猫眼数据与SVR的票房预测系统完整链路解析

基于猫眼数据与SVR的票房预测系统完整链路解析 简介基于猫眼电影数据和SVR回归器的电影票房预测系统是一份面向Python学习者的完整毕业设计/课程设计源码包覆盖数据爬取、特征分析、票房预测全流程。项目包含数据爬取模块、预处理模块、特征工程模块及SVR回归预测模块并附有文档说明代码注释详细便于新手快速上手。压缩包共18个文件以Python脚本.py、缓存文件.pyc、字体文件.woff和电影数据表.xls为主整体仅179KB轻量易部署。其中woff字体文件用于处理猫眼电影反爬字符映射xls文件为处理后的电影特征数据集可辅助理解数据清洗思路。已有379人学习下载适合作为大数据或人工智能方向毕业设计、期末大作业的参考项目也可用于学习SVR在票房预测中的实际应用。1. SVR与猫眼数据这套票房预测系统把数分完整链路跑通了做票房预测的公开方案不少但大多数止步于拿现成数据集调一个回归模型数据怎么来、字段怎么清洗、特征间有什么矛盾一概不讲。这套基于猫眼电影数据和SVR回归器的系统是少数把完整链路暴露出来的从猫眼接口的加密字体反爬到多字段特征加工再到SVR网格寻参最后输出预测误差对比。对正在做Python毕业设计、期末大作业或者想补全「爬虫到建模」实战经验的人来说它最大的价值不是模型多先进而是每一步都有代码可对出问题能定位到具体环节。系统源码覆盖catch_movie_data.py、movie_detail.py、font.py、findIP.py、data_feature.py、data_preprocess.py、svm_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, paramsparams, headersheaders, timeout10) data resp.json() return data[data][list]params中的date控制查询日期headers里必须带Referer否则接口会拒绝响应。resp.json()直接解析返回的JSON结构其中data.list是票房记录的数组每条包含movieName、boxInfo、avgShowView、avgSeatView等字段。初次运行时建议先打印data.keys()确认字段层级不同时间段接口返回结构可能略有调整。2.2 数字加密字体woff的破解思路票房数字在响应里不是明文。猫眼把数字做成了自定义字体页面和接口返回的字符串里包含的是字形编码直接拿去用会得到乱码。项目里的font.py和test.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文件因为猫眼会定期更换字体。提示接口里的加密数字串是一串形如#xe0e1;的实体编码解析前要先转成十六进制码点再去cmap表查对应的字形ID。2.3 IP代理池与请求频率控制高频请求下猫眼会封IP被封后接口直接返回验证页面。项目里的findIP.py就是代理池的采集和校验脚本。它的工作逻辑是从免费代理源抓取候选IP逐个用目标接口做连通性测试把延迟低、可用性高的IP缓存到本地供爬虫轮询使用。# findIP.py 代理校验示例 import requests def check_proxy(proxy_ip, proxy_port): proxies { http: fhttp://{proxy_ip}:{proxy_port}, https: fhttp://{proxy_ip}:{proxy_port} } try: resp requests.get( https://piaofang.maoyan.com/rankings/year, proxiesproxies, timeout5 ) 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电影唯一IDint1356037movie_name电影名string流浪地球2release_date上映日期datetime2023-01-22first_day_box首日票房float46300.5万pre_sale_box预售票房float15280.0万avg_price平均票价float52.3avg_show_view场均人次int38avg_seat_view场均上座率float0.162score猫眼评分float9.1want_see想看人数int1380000final_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_namemovies) 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 dfclean_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, annotTrue, cmapRdYlBu_r) plt.savefig(correlation.png, dpi150) 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_box和pre_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, scalerStandardScaler只用训练集来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(kernelrbf) grid GridSearchCV( svr, param_grid, cv5, scoringneg_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因为在票房预测场景评估「平均差多少万」比评估「拟合优度」更直观。cv5表示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(fMAE: {mae:.2f} 万) print(fRMSE: {rmse:.2f} 万) print(fR2: {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条之间这个量级下网格搜索很容易过拟合。常见问题是C和gamma同时调大后交叉验证分数很高但测试集表现很差。解决办法有两个一是缩小参数网格的搜索范围C不要超过100gamma不要小于0.001二是把cv从5折改成3折减少交叉验证中的方差。调试时先固定C1和epsilon0.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如果项目没有提供依赖清单手动装pandas、numpy、scikit-learn、requests、fontTools、matplotlib、seaborn、xlrd即可。xlrd要注意装1.2.0版本新版本不支持.xls格式读取。5.2 排查woff文件过期问题的验证方法系统里带有多个woff文件1588316706.069114.woff、1588248077.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(fmove {p[0]:.1f},{p[1]:.1f}) def _lineTo(self, p): self.lines.append(fline {p[0]:.1f},{p[1]:.1f}) def _curveToOne(self, p1, p2, p3): self.lines.append(fcurve {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表里反查其余码点能显著缩短字体反爬的开发周期。这个方法在应对现代网页字体反爬时依然有效迁移到其他站点也只是改一下字体文件路径而已。本文还有配套的精品资源点击获取