ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的电影数据分析系统:爬虫、Flask与机器学习实战

基于Python的电影数据分析系统:爬虫、Flask与机器学习实战 每年3月底到5月是高校毕设最焦灼的时段。我见过不少选题写着基于Python的电影数据分析系统的同学一上来就卡在爬虫被反爬、Flask页面死活不出数据、机器学习模型跑不出指标这三道坎上。这篇文章我想把这类项目按毕设交付的标准完整拆一遍豆瓣电影数据怎么抓得稳、分析维度怎么定得有说服力、Flask可视化页面怎么把整个链路串起来以及答辩时老师大概率会问什么。如果你手里正好是这个题或者想把它改造成数据分析方向的个人项目照着这份思路走能少走很多弯路。1. 项目拆解电影数据分析系统到底在解决什么问题1.1 从毕设选题视角看这个项目先别急着写代码。你得先搞清楚老师看一个毕设到底在看什么。这类项目的本质是一条完整的数据流水线采集 → 清洗 → 存储 → 分析 → 可视化展示。表面上看你只是做了一个网站把豆瓣电影数据爬下来再展示出去。但老师的评分逻辑通常是这样的系统完整度从数据采集到最终展示链路是否闭环。技术栈覆盖面是否用了爬虫、数据库、Web框架、数据分析、机器学习这些该有的东西。分析逻辑是否有说服力不是我写了代码就行而是你能否用数据回答某个问题——比如什么因素最影响电影评分。工程规范性代码结构、异常处理、文档是否像样。说白了这个项目是以数据为中心的Web应用机器学习和可视化都是数据价值的放大器。理解了这个定位你就知道为什么很多同学只写了一个爬虫表格页面却被扣分——因为压根没体现出分析。我当时做类似项目时把核心目标定为一句话让用户通过可视化面板直观看到豆瓣电影TOP250的整体画像并能用机器学习模型预测一部新电影的大致评分。这句话既能指导开发也方便在答辩时一句话讲清楚项目价值。1.2 技术栈选型背后的为什么标题里已经锁定了Python、Flask、requests、机器学习、大数据但具体怎么选是有逻辑的。Python 3.8数据处理生态最全pandas、scikit-learn、requests全是老朋友。这里有个小提醒除非你的开发机器有问题否则直接装最新稳定版不必为了兼容性而用3.6。requests做HTTP请求的利器。为什么不选ScrapyScrapy功能很强但它的异步调度、中间件机制对毕设来说有点重学习成本高。requests直接、可控做顶层数百条数据的抓取完全够用。我建议先把requests玩熟再去碰Scrapy。Flask轻量级Web框架。为什么不选DjangoDjango自带ORM、Admin后台、Auth体系功能全但你要为用不到的功能付出大量理解成本。Flask灵活路由、模板、接口随便写而且毕设答辩时你完全能说清楚Flask的请求处理流程这在Django里要解释的东西就多得多。MySQL存结构化数据。TOP250电影的字段很规整片名、评分、评价人数、导演、主演、年份、类型、台词。MySQL按字段存查询方便也符合大数据项目对存储引擎的预期。scikit-learn做评分预测。它封装好了线性回归、随机森林等模型几行代码就能跑通。ECharts前端可视化。基于JavaScript渲染交互图表非常漂亮而且不需要你从头写Canvas。这套组合的优点在于每一层都有成熟方案但你又必须亲手把它拼起来。拼的过程就是毕设真正考察你的过程。1.3 项目目录结构与模块划分不管老师要不要求我强烈建议你上来就把目录结构搭好。这是工程化的第一印象。movie_analysis/ ├── app.py # Flask入口注册蓝图、启动服务 ├── config.py # 数据库及通用配置 ├── crawler/ │ ├── spider.py # 爬虫核心请求、解析、调度 │ └── parser.py # 单页解析函数 ├── analyze/ │ ├── preprocess.py # 数据清洗、特征工程 │ ├── train_model.py # 训练评分预测模型 │ └── eval_model.py # 模型评估 ├── web/ │ ├── views.py # 路由视图页面和数据接口 │ ├── templates/ # Jinja2模板 │ └── static/ # CSS、JS、ECharts资源 ├── data/ │ └── movies.csv # 爬取后的中间数据 └── requirements.txt注意crawler、analyze、web这三大模块正好对应数据流水线的前中后段。答辩时你只需要指着这个目录说一遍就比90%临时堆代码的同学强。后面所有操作都围绕这套结构来。2. 数据采集层豆瓣爬虫的设计思路与反爬应对2.1 requests爬虫的完整工作流这个项目里爬虫的常规目标是豆瓣电影TOP250地址是https://movie.douban.com/top250。它的翻页规律非常经典每页25部电影URL通过?start0、?start25、?start50这样步进。我的建议是先写一个最小原型确认能正常拿到内容再逐步加上健壮性处理。最小原型长这样import requests from bs4 import BeautifulSoup url https://movie.douban.com/top250?start0 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(url, headersheaders, timeout5) soup BeautifulSoup(resp.text, html.parser) print(soup.title.string)能打印出页面的title说明你已经可以走进豆瓣。接下来要做的是把每一条电影信息精确地抠出来。我见过不少人在这里踩坑直接用find_all(div, class_item)定位条目结果因为标签嵌套层次搞错了提取出来的片名、导演对不上位。我的习惯是先用浏览器开发者工具打开豆瓣TOP250找到一条电影对应的HTML块逐层写selector。比如提取标题和评分def parse_one_item(item_div): title_tag item_div.find(span, class_title) title title_tag.text.strip() if title_tag else rating_tag item_div.find(span, class_rating_num) rating float(rating_tag.text.strip()) if rating_tag else 0.0 quote_tag item_div.find(span, class_inq) quote quote_tag.text.strip() if quote_tag else return {title: title, rating: rating, quote: quote}这里的核心是理解一个条目内部标题、评分、导演、引用语分别藏在哪个class里。不同网站的HTML结构都不一样没有固定公式但方法是一样的打开F12 - 选中元素 - 写select - 本地打印验证。我通常会在解析完一页后先把结果print出来人工核对一遍再继续写循环——不要偷这个懒。2.2 反爬机制与工程化兜底豆瓣的反爬其实不算最狠的但足够让不管三七二十一就高频请求的同学喝一壶。最常见的现象是前几页正常第4页开始返回418 Im a teapot或者验证页。核心应对策略就三件事User-Agent轮换把Headers里的User-Agent伪装成常见浏览器的UA不要每次都发同一个也不要裸奔。写一个UA列表随机选取。控制请求频率每次请求之间time.sleep(1)到time.sleep(3)。很多人觉得爬得快才高效但在毕设场景里250条数据几十秒就能抓完你根本不需要追求速度慢一点反而更稳定。增加重试机制用try/except捕获超时和HTTP错误码遇到403、418就让程序暂停几秒再重试最多重试3次。我自己常用的模板是from time import sleep import random import requests UA_LIST [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/120.0, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Firefox/121.0, ] def fetch_page(url, max_retries3): for attempt in range(max_retries): headers {User-Agent: random.choice(UA_LIST)} try: resp requests.get(url, headersheaders, timeout5) if resp.status_code 200: return resp.text else: sleep(2 * (attempt 1)) except requests.RequestException: sleep(2) return None把抓取逻辑抽象成fetch_page解析逻辑单独放这样爬虫挂了你只需要重启爬虫不用重启Flask应用。很多同学以为只要服务器不封IP就行忽略了一个事实同一IP高频请求同样会被识别。所以即便你只是做一个毕设最好也别开着多线程去爬。线程是能加速但带来的封禁风险不值得。2.3 解析与入库最容易翻车的一环爬虫拿到的是HTML字符串必须转成结构化数据。我习惯先把所有电影解析成list[dict]再一次性写入数据库。这样便于中途调试和多次入库。数据清洗是个细活。豆瓣TOP250的典型字段包括字段示例清洗要点title肖申克的救赎提取主标题去掉中文名/英文名混杂部分rating9.7转浮点型手动处理暂无评分rating_num2920000转整数型director弗兰克·德拉邦特可能包含多导演用/分隔actors蒂姆·罗宾斯 / 摩根·弗里曼截取前3位减少噪声year1994正则提取四位年份types剧情 / 犯罪按/拆开后strip空格quoteHope is a good thing有些电影没有台词填空字符串建表语句我建议保持简单但字段留足够的冗余CREATE TABLE movies ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, rating FLOAT, rating_num INT, director VARCHAR(200), actors VARCHAR(300), year INT, types VARCHAR(100), quote VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个容易被忽略的细节访问豆瓣TOP250每页提取的25条记录本身不含排名字段但你完全可以按爬取顺序给它们编号。如果后面要做Top10排行榜之类的可视化这个编号就有用了。3. 机器学习分析层从数据升维到结论3.1 分析维度的设计机器学习不是硬塞进项目里的装饰品它必须回答一个能用数据验证的问题。在这个电影系统里最自然的切入点是豆瓣电影的评分到底和哪些因素有关以及能否根据这些因素预测一部电影的评分围绕这个问题分析维度可以这样设计基础统计评分的整体分布、各年份电影数量趋势、不同类型电影的占比。关联分析评分与演员、导演、类型、年份之间的相关性。机器学习建模用特征工程后的数据训练模型预测新电影的评分。我特别建议你把分布图和预测模型分开讲分布图是描述性分析负责把画像展示给用户预测模型是推断性分析负责展示机器学习能力。两者结合正好覆盖大数据分析的两个层次。3.2 一个贴合数据集的评分预测模型以我的经验250条数据的样本规模不大不要一上来就上深度学习。线性回归、决策树、随机森林这些传统模型完全够用而且更容易解释。随机森林是首选因为它能处理数值特征也能给出特征重要性答辩时特别加分。实现思路是这样的from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.preprocessing import OneHotEncoder import pandas as pd import numpy as np # 读入爬好的数据 df pd.read_csv(data/movies.csv) df[year] pd.to_numeric(df[year], errorscoerce) df[rating_num] pd.to_numeric(df[rating_num], errorscoerce) df[rating] pd.to_numeric(df[rating], errorscoerce) # 把类型字段拆开做One-Hot types_list df[types].str.split(/) # 假设我们只保留出现频率最高的10个类型 all_types set() for types in types_list.dropna(): all_types.update([t.strip() for t in types]) top_types [剧情, 爱情, 犯罪, 动作, 喜剧, 科幻, 动画, 悬疑, 惊悚, 冒险] for t in top_types: df[t] df[types].apply(lambda x: 1 if isinstance(x, str) and t in x else 0) # 选择特征 features [year, rating_num] top_types X df[features].fillna(-1) # 缺失年份填-1使其不影响树模型 y df[rating] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model RandomForestRegressor(n_estimators100, random_state42) model.fit(X_train, y_train) score model.score(X_test, y_test) print(R^2:, score)这里有一个直接关系到模型效果的关键点要区分评价人数和评分这两个概念。评价人数是评论的条数表示热度评分是豆瓣用户打的星级表示口碑。如果模型里把评价人数作为特征预测出来的评分会偏向热度高的电影这在逻辑上说不通。所以特征里要不要放rating_num你必须在答辩时能讲清楚理由。3.3 特征工程与模型评估的坑基于我的实操经验这类项目在特征工程和模型评估上有几个特别值得注意的坑缺失值处理不能一刀切。比如有些电影没有台词字段这很正常你要把quote处理成未知而不是删除整行有些老电影年份缺失倒不如直接丢弃——样本就250条删掉几条对总体影响不大但填充一个假年份会让模型产生错误信息。One-Hot编码不要无脑全量编码。豆瓣TOP250的类型有十几种纪录片传记占比极低全部编码会稀释特征密度。我建议只保留出现频率最高的10类其余归为其他。这样模型更稳定图表也能聚焦主要矛盾。评估指标要选对。回归模型常见指标是均方根误差RMSE和R²。对电影评分这种0~10区间的数据RMSE在0.5以内就算比较理想了。千万不要只看R²R²在样本少的时候虚高答辩时老师一问就露馅。另外我强烈建议你在训练前打印一下特征的dtypes。机器学习最常见的报错就是 could not convert string to float这通常是因为年份被读成了字符串。处理好类型后面才不会被这种低级错误浪费时间。很多同学做完模型就结束了但我会再做一步输出特征重要性排序。随机森林模型直接有feature_importances_属性把它画成柱状图就能直观告诉用户年份和类型对评分的影响有多大。这是一个成本极低、答辩效果极好的操作。import matplotlib.pyplot as plt importance model.feature_importances_ names X.columns plt.figure(figsize(10, 6)) plt.barh(names, importance) plt.xlabel(Importance) plt.title(Feature Importance in Rating Prediction) plt.tight_layout() plt.savefig(web/static/images/importance.png)4. Flask应用层把数据和模型变成可视化页面4.1 Flask路由与数据接口设计很多同学写完爬虫和分析就不知道干什么了——这些人往往会在Flask里把所有Python对象直接塞给模板渲染代码乱成一锅粥。我更推荐的是接口式开发Flask只负责提供JSON数据接口页面负责通过Ajax异步获取数据并交给ECharts渲染。这样项目结构清晰也好调试。一个典型的Flask蓝图from flask import Blueprint, jsonify import pymysql api_bp Blueprint(api, __name__, url_prefix/api) def get_db(): conn pymysql.connect( hostlocalhost, userroot, password123456, databasemovie_db, charsetutf8mb4 ) return conn api_bp.route(/movies) def movies(): conn get_db() cursor conn.cursor() cursor.execute(SELECT title, rating, year FROM movies ORDER BY rating DESC LIMIT 20) rows cursor.fetchall() conn.close() return jsonify([{title: r[0], rating: r[1], year: r[2]} for r in rows])为什么用pymysql而不是别的ORM我的理由是毕设阶段你完全能读懂原生SQL用ORM反而增加一层抽象答辩时老师让你写SQL你却写不出来会很尴尬。而且从这个接口返回的JSON结构正好对应ECharts里series.data的格式基本不用二次处理。4.2 ECharts可视化面板的实现思路前端首选ECharts它的图表类型丰富鼠标交互流畅而且文档非常详细。最常见的几种图表评分分布直方图展示电影评分集中在哪个区间。年份与评分散点图看老电影是否评分更高。类型分布饼图看TOP250里哪种类型占比最大。特征重要性条形图展示机器学习模型的结论。在模板里引入ECharts编写Ajax请求div idrating-chart stylewidth:100%; height:400px;/div script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script fetch(/api/rating_dist) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(rating-chart)); chart.setOption({ title: { text: 电影评分分布 }, xAxis: { type: category, data: data.bins }, yAxis: { type: value }, series: [{ type: bar, data: data.counts }] }); }); /script这里有一个前端细节非常容易踩坑图表容器必须设置明确的宽高。很多同学写完发现图表不显示或只占一小块原因往往是div没有设置高度。调成height: 400px或者给它一个固定的CSS类问题立刻解决。另外ECharts在页面初始化时如果模板渲染完成后容器还没有加载完也会导致空白。所以脚本要么放在DOMContentLoaded事件里要么放在/body前面。4.3 前后端联调与页面性能前后端联调时一个重要原则是接口先单独验证再接入页面。我习惯在一个浏览器标签页里直接访问/api/movies确认返回的是合法JSON再去写前端。这样可以省掉大量到底是我接口写错了还是前端拿错字段的怀疑时间。这部分还有一个容易被忽视的性能问题。你已经爬了250条电影数据但如果做一个全部电影列表页一次性渲染250条记录在本地完全没问题可是如果加上了图片或者大段文本页面加载就会变慢。我的建议是列表页做分页每页显示20条或30条。图片懒加载不一次性响应所有图片请求。数据接口只返回当前需要的字段不要SELECT *。另外要说的是Flask自带服务器是单进程单线程只能用于开发和展示。毕设答辩当天如果老师现场让你点一下页面本地跑没问题。但如果你准备部署到服务器或者跑在线演示一定要换用gunicorn或uwsgi启动Flask否则并发稍微高一点就卡住。5. 毕设验收视角演示路径与答辩准备5.1 系统演示的动线设计答辩现场时间有限你的演示必须主次分明。我会建议按下面这条动线来走第一站数据采集演示打开爬虫脚本所在页面显示数据库里的电影总数展示爬虫运行日志。这一站告诉老师我的数据是真实爬来的不是造出来的。第二站数据分析面板展示评分分布、类型占比、年份趋势这几张图表用一两句话说清楚结论——比如评分集中在8到9分之间剧情片和犯罪片占比最高。言简意赅先证明数据有价值。第三站机器学习部分输入一个电影的特征比如年份2019类型为喜剧爱情前端调用预测接口返回一个预估评分。这里老师会非常感兴趣因为这是整个系统里智能的部分。演示结束。不要逐页展示所有代码没时间也没必要。每站控制在1分钟内总共3分钟讲完剩下时间留给提问。5.2 答辩常问的问题与回答思路根据我的观察老师高频提问有以下几类提前准备答案很重要为什么用随机森林而不用线性回归回答思路数据量不大线性回归相对单一随机森林能捕捉非线性关系并且有内置的特征重要性适合解释电影评分这种多因素影响的问题。可以补充一句我用训练集测过线性回归R²不到随机森林的一半所以我选择了后者。如果爬虫被反爬封了怎么办回答思路我控制请求频率、轮换UA、加了重试。即便被封了我爬到的数据已经入库系统仍然可以正常运行爬虫只是数据采集模块而非系统核心。250条数据这么少机器学习有说服力吗回答思路样本量少确实是局限所以我强调了这是基于公开榜单的探索性分析模型用于预测相对评分趋势而非精准值。如果要做更强的模型可以扩展数据源比如豆瓣全部热门电影或者加入IMDb数据。评分预测模型删除了评价人数这个特征为什么回答思路评价人数代表热度而非口碑模型若用它会对粉丝多的电影给出偏高评分干扰对内容质量的学习。我保留了年份和类型这些内容特征让模型更聚焦于哪些内容属性导致高分。这些问题没有标准答案但你必须展现出思考过权衡过程的样子。这比背一段华丽的项目介绍有用得多。5.3 个人使用后的几点遗憾和改进建议最后分享几个我做完类似项目后的真实体会属于复盘部分。最遗憾的事是当时没有给爬虫做增量更新功能。为了拿到新的电影数据我得手动重跑脚本然后清空数据库重插。如果你有时间建议给爬虫加一个按更新时间增量抓取、只插入新数据的逻辑这样系统能长期运行演示的时候直接打开页面就是最新数据观感好很多。第二件遗憾的事是预测模型的特征过于单一。我只用了年份和类型演员、导演这些信息虽然爬下来了但并没有转成结构化特征。如果你不嫌麻烦把导演的历史平均评分、演员的历史平均评分作为特征模型效果会好很多而且讲起来也更有故事性。第三点建议是界面别太程序员审美。花一点时间调一下布局、配色、字体加一点CSS视觉上先赢。很多老师不懂你代码有多优雅但看第一眼页面好看不好看心里已经有了印象分。Bootstrap或者简单的手写CSS都行别让页面停留在黑白表格。根据我个人的经验这种数据处理Web展示机器学习的三段式项目是最容易在毕设里拿到良好以上评价的车型。它不像纯算法研究需要数学功底也不像纯网站开发缺少数据深度正好卡在实用和有深度之间的平衡点上。你只要把数据链路跑通、把每个决策背后的理由准备好整个过程其实没那么可怕。
返回列表