多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

猫眼电影票房预测:爬虫+特征分析+SVR回归全流程实战

猫眼电影票房预测:爬虫+特征分析+SVR回归全流程实战 简介基于猫眼电影真实数据与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;q0.9, } def fetch_html(url): resp requests.get(url, headersheaders, timeout10) 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_fileproxies.txt): with open(ip_file, r, encodingutf-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_name0) # 去掉完全重复的行 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[上映日期], errorscoerce) 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(ascendingFalse) print(corr_with_target) # 可视化想看人数 vs 票房 df.plot.scatter(x想看人数, y票房万元, alpha0.5)运行后基本会看到「首日票房」和「想看人数」两个特征与最终票房的相关性明显高于其他字段档期码的相关性通常很弱。这说明猫眼票房预测任务里前期热度数据的解释力比档期强。相关性弱不代表没价值但做特征筛选时可以适当降权。需要注意的是corr 只衡量线性相关SVR 用 rbf 核能拟合非线性关系所以相关性靠后的特征不一定对模型没用。这个环节更重要的作用是发现异常如果「票房万元」和某个特征的相关系数接近 1就要警惕是否有数据泄漏比如把最终票房的一部分当成了特征。4. SVR 回归建模与调参把预测误差压下去4.1 为什么选 SVR小样本回归的稳健选择电影票房数据有个明显特点样本量通常只有几百到几千条而且存在大量离群值。头部爆款电影票房可能是普通电影的几十倍这种分布对线性回归和普通神经网络都不友好。SVR 在这种情况下比它们稳核心在于它的损失函数只惩罚超出 epsilon 区间的误差对离群值没有线性回归那么敏感。模型对离群值敏感度非线性拟合小样本表现调参成本线性回归高弱中低决策树中强中中SVR低强好中XGBoost中强依赖样本量高在这个项目的样本量下通常几百部影片XGBoost 容易过拟合SVR 反而能给出更稳定的结果。这套源码选 SVR 是合理的也不是为了显得高端。4.2 特征归一化与数据拆分SVR 对特征量纲极其敏感。如果「想看人数」是几百万量级「档期码」只有 0-3gamma 会直接忽略掉小量纲特征。所以训练前必须先做标准化。常用的归一化有两种StandardScaler 把特征变成均值 0、标准差 1MinMaxScaler 把特征压缩到 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_size0.2, shuffleTrue, random_state42 ) svr SVR(kernelrbf) 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(kernelrbf), param_grid, cv5, scoringr2, 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, squaredFalse)) 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.pyprint 步输出乱码或者 pandas 的 to_excel 报 UnicodeEncodeError。原因Windows 终端默认编码经常是 GBK而源码字符串是 UTF-8 编码print 中文时编码冲突。Excel 对编码也很挑剔直接写入普通 UTF-8 文件时中文表头可能变成乱码。解决写入 Excel 时统一用 OpenPyXL 引擎不需要额外设置如果是存 CSV指定 encodingutf-8-sig这个编码会让 Excel 正确识别中文。控制台乱码可以在运行前设置环境变量 PYTHONIOENCODINGutf-8或者在脚本开头对 sys.stdout 重写import sys sys.stdout.reconfigure(encodingutf-8)5.3 SVR 预测值全部集在均值附近R2 是负数现象训练完成后打印 R2分数是负的把预测值和真实值并列对比预测值基本没什么变化像一条水平线。原因这类现象大概率是两个原因叠加——目标列泄漏和量纲混乱。目标列泄漏是指特征里混入了「最终票房」本身或与它高度重合的字段比如把「累计票房」同时放进了 X 和 y模型直接乱掉。量纲混乱指想看人数、评分、票房三者数量级差异太大rbf 核直接忽略了小数特征。解决先检查 feat_cols 里有没有和 target 相同的列或派生列有就删掉然后按上一章的做法用 StandardScaler 对特征做标准化。还有一个不起眼但常见的坑train_test_split 没有设置 shuffleTrue默认状态在有序数据下会让训练集和测试集分布严重失衡加上 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, squaredFalse)) print(R2:, r2_score(y_test, y_pred)) result.to_excel(movie_predict_result.xlsx, indexFalse)误差率是最直观的指标我一般按误差率小于 0.3 的样本占比去判断模型是否能用。真实业务里票房预测误差在 30% 以内已经算不错不用追求绝对精准。如果你想快速验证这套流程用在别处的效果替换数据源就行把 movie.xls 换成你自己整理的其他影片数据特征列名称对齐后预处理、特征分析、SVR 调参这条链路完全可以复用。我第一次复现这个项目时上来就直接跑 svm_movie.py结果预测值全在均值附近。排查了半天才发现特征里混进了目标列去掉之后 R2 才从负值变成正常水平。从那以后我每次拿到这类回归项目都会先检查目标列有没有泄漏、特征量纲是否统一再去看指标。希望帮到你。本文还有配套的精品资源点击获取
返回列表