ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python电影数据可视化分析:从爬虫采集到Flask系统实战

Python电影数据可视化分析:从爬虫采集到Flask系统实战 简介一个以豆瓣电影为数据来源的Python可视化分析系统作为高分毕业设计作品面向计算机专业学生与数据分析入门者完整展示从爬虫采集、SQLite存储到Flask后端整合ECharts、BootStrap与词云展示的实战链路。资源共2000个文件大小76.51MB核心为1684个Python源码文件另含124个HTML页面、93个JavaScript脚本、18个CSS样式以及若干配置与说明文档代码结构清晰便于二次修改与学习。该项目已通过严格调试评审分95分以上下载后可直接运行体验完整功能。目前已有1018人学习适合需要参考毕业设计实现思路、学习Python全栈数据可视化或复用同类项目框架的读者。1. 电影数据可视化分析系统毕业设计最容易出彩也最容易翻车的方向做过数据分析类毕设的人都有体会答辩老师最常问的一句话不是“你用了什么算法”而是“你的数据从哪来图表的结论是什么”。基于Python的电影数据可视化分析系统本质上是一条从数据采集、字段清洗、统计聚合到图表输出的完整链路它把爬虫、pandas、Matplotlib和Flask串成一个可演示的Web系统几乎覆盖了数据类毕业设计的所有高频考点。适合计算机、大数据、数据科学方向的学生拿来直接做底稿也适合工作后想补一个数据案例的从业者快速热身。但说句实话这个题目做出来不难做到“能答辩、禁得住追问”却有不少细节坑从豆瓣反爬到中文乱码再到图表横坐标挤成一团每一关都能让人卡住本文把整套方案和踩过的坑一次讲清楚。2. 数据从哪来爬虫和公开数据集如何凑齐分析底料2.1 豆瓣Top250与TMDB公开API两条数据来源的取舍做电影数据分析最理想的数据集包含评分、票房、类型、导演、主演、年份、地区等字段。国内学生最常用的两个来源是豆瓣Top250和TMDBThe Movie Database公开API。豆瓣的好处是字段全、中文友好、可以直接爬取静态HTML但反爬策略时松时紧且没有官方API属于灰色路径自己练习没问题课程报告里最好注明“仅作学习用途”。TMDB则提供免费API Key有完备的演员表、预算、收入字段甚至能拼出票房分析但需要注册申请Key而且英文数据多还得自己做中文翻译映射。我一般建议两条腿走路先用豆瓣Top250爬一份“老师看得懂”的中文数据集做展示再用TMDB API补上预算和票房字段做进阶分析。要注意的是TMDB的接口有速率限制免费版每秒约1到2次请求批量拉数据时务必sleep否则会被封IP。数据量上毕设展示不必追求全量数据500到1000部电影的样本量已经足够让评分分布和年份趋势图表显出统计规律。2.2 爬虫代码的最小骨架请求头、解析与去重豆瓣Top250的HTML结构稳定翻页靠URL的start参数每页25部共10页。用requests抓页面再用BeautifulSoup解析是入门成本最低的组合。以下是可直接运行的最小骨架我用Python 3.8到3.11都跑过只要requests和beautifulsoup4装上就能用import time import requests from bs4 import BeautifulSoup headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_one_page(start): url fhttps://movie.douban.com/top250?start{start} resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() soup BeautifulSoup(resp.text, lxml) items [] for li in soup.select(.grid_view .item): title li.select_one(.title).text.strip() rating li.select_one(.rating_num).text.strip() quote li.select_one(.inq) quote quote.text.strip() if quote else items.append({title: title, rating: float(rating), quote: quote}) return items all_movies [] for start in range(0, 250, 25): all_movies.extend(fetch_one_page(start)) time.sleep(2) # 控制请求频率避免被反爬 print(f共抓取 {len(all_movies)} 部电影)这段代码里headers是关键中的关键没有User-Agent的请求几乎必然被豆瓣直接拒绝Accept-Language带上zh-CN可以保证返回中文页面避免解析到英文标题。select_one的CSS选择器直接对准豆瓣列表项里的类名抓不到字段时不要急着改代码先在浏览器F12里看页面结构豆瓣改版频率不高但偶尔会有微调。循环末尾的sleep(2)是在用最笨也最有效的方式规避反爬两秒一次请求10页也就多等18秒值得。注意requests和beautifulsoup4需要先用pip install requests beautifulsoup4 lxml安装如果numpy库还没装顺手一起装掉后面的表格运算也离不开它。2.3 用pandas做字段规整演员列表、导演、评分一个都不能少原始列表里只有片名、评分和一句话短评做可视化还缺年份、导演、主演、类型、片长这些关键字段。豆瓣详情页里能补全这些信息但逐条访问250个详情页对毕设来说成本太高。更务实的做法是只对Top10或Top50做详情补抓其余保留基础字段之后分析时按“基础字段做评分分布和年份趋势补充字段做导演和类型分析”。用pandas规整字段时最常遇到的是脏数据评分是字符串、年份混在标题里、演员表有多人。以下代码演示了如何用DataFrame完成清洗import pandas as pd df pd.DataFrame(all_movies) # 从霸王别姬 (1993)这种标题中提取年份 df[year] df[title].str.extract(r\((\d{4})\)).astype(Int64) # 去掉标题中的年份后缀保留纯净片名 df[clean_title] df[title].str.replace(r\s*\(\d{4}\), , regexTrue) # 评分已经是float类型无需转换若为字符串则必须强转 df[rating] pd.to_numeric(df[rating], errorscoerce) df df.dropna(subset[rating, year]) print(df.describe())str.extract用正则把孩子从标题里精确抠出年份Int64是pandas的空值安全整数类型比int更能容忍缺失值。errorscoerce是毕设里的保命参数评分列里一旦混入“暂无评分”这样的文本默认转换会直接抛异常加上coerce会把非法值转成NaN后续dropna统一清理。注意pandas早期版本对正则替换的API略有差异如果跑出来列名找不到先检查pandas版本pip show pandas2.x版本与1.x在str.replace的正则默认值上有细微差别。2.4 数据落库CSV还是SQLite毕业设计怎么选清洗完的数据需要落地保存CSV和SQLite是两种主流选择。CSV的好处是肉眼可见、Excel可开、答辩时可以直接展示数据文件适合数据量小、分析步骤简单的情况。SQLite则适合需要按条件查询、多表关联的场景比如你想做“导演-电影-评分”的多表join。但毕设项目要控制复杂度我默认推荐先导出CSV等到系统里需要做动态筛选时再用SQLite也不迟。df.to_csv(movies.csv, indexFalse, encodingutf-8-sig) # 若需导入SQLite import sqlite3 conn sqlite3.connect(movies.db) df.to_sql(movies, conn, if_existsreplace, indexFalse) conn.close()utf-8-sig是第二个保命参数。直接用UTF-8编码写CSV用Excel打开会乱码因为Excel默认用GBK读取CSV文件。加上-sig后Excel能正确识别UTF-8 BOM头这是国内处理CSV最容易踩的编码坑。至于SQLiteto_sql一行就能建表后续查询用pd.read_sql_query(SELECT * FROM movies WHERE rating 8.5, conn)即可不需要手动建表维护结构。3. 从DataFrame到图表用Matplotlib和Seaborn做出能进答辩PPT的图3.1 先定问题再选图表评分分布、年份趋势、类型对比拿到干净数据后不要急着画图先把要回答的问题列出来。常见方向有三个评分分布是否呈正态、高评分电影是否集中在某个年代、不同类型电影的评分水平是否有显著差异。问题决定图表类型评分分布用直方图加核密度曲线年份-评分关系用散点加回归线类型对比用水平条形图。Matplotlib擅长精细控制细节Seaborn擅长统计图表和多变量关系展示两者组合使用是数据分析项目的标配。很多人在这一步卡住不是不会写代码而是不知道选什么图。一个朴素的判断方法要展示“多少个”用直方图或条形图要展示“趋势”用折线或散点回归要展示“占比”用饼图但我几乎不用饼图答辩时容易被问比例是否准确。项目里把三类图都做全答辩时才能呈现出完整的分析逻辑。3.2 评分分布直方图核密度估计与坐标轴截断评分分布是最能体现统计基础的一幅图。豆瓣的评分集中在7到9.5分之间如果直接画全区间直方图会看到左边一大片空白分布形态被拖扁。处理方法是设置合理的bins区间和x轴范围再叠加核密度估计曲线让分布轮廓更平滑。import matplotlib.pyplot as plt import seaborn as sns import pandas as pd df pd.read_csv(movies.csv, encodingutf-8-sig) plt.rcParams[font.sans-serif] [SimHei] # 中文字体 plt.rcParams[axes.unicode_minus] False # 解决负号显示为方块 fig, ax plt.subplots(figsize(10, 6)) sns.histplot(df[rating], bins15, kdeTrue, color#4C72B0, edgecolorwhite, axax) ax.set_xlim([6, 10]) ax.set_xlabel(豆瓣评分) ax.set_ylabel(电影数量) ax.set_title(电影评分分布集中在7-9分高分区出现长尾) plt.tight_layout() plt.savefig(static/rating_dist.png, dpi150)bins15是经过试错的经验值电影样本约250条时15到20个柱子能兼顾细节和光滑度太少丢失分布特征太多会出现锯齿。kdeTrue在直方图上叠加核密度曲线曲线能帮助评委一眼看出分布中心和偏态。这两行rcParams中文字体设置是项目里所有图表的公共前置代码建议单独放到一个style.py里统一导入避免每张图重复写。保存图片时static/目录是为后续Flask系统准备的现在就先留好接口。3.3 年份-评分散点回归用regplot展现年代变迁趋势年份-评分关系适合回答“最近这些年电影评分是否变低了”之类的问题用Seaborn的regplot一步就能出散点和线性回归线。但直接画会有严重的重叠点问题同一年的多部电影会互相遮挡回归线容易受离群点干扰。此时需要给散点加透明度和抖动。import numpy as np fig, ax plt.subplots(figsize(12, 6)) sns.regplot( datadf, xyear, yrating, scatter_kws{alpha: 0.5, s: 30, color: #55A868}, line_kws{color: #C44E52, linewidth: 2}, axax, ) ax.set_xlabel(上映年份) ax.set_ylabel(豆瓣评分) ax.set_title(不同年份电影评分变化趋势) plt.tight_layout() plt.savefig(static/year_rating.png, dpi150)alpha0.5让重叠点呈现半透明效果颜色越深表示重合度越高s30控制散点大小这些参数都是调出来的。回归线默认带置信区间阴影答辩时这层阴影常被问到要能解释“阴影越窄说明回归越可靠”。如果年份具有明显的时间间隔不均衡比如1990到2000年样本密集2020到2025年样本稀疏regplot的线性拟合可能失真此时改用lowessTrue局部加权回归作为替换方案会更稳妥。3.4 中文字体和横坐标过密的两个必调参数Matplotlib画图最常见的两个视觉问题中文变方块、横坐标刻度挤成一团。中文问题已经在3.2节里通过rcParams解决但要注意SimHei字体在Linux服务器上可能不存在部署到云服务器时需要改用plt.rcParams[font.sans-serif] [WenQuanYi Zen Hei]或者下载中文字体文件并指定路径。横坐标过密是第二个高频问题尤其是年份跨度大时每一年都显示就糊成一片。常见的处理方案是限制刻度数量或旋转角度# 方案一只显示一部分年份用MaxNLocator from matplotlib.ticker import MaxNLocator ax.xaxis.set_major_locator(MaxNLocator(nbins10)) # 方案二旋转刻度标签适用于分类坐标 plt.xticks(rotation45, haright) # 方案三自定义刻度间隔 import numpy as np ticks np.arange(df[year].min(), df[year].max(), 5) # 每隔5年一个刻度 plt.xticks(ticks)三个方案按场景选用年份是数值型时用MaxNLocator最优雅自动选择10个刻度点展示当横坐标是电影类型这类分类变量且名称较长时旋转45度加haright才能避免标签被截断手动指定np.arange适合需要精确控制展示密度的场景。这组参数的调整顺序建议先旋转后限数量先解决重叠再看可读性两个一起调能少花很多调试时间。4. 把图表变成系统Flask 模板引擎的轻量展示方案4.1 为什么选Flask而不是Django毕设要求是“系统”如果只交一个Jupyter Notebook会被评委直接打回所以需要包一层Web展示。选Flask而非Django的理由很实际Flask是微框架一个app.py文件就能启动整个系统路由和模板渲染的学习成本能控制在半天Django自带ORM和Admin后台功能强大但对数据分析项目来说是坦克车碾蚂蚁光配置settings.py的时间和报错排查就够写半篇论文。用Flask把已经生成的图表挂在网页上再配合几个筛选参数技术上是完全够用的。4.2 视图函数、静态图表和模板渲染的最小流程Flask项目结构很清晰项目根目录放app.pytemplates/目录放HTML模板static/目录放图表图片和CSS。以下是最小可运行的app.py实现了首页和评分分布页两个路由from flask import Flask, render_template import os app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/rating) def rating(): chart_path static/rating_dist.png return render_template(chart.html, chartchart_path, title评分分布分析) if __name__ __main__: # debugFalse答辩演示时避免代码变更导致服务重载 app.run(host127.0.0.1, port5000, debugFalse)render_template会把chart变量传给HTML模板模板里用img src{{ chart }} alt{{ title }}就能显示图片。这里有两个细节端口固定为5000避免每次启动端口随机变化导致浏览器地址不一致debugFalse是答辩时的硬性要求开着debug模式容易在演示现场因为某个无关文件变动触发重载页面刷一下就白屏。模板文件chart.html最简版本只需要HTML骨架加上路由传入的图片路径!DOCTYPE html html langzh-CN head meta charsetUTF-8 title{{ title }}/title /head body h1{{ title }}/h1 img src{{ chart }} stylewidth: 80%; max-width: 900px; /body /html4.3 让分析结果可交互图表缩略图与筛选参数传递静态图片挂在网页上虽然能看但经不住追问。更体面的做法是让用户通过下拉框选择年份区间或类型系统重新生成或筛选对应图表。Flask里做动态筛选通常有两种路径一种是后端实时重新绘图另一种是前端只做图片切换。后者更省资源适合毕设数据量。from flask import request app.route(/filter) def filter_movies(): min_rating request.args.get(min_rating, default7.0, typefloat) year_start request.args.get(year_start, default1980, typeint) # 用筛选条件重新统计并重新绘图 filtered df[(df[rating] min_rating) (df[year] year_start)] fig, ax plt.subplots(figsize(10, 6)) sns.histplot(filtered[rating], bins15, kdeTrue, axax) ax.set_xlim([6, 10]) ax.set_title(f评分分布≥{min_rating}分{year_start}年后) plt.tight_layout() temp_path static/temp_filter.png plt.savefig(temp_path, dpi150) return render_template(chart.html, charttemp_path, title筛选结果)request.args.get从URL查询参数里读值URL形如/filter?min_rating8.0year_start2000模板里用普通链接或表单下拉框拼出这个URL即可。实时绘图的代价是每次刷新都要重新生成图片数据量小时没有感知但要注意每次savefig覆盖同一个临时文件时浏览器可能因为缓存显示旧图解决办法是在图片路径后加时间戳参数temp_path ?t str(time.time())。4.4 打包与答辩requirements.txt和README的写法系统能跑起来之后还要保证换一台电脑也能跑。毕业设计最普遍的死法是答辩现场换了投影电脑库不全或版本不对双击启动直接报错。一份完整的requirements.txt能让你在五分钟内重建环境。生成方式很简单pip freeze requirements.txt。但在项目交付前我一般会在这个文件里删掉无关紧要的包只保留系统真正依赖的项否则里面会混入几十个和系统无关的库。一个可行的要求清单大致包含Flask、pandas、numpy、matplotlib、seaborn、requests、beautifulsoup4、lxml这几项。README里写清楚以下四步创建虚拟环境、安装依赖、执行爬虫脚本、启动Flask服务。同时把启动命令写成一行python app.py避免新环境上因包缺失报错后求助时还要对着一屏堆栈慌神。5. 避坑毕业设计里反复遇到的6个数据与可视化问题5.1 豆瓣爬虫被拦截页面返回418现象爬虫第一次运行正常第二次运行后返回的页面内容为空或者状态码变成418。原因豆瓣对连续请求有IP和Cookie双重风控短时间内请求次数超过阈值会触发反爬直接返回JSON错误或HTML验证页。解决把请求间隔从2秒提高到5秒并在请求头中定义Cookie字段。如果服务器上已经有被标记的IP等几十秒或分钟级后再试。不要用线程池并发爬单人毕设项目没必要拿整个IP做赌注。5.2 读取CSV后中文显示为乱码现象用pandas的read_csv读入CSV文件后print(df)里的电影名称变成乱码或问号。原因写入时用了encodingutf-8而不是utf-8-sig或者CSV文件是在Excel里手动保存的Excel默认用GBK/ANSI编码。解决写入CSV务必使用df.to_csv(..., encodingutf-8-sig)。如果文件已经生成读取时用pd.read_csv(..., encodinggbk)或encodingutf-8交替试一次哪个能读出来就用哪个务必记住写入和读取的编码要保持一致。5.3 电影类型字段是“动作,冒险”混在一起无法分组统计现象做类型对比时发现每个类型都带逗号df[genre].value_counts()出来全是组合类型根本没法分析。原因爬虫把多值字段抓成了一个字符串没有拆行拆分。解决用pandas的explode函数把每个逗号分隔的类型拆成多行一条电影记录会按每种类型复制一行。示意代码如下# 假设df含有列genre值为动作,冒险 df[genre] df[genre].str.split(,) df_exploded df.explode(genre) df_exploded[genre] df_exploded[genre].str.strip()拆分之后再groupby(genre)[rating].mean()就能得到每种类型的平均分。注意拆分后数据行数会膨胀数据量变大绘图前若有去重需求需另加标识列。5.4 评分直方图柱子太密或全是空档现象设置bins100或者数据本身评分集中在8分附近画出来的图要么一块黑坨要么稀疏漏风看不出分布形状。原因bin数太多导致每个bin内频数过低或数据取值范围太小而bin宽度过窄统计噪声盖过分布规律。解决先用df[rating].describe()查看四分位数再设置适当的bins。样本量在200到500之间时bins15到20比较稳妥。同时结合ax.set_xlim([min_score - 0.5, max_score 0.5])把坐标范围收窄图表的分布轮廓会立刻清晰。5.5 Flask启动正常但浏览器显示“无法访问此网站”现象终端打印出“Running on http://127.0.0.1:5000”但浏览器输入地址无法打开页面。原因最常见的是端口被其他进程占用5000端口被后台服务抢占另有一种情况是app.run(host0.0.0.0)后本机仍然访问127.0.0.1不通可能是防火墙拦截了回环地址。解决先看终端输出有没有异常报错再lsof -i:5000macOS或netstat -ano | findstr 5000Windows查看端口占用把占用进程杀掉或改成其他端口比如port5001。本机演示时不要用host0.0.0.0直接保持127.0.0.1最省事。5.6 pandas版本差异导致API行为变化现象在别人电脑上运行同一份代码df.append报错AttributeError或者pd.df某些方法不存在。原因pandas 2.0版本废弃了很多早期API比如df.append()在1.4版本就标记废弃2.0直接移除字符串正则替换的行为在1.x和2.x之间也有差异。解决项目交付前在requirements.txt里锁版本pandas2.0.3或明确指定使用的版本号。不要把pip freeze当做万能护身符要主动检查代码里是否使用已废弃的API。最稳妥的办法是在文档开头写明“开发环境为Python 3.10 pandas 2.0.3其他版本请先跑通测试脚本”。6. 最后的验证与加分技巧从“能跑”到“能答辩”6.1 用三个测试用例确认分析逻辑没有跑偏答辩前的最后一步不是验收图表好不好看而是确认系统核心逻辑的可靠性。我习惯准备三个固定的测试用例每次改动后跑一遍成本极低。第一个用例检查数据文件非空且关键字段类型正确用df.info()确认rating列是float类型而不是object。第二个用例确认每张图表文件真实生成且文件大小大于10KB小于这个值多半是绘图失败输出了一张空白图。第三个用例Flask筛选路由带参数请求返回HTTP 200用一个简单的测试脚本连续请求三个URL看是否都能打开。这三个用例覆盖数据层、图表层和展示层任何一层出问题都能第一时间定位。6.2 图表解释卡给每张图配一句结论评委看图的耐心有限能在页面或PPT里给出“图注结论”能显著提升专业感。比如评分分布图下写“样本中超过六成电影评分落在7.5到9分之间整体呈现左偏分布”年份趋势图下写“2000年后电影评分波动收窄高分区比例增加”。这不仅让分析有深度也让你在答辩时不至于对着图说不出话。实现上可以在模板中增加一个conclusion参数传入字符串页面上显示在图片下方美观且好讲解。6.3 一键启动脚本把环境配置和启动缩成一条命令我最看重的收尾技巧是整个项目能一键启动。常见做法是在项目根目录放一个start.batWindows或start.shLinux/macOS内容包办建虚拟环境、装依赖和启动Flask# start.shLinux/macOS #!/bin/bash python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python app.py对应的Windows版本去掉source venv/bin/activate改成venv\\Scripts\\activate。这样评委想现场复现时不用问你“需要装哪些库”双击或一条命令就能看到系统。把项目做到这个程度答辩时的底气完全不一样因为你知道无论哪台机器只要网络通顺、有Python环境这套系统就能还原。我个人的习惯是每次答辩前都在一台干净机器上完整走一遍一键启动脚本把环境中缺失的步骤提前补掉。这个动作帮我避过了很多次现场装包翻车的尴尬也让“数据分析系统”这个题目真正落到了“可运行、可验证、可解释”三个词上。希望这个从爬虫到Web展示的完整路径以及文中这些踩坑记录能帮你的毕业设计少走弯路把精力真正花到分析和结论上。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进