ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于SVR的电影票房预测:猫眼数据爬取与模型调参全流程

基于SVR的电影票房预测:猫眼数据爬取与模型调参全流程 简介一套基于猫眼电影票房预测的完整Python项目面向计科、人工智能、自动化等专业的毕设选题或课程设计解决从数据爬取、特征分析到SVR回归预测的完整链路问题。资源总计18个文件核心为7个Python脚本含数据采集、预处理、特征构建、SVR建模与预测等模块另附6个编译缓存pyc、4个用于解析猫眼字体反爬的woff文件以及1个已采集的movie.xls数据表包体约186KB结构清晰便于对照修改。目前已有155人学习适合需要完成毕业设计或入门电影票房预测实战的读者。下载后可获得可直接运行的源代码与文档说明代码经测试通过还包含字体反爬解析、特征筛选等关键细节若基础薄弱也可通过私聊获得远程教学支持从环境配置到运行调优逐步上手作为课程设计或项目演示均有较高参考价值。1. 为什么票房预测要用 SVR 回归器很多毕设做票房预测第一反应是跑一遍线性回归然后看着 0.3 左右的 R² 不知道问题出在哪。电影票房数据有三个特点样本量通常只有几百条各特征之间是强非线性关系而且票房标签严重右偏。SVR支持向量回归靠核函数把原始特征映射到高维空间在不敏感带内忽略小幅误差正好压得住这类小样本非线性问题。这套基于猫眼电影数据的系统并不只有模型它把数据爬取、特征分析、数据预测串成了一条完整链路代码分模块写在 catch_movie_data.py、data_preprocess.py、data_feature.py 和 svm_movie.py 里跑通之后既能交毕设也能学到怎么处理真实反爬场景。2. 猫眼数据爬取woff 字体反爬与动态请求处理抓猫眼数据最麻烦的不是登录或者验证码而是页面里的数字用自定义字体渲染直接抓到的 HTML 里看不到真实票房值。这个项目的爬虫部分拆成了 catch_movie_data.py、movie_detail.py 和 font.py 三个模块分别负责列表页、详情页和字体解析下面按流程拆开讲。2.1 抓包先看数据结构Ajax 接口与 params猫眼电影榜单走的不是普通 HTML 爬取而是页面加载完成后通过 XHR 请求接口拿 JSON 数据。接口返回的字段包括电影名、评分、演员、上映日期等基础信息但具体 URL 和返回结构会随版本调整。常见做法是抓包找到ajax开头的接口然后用params传 offset 和 limit 实现翻页。import requests def fetch_movie_list(offset0, limit10): url https://m.maoyan.com/ajax/movie_on_info_list params { offset: offset, limit: limit, } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://m.maoyan.com/, } resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() return data.get(movieList, [])这里的params是请求查询参数offset控制从第几条开始limit控制一次性返回多少条通常取 10headers里的User-Agent和Referer必须模拟浏览器否则接口直接返回 403。timeout10是超时时间单位秒防止某个坏代理把线程卡死。拿到列表后再根据每条电影的id去请求详情页这就是 movie_detail.py 干的事。需要注意不要只看接口返回就完事。实际这个项目里列表页拿到的是电影 id 和基本字段真正要用于特征分析的评分、评论数、上映天数有不少在详情页接口里。开发时建议先用 curl 或 Postman 验证接口字段再写代码否则容易到处踩 403。2.2 字体反爬woff 映射还原与字体缓存猫眼把票房、评分等数字用 CSS 自定义字体渲染网页源码里是一堆乱码或者空格。反爬原理是服务端会动态下发一个.woff字体文件页面使用这个字体渲染数字但实际 Unicode 码位被重新映射过肉眼看到的数字和字体文件里的 glyph 对应关系需要解析出来。项目里 cache 目录下那几个.woff文件比如 1588316706.069114.woff就是抓取过程中下载并缓存下来的字体文件。from fontTools.ttLib import TTFont def parse_woff_mapping(woff_path): font TTFont(woff_path) cmap font.getBestCmap() # cmap 的结构是 {unicode_code: glyph_name} mapping {} for code, glyph in cmap.items(): # 把 glyph 名和页面上 class 中的实体名对齐 mapping[glyph] chr(code) return mapping代码解释TTFont(woff_path)用 fontTools 加载字体文件getBestCmap()返回字符映射表key 是 Unicode 码点value 是 glyph 名字。实际页面里的数字标签会包含类似#x10001;这样的实体解析后拿到 glyph 名再用这个映射反查真实数字。注意字体文件是动态变化的同一个数字在不同批次可能对应不同 glyph所以一定要把抓到的 woff 缓存到本地比如 cache 目录重复解析时直接读取不需要重新下载。test.woff是验证用的字体文件开发时可以用它与线上 woff 做 diff确认映射逻辑没有过期。我一般会写一个单元测试把已知数字的文本渲染结果和解析结果比对避免某个版本的字体多了几个 glyph 导致解析错位。2.3 findIP.py 与代理检测控制抓取频率批量抓猫眼接口一个 IP 连续请求几十次就会触发频率限制。项目的 findIP.py 就是干这个的它维护一批候选代理地址逐个发送测试请求能返回 200 的代理才进入可用池。def check_proxy(proxy, timeout5): test_url https://m.maoyan.com/movie try: resp requests.get(test_url, proxies{ http: proxy, https: proxy, }, timeouttimeout) return resp.status_code 200 except Exception: return False这个函数的proxy参数格式是ip:porttimeout不能太小太小会把慢代理误判为不可用但也不能太大否则检查一轮要等很久。测试 URL 最好用真实目标站因为有些代理对特定域名不生效。实际爬取时再结合time.sleep(1)之类的限速每请求一次休息 1 到 2 秒能显著降低被封概率。下表是这套爬虫模块的职责划分方便对照代码文件理解整个流程文件职责输出catch_movie_data.py抓取猫眼电影列表页、翻页控制原始 JSON 记录movie_detail.py抓取每部电影的详情字段如评分、评论数结构化列表font.py解析 woff 字体映射还原被混淆的数字字体映射字典findIP.py检测候选代理是否可用可用代理列表cache保存下载过的 woff 字体文件.woff 文件这里有一个容易踩的坑font.py 与 movie_detail.py 的执行顺序。必须先把字体映射解析出来再解析详情页里的混淆数字否则详情页的票房字段全是无效值。项目里 cache 目录也承担了断点续传的功能测试中断后再次运行不用重新下载全部字体。3. 特征工程与数据预处理把非结构化字段变成 SVR 输入爬到的数据是 JSON 或 Excel 里的原始记录不能直接扔给 SVR。特征分析阶段的核心工作是做数据清洗、特征构造、标签变换这一部分对应 data_preprocess.py 和 data_feature.py。3.1 数据清洗从 movie.xls 到干净的 DataFrame项目里有一份 movie.xls是爬取结果的汇总。先读取 Excel检查字段类型和缺失比例。常见的脏数据包括重复电影、空评分、票房单位不统一、日期格式混乱。import pandas as pd df pd.read_excel(movie.xls) df df.drop_duplicates(subset[movie_name]) df[release_date] pd.to_datetime(df[release_date]) df[score] df[score].fillna(df[score].median()) def to_wan(s): s str(s).strip() if 亿 in s: return float(s.replace(亿, )) * 10000 if 万 in s: return float(s.replace(万, )) return float(s) / 10000 df[box_office] df[box_office].map(to_wan)清洗逻辑拆开看drop_duplicates(subset[movie_name])是去重一部电影只保留一条记录否则训练集和测试集会同时出现同一部电影造成数据泄漏pd.to_datetime把日期字符串洗成时间类型score用中位数填充比均值更抗异常值因为评分分布偏斜时均值会被极端值拉高to_wan函数把2.3亿转成 23000、4500万转成 4500最终单位统一为万元。需要注意的是如果某部电影的release_date缺失pd.to_datetime会直接抛错。这里常见的做法是用errorscoerce把解析失败的置为 NaT后续再用fillna或直接丢弃。数据量小的时候丢弃几条缺失严重的记录比强行填充更安全。3.2 特征构造数量、占比与时间差原始字段里导演、演员、类型都是斜杠分隔的字符串直接 one-hot 编码会让特征矩阵变得极稀疏。特征分析阶段通常把它们转成计数特征再结合时间信息构造新变量。df[days_since_release] (df[stat_date] - df[release_date]).dt.days df[director_count] df[director].astype(str).str.count(/) 1 df[genre_count] df[genre].astype(str).str.count(/) 1 df[cast_count] df[cast].astype(str).str.count(/) 1 df[avg_score_actor] df.groupby(cast)[score].transform(mean)days_since_release是统计日期与上映日期的间隔反映电影上映了多久是票房预测里最核心的时间特征director_count字段是因为导演字段可能写郭帆/路阳这种联合导演用str.count(/) 1就能算出人数avg_score_actor是演员平均历史得分先把同一演员所有电影的平均分算出来再放回原数据等于把文本名称变成有统计意义的数值。这里有个细节stat_date必须是一个固定的截断日期不能使用预测时点之后的信息。很多毕设在这里犯错用影片的真实后几天数据做特征会导致测试集表现虚高。一般来说统计日期应该取该电影上映后第 N 天比如 N30再去预测最终票房。3.3 标签处理与相关性筛选票房数据是典型的长尾分布几部大片占了绝大部分直接回归会让模型只顾着拟合头部高票房样本忽略中尾部电影。常见的做法是给标签做对数变换把乘法关系变成加法关系。import numpy as np df[log_box_office] np.log1p(df[box_office]) feature_cols [days_since_release, score, director_count, genre_count, cast_count, comment_count] corr df[feature_cols [log_box_office]].corr()[log_box_office].abs().sort_values(ascendingFalse) print(corr)np.log1p相当于log(1 x)好处是即使票房为 0 也能算预测完再np.expm1还原。corr()计算的是皮尔逊相关系数输出的 Series 按与log_box_office的相关性降序排列用来剔除明显无关的特征。比如某个特征相关性绝对值不到 0.1就可以考虑删掉减少 SVR 在高维空间上的拟合压力。在 data_feature.py 里特征分析阶段通常还会计算方差膨胀因子 VIF判断多重共线性。对于 count 类特征如果两个特征相关系数超过 0.8保留一个即可比如cast_count和genre_count有时会同时偏高但实际没有独立贡献。下表是本项目特征工程的输入输出映射原始字段处理后特征处理方式说明release_datedays_since_release日期求差从上映到统计日间隔directordirector_countcount(/)1联合导演人数genregenre_countcount(/)1类型数量castcast_countcount(/)1主演数量comment_countcomment_count直接数值化评论数做特征scorescore中位数填充缺失值处理4. SVR 回归器的参数调优与模型评估把特征矩阵和 log 票房送到 SVR 之前得先搞清楚 SVR 在小样本回归上的优势以及参数怎么影响结果。svm_movie.py 是整个系统的模型部分下面按照从原理到调参的顺序展开。4.1 为什么是 SVR 而不是线性回归线性回归试图找到一组权重 w让y w·x b的误差平方和最小。问题在于票房和特征的关系并不是一条直线上映天数、评分、评论数对票房的影响是边际递减的前期宣传效果强后期口碑作用变大这种关系用线性函数很难刻画。SVR 的思路不同。它用核函数K(x_i, x_j)把样本隐式映射到高维空间在高维空间里做线性回归回到原始空间就是非线性回归。同时 SVR 有一个 epsilon 不敏感带只要预测值和真实值的差值在 epsilon 范围内就不计算损失。这意味着模型不会为了拟合每一个样本点而剧烈抖动对离群影院票房的容忍度比线性回归高。另一个实际好处是SVR 在样本量几百的小数据集上表现稳定。线性回归在小样本上容易受一两个极端值影响而随机森林又可能过拟合到特定模式。SVR 的C参数可以显式控制对误差的惩罚强度是这套系统选它的核心原因。4.2 关键参数C、epsilon、gamma 与 kernel构造 SVR 时几个核心参数直接决定模型能力from sklearn.svm import SVR svr SVR(kernelrbf, C1.0, epsilon0.1, gammascale) svr.fit(X_train, y_train)kernelrbf是高斯径向基核把特征映射到无穷维适合非线性数据C是误差项的惩罚系数C 越大模型越趋向于把训练样本拟合到位C 越小越偏向平滑C 过大容易过拟合epsilon是不敏感带的宽度epsilon 越大支持向量越少模型越稀疏但预测误差上限也变大gamma控制单个样本的影响力范围gamma越小决策边界越平滑gamma越大越能捕捉细节但也越容易过拟合。gammascale是 sklearn 的默认值它等于1 / (特征数 * X 的方差)会自动根据数据规模做缩放。如果特征里既有天数又有评论数量纲差距很大建议先做标准化再让 gamma 用 scale 模式才有意义。参数对结果的影响可以参考下表参数作用常见取值范围调试经验C误差惩罚系数控制过拟合0.1 ~ 100样本少时不建议超过 50epsilon不敏感带宽度0.01 ~ 1标签做了 log 变换后取 0.1 合理gamma径向基核的宽度scale / 0.1 / 0.01过小时模型欠拟合过大时边界崎岖kernel核函数类型rbf / linear / poly线性核可作为 baseline 对比4.3 网格搜索与交叉验证svm_movie.py 的调参流程svm_movie.py 里通常不会手动一个个试参数而是用网格搜索加 K 折交叉验证一次性评估参数组合。这里要特别注意票房数据有上映时间顺序不能随机打乱 K 折否则会泄漏未来信息。常见做法是先用shuffleFalse或者按时间排序后再切分保证训练集都是早于测试集的电影。from sklearn.model_selection import GridSearchCV, TimeSeriesSplit from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (svr, SVR(kernelrbf)), ]) param_grid { svr__C: [0.1, 1, 10], svr__epsilon: [0.01, 0.1, 0.5], svr__gamma: [scale, 0.1, 0.01], } tscv TimeSeriesSplit(n_splits5) grid GridSearchCV(pipe, param_grid, cvtscv, scoringneg_mean_absolute_error) grid.fit(X_train, y_train) print(grid.best_params_)这段代码里Pipeline把标准化器和 SVR 串联起来避免数据泄漏StandardScaler对每个特征做 z-score 标准化因为 SVR 对特征尺度敏感TimeSeriesSplit是按时间窗口递增划分交叉验证避免随机打乱造成未来数据泄漏scoringneg_mean_absolute_error表示用负 MAE 作为评估分数sklearn 里分数越大越好所以 MAE 要取负最终best_params_给出使 MAE 最小的参数组合。这里有个常见误区直接把 pandas 返回的特征矩阵丢进 GridSearchCV 不做标准化。这样 C 和 gamma 的最优值会因特征量纲不同而偏移比如评论数几万、导演人数只有个位数距离计算全部被评论数主导评分等特征等于没起作用。4.4 评估指标与结果还原模型调完之后要在独立的测试集上算 MAE、RMSE 和 R²。from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred_log grid.best_estimator_.predict(X_test) y_true_original np.expm1(y_test) y_pred_original np.expm1(y_pred_log) mae mean_absolute_error(y_true_original, y_pred_original) rmse mean_squared_error(y_true_original, y_pred_original) ** 0.5 r2 r2_score(y_test, y_pred_log) print(fMAE{mae:.1f} 万元, RMSE{rmse:.1f} 万元, R²{r2:.3f})注意这里把预测结果expm1还原成原始票房不能拿 log 空间的误差直接看。mae是平均绝对误差单位是万元直观反映了平均差多少rmse对大误差更敏感如果个别大片预测偏差很大RMSE 会显著大于 MAEr2_score计算的是预测值与真实值的决定系数越接近 1 越好。我复现这类项目时一般会记录两个结果一个是 log 空间的 R²一个是原始票房的 MAE。log 空间的 R² 在 0.7 以上说明模型学到了主要模式原始票房 MAE 则用来判断实际可不可用。如果 R² 很高但 MAE 也很大通常是少数高票房样本预测错了可以检查是不是特征里缺少档期、排片占比这些关键变量。5. 模型保存与接口封装离线预测如何落地到这一步系统已经能训练出 SVR 模型但离“能给别人用”还差两个动作持久化模型和暴露预测接口。svm_movie.py 里训练好的模型如果是进程内变量脚本结束就没了所以需要把模型序列化到磁盘。这里用 joblib 比 pickle 更稳妥它对 numpy 数组和 sklearn 对象的压缩更好。import joblib joblib.dump(grid.best_estimator_, svm_movie_model.pkl) loaded_model joblib.load(svm_movie_model.pkl)保存下来的是完整的 Pipeline包括 StandardScaler 和 SVR加载后可以直接predict不需要重新做标准化。joblib.dump的第二个参数是保存路径默认压缩格式能在几百 KB 的模型文件上表现很好。封装接口时我推荐用 FastAPI 或 Flask。下面是一个最小可用的 Flask 预测接口from flask import Flask, request, jsonify import joblib import numpy as np app Flask(__name__) model joblib.load(svm_movie_model.pkl) app.route(/predict, methods[POST]) def predict(): data request.get_json() features [data.get(days), data.get(score), data.get(director_count), data.get(genre_count), data.get(cast_count), data.get(comment_count)] pred_log model.predict([features])[0] pred_box np.expm1(pred_log) return jsonify({pred_box_office: round(float(pred_box), 2)}) if __name__ __main__: app.run(host0.0.0.0, port8000)这段代码把特征按训练时的顺序排成列表model.predict([features])要求二维数组[features]就是包了一层列表。np.expm1还原对数标签。需要注意的是接口入参必须包含所有训练特征缺一个模型会直接报错测试时可以用curl -X POST -H Content-Type: application/json -d {days:30,score:8.5,director_count:2,genre_count:3,cast_count:5,comment_count:10000} http://127.0.0.1:8000/predict验证。验证这套系统是否真的可靠除了测试集指标外还可以做一种更贴近实际的检查把一部分电影按上映时间切到训练集之外模拟“用过去预测未上映电影”的情况。如果 TimeSeriesSplit 的分数远低于随机 K 折说明模型对时间特征产生了过拟合需要重新审视days_since_release等时序特征的使用方式。票房预测本质上是一个强时效性任务上线使用时要定期重新爬取、重新训练模型文件里的核函数参数和缓存 woff 一样都需要版本管理。最后留一个调试技巧当预测结果全部偏向均值时先检查epsilon是不是设置得太大当某个特征缺失导致预测结果突变时在接口里增加字段校验返回 400 和缺失字段名比让模型抛异常更便于排查。本文还有配套的精品资源点击获取
返回列表