首页 / 资讯中心 / 文章详情

Python电影数据可视化分析系统:爬虫、清洗与Flask展示全流程

Python电影数据可视化分析系统:爬虫、清洗与Flask展示全流程 ★ FEATURED ARTICLE
简介这是一套面向高校计算机相关专业学生的毕业设计完整源码围绕豆瓣电影数据构建可视化分析系统适合需要完成课程设计或毕设、希望快速上手数据分析与Web开发的学习者。项目将爬取到的电影数据存入SQLite数据库再借助Flask搭建后端服务配合ECharts、BootStrap与WordCloud实现图表展示、词云生成与页面布局覆盖数据采集、存储、分析与前端呈现的完整链路。压缩包共约2000个文件以1684个Python脚本为核心辅以124个HTML页面、93个JavaScript文件及CSS样式、JSON配置、PDF文档等整体约76.51MB目录结构清晰便于按模块查阅与二次修改。目前已有1018人学习下载项目经严格调试评审分达95分以上可直接运行对新手较为友好。读者可据此掌握爬虫编写、数据库操作、Flask接口设计与ECharts可视化配置等关键技能并作为毕设答辩与功能扩展的参考方案。1. 电影数据可视化分析系统从一份毕业设计压缩包说起很多计算机专业的同学拿到「基于 Python 的电影数据可视化分析系统」这个题目时第一反应是去搜「免费 Python 源码大全」想直接下载一份跑通交差。但真正做过答辩的人都知道导师第一个问题往往不是「你用了什么框架」而是「你的数据从哪来、清洗规则是什么、为什么这个维度用柱状图而不是折线图」。这个题目本质上是把「Python 爬虫 数据清洗 可视化 Web 展示」四条链路串成一个闭环考察的是你能不能独立走完一次小型数据分析项目。它适合正在做计算机毕业设计、想选一个数据量可控又容易出图的方向的同学也适合刚学完 Python 基础语法、想找一个完整项目练手的人。下面我按自己带过几届毕设的经验把这条链路拆开讲清楚包括数据怎么拿、图表怎么选、界面怎么搭以及那些答辩前夜才发现的坑。2. 数据从哪来爬虫抓取与字段设计2.1 为什么选豆瓣 Top250 而不是实时票房接口做电影数据可视化第一步永远是数据源。常见做法有三类公开数据集如 MovieLens、开放 API如 TMDB、网页抓取。MovieLens 数据干净但字段偏评分行为缺少「导演、演员、国家、时长」这类适合做可视化展示的维度TMDB 的 API 需要申请 key答辩演示时网络一断就尴尬。我一般推荐用豆瓣 Top250 作为主数据源原因是页面结构稳定、字段齐全片名、评分、评价人数、导演、主演、年份、国家、类型、时长而且 250 条数据量刚好——太少图表撑不起来太多爬取时间长且容易被限速。需要提前说明的是抓取任何网站都要遵守其 robots 协议控制请求频率只用于个人学习研究不要做二次分发或商用。这是做毕设时应该有的基本意识答辩时老师也可能问到。2.2 爬虫代码requests BeautifulSoup 的最小实现import requests from bs4 import BeautifulSoup import csv import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def parse_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) items soup.select(ol.grid_view li) rows [] for it in items: title it.select_one(span.title).get_text(stripTrue) rating it.select_one(span.rating_num).get_text(stripTrue) # 评价人数在 div classstar 的最后一个 span 里 star_div it.select_one(div.star) votes star_div.select(span)[-1].get_text(stripTrue) info it.select_one(div.bd p).get_text(stripTrue) rows.append({ title: title, rating: float(rating), votes: int(votes.replace(人评价, ).replace(,, )), info: info }) return rows def crawl_top250(): all_rows [] for start in range(0, 250, 25): url fhttps://movie.douban.com/top250?start{start} all_rows.extend(parse_page(url)) time.sleep(random.uniform(1.5, 3.0)) # 随机延时降低被封风险 return all_rows if __name__ __main__: data crawl_top250() with open(movies_raw.csv, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, rating, votes, info]) writer.writeheader() writer.writerows(data) print(f共抓取 {len(data)} 条)这段代码的逻辑很直白parse_page负责解析单页crawl_top250按start参数翻页每次请求之间随机 sleep 1.5 到 3 秒。几个关键参数需要说明timeout10防止某个请求卡死拖垮整个脚本resp.encoding utf-8是因为 requests 自动识别的编码有时会乱码encodingutf-8-sig写 CSV 是为了 Excel 打开不乱码这个细节答辩演示时很加分。info字段是一整段文本包含导演、主演、年份、国家、类型后面清洗时要拆开。2.3 字段拆解把一坨文本变成结构化列原始info字段长这样导演: 弗兰克·德拉邦特 主演: 蒂姆·罗宾斯 / 摩根·弗里曼... 1994 / 美国 / 犯罪 剧情。直接拿去做可视化没法用必须拆成year、country、genre、director四列。import pandas as pd import re df pd.read_csv(movies_raw.csv) def extract_year(info): m re.search(r(\d{4}), info) return int(m.group(1)) if m else None def extract_country(info): # 年份后面紧跟的「/ 国家」段 parts info.split(/) if len(parts) 2: return parts[-2].strip() return 未知 def extract_genre(info): parts info.split(/) return parts[-1].strip() if parts else 未知 df[year] df[info].apply(extract_year) df[country] df[info].apply(extract_country) df[genre] df[info].apply(extract_genre) df df.dropna(subset[year]) df[year] df[year].astype(int) df.to_csv(movies_clean.csv, indexFalse, encodingutf-8-sig) print(df[[title, year, country, genre]].head())这里有个血泪经验split(/)之后取[-2]和[-1]是因为不同电影的 info 段数不一样有的国家是「美国 英国」两个词有的类型是「剧情 爱情 犯罪」三个词。用固定索引会翻车所以取倒数两段相对稳。清洗完一定要dropna并打印前几行肉眼检查我见过太多人直接跳到画图结果年份列里混进了「1994年」这种带单位的字符串后面astype(int)直接报错。3. 可视化怎么做图表选型与参数调优3.1 四类必做图表及其选型理由毕设的可视化部分不是图越多越好而是要能讲出「为什么用这张图」。我一般建议做四类评分分布用直方图看整体分布形态、年份产量用折线图看时间趋势、国家占比用饼图或环形图看构成、类型与评分的交叉用热力图或箱线图看关系。这四张图覆盖了分布、趋势、构成、关系四种分析视角答辩时逻辑完整。用 Pyecharts 而不是 Matplotlib 的原因是前者生成交互式 HTML能直接嵌到 Web 页面里鼠标悬停显示数值演示效果好很多。Matplotlib 适合做静态图插入论文两者可以配合用。3.2 Pyecharts 评分分布直方图from pyecharts.charts import Bar from pyecharts import options as opts import pandas as pd df pd.read_csv(movies_clean.csv) # 按 0.5 分一档统计 bins [8.0, 8.5, 9.0, 9.5, 10.0] labels [8.0-8.5, 8.5-9.0, 9.0-9.5, 9.5-10.0] df[rating_bin] pd.cut(df[rating], binsbins, labelslabels, rightFalse) counts df[rating_bin].value_counts().reindex(labels).fillna(0).astype(int) bar ( Bar() .add_xaxis(labels) .add_yaxis(电影数量, counts.tolist(), category_gap50%) .set_global_opts( title_optsopts.TitleOpts(titleTop250 评分分布), xaxis_optsopts.AxisOpts(name评分区间), yaxis_optsopts.AxisOpts(name数量), toolbox_optsopts.ToolboxOpts(is_showTrue), ) ) bar.render(rating_dist.html)pd.cut的rightFalse表示左闭右开区间避免 8.5 分同时落进两个桶。category_gap50%控制柱子宽度默认太宽不好看。toolbox_opts打开右上角工具栏可以下载图片答辩截图就靠它。这里要注意reindex(labels)这一步如果某个区间没有电影value_counts不会生成该行直接画图会少一根柱子reindex 补 0 是必须的。3.3 年份趋势折线图与类型热力图from pyecharts.charts import Line, HeatMap from pyecharts import options as opts # 年份趋势 year_counts df.groupby(year).size().reset_index(namecount) year_counts year_counts[year_counts[year] 1980] # 过滤早期稀疏数据 line ( Line() .add_xaxis(year_counts[year].astype(str).tolist()) .add_yaxis(产量, year_counts[count].tolist(), is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title各年份电影产量), xaxis_optsopts.AxisOpts(name年份), yaxis_optsopts.AxisOpts(name数量), ) ) line.render(year_trend.html) # 类型-评分热力图先拆分多类型 genre_rows [] for _, row in df.iterrows(): for g in str(row[genre]).split(): genre_rows.append({genre: g, rating: row[rating]}) genre_df pd.DataFrame(genre_rows) pivot genre_df.groupby(genre)[rating].mean().round(2).sort_values(ascendingFalse) print(pivot.head(10))热力图如果类型太多会挤成一团我一般只取出现次数前 8 的类型做交叉。is_smoothTrue让折线平滑但要注意平滑会轻微扭曲数据论文里如果强调精确趋势可以关掉。年份过滤到 1980 之后是因为 Top250 里早期电影极少折线前段会贴着 0 走视觉上很难看。4. Web 界面怎么搭Flask 把图表串起来4.1 为什么用 Flask 而不是 Django毕业设计的时间通常只有几周Django 的 ORM、中间件、admin 对这个小项目来说是过度设计。Flask 一个文件就能起服务配合 Jinja2 模板渲染 HTML把 Pyecharts 生成的图表用iframe或直接嵌入的方式展示半天就能搭出可演示的界面。常见做法是app.py定义路由templates/放页面模板static/放生成的 HTML 图表文件。4.2 Flask 主程序与路由设计from flask import Flask, render_template import pandas as pd from pyecharts.charts import Bar, Line, Pie from pyecharts import options as opts app Flask(__name__) def get_df(): return pd.read_csv(movies_clean.csv) app.route(/) def index(): df get_df() total len(df) avg_rating round(df[rating].mean(), 2) top_movie df.loc[df[rating].idxmax(), title] return render_template(index.html, totaltotal, avg_ratingavg_rating, top_movietop_movie) app.route(/chart/rating) def chart_rating(): df get_df() bins [8.0, 8.5, 9.0, 9.5, 10.0] labels [8.0-8.5, 8.5-9.0, 9.0-9.5, 9.5-10.0] df[rating_bin] pd.cut(df[rating], binsbins, labelslabels, rightFalse) counts df[rating_bin].value_counts().reindex(labels).fillna(0).astype(int) bar ( Bar() .add_xaxis(labels) .add_yaxis(电影数量, counts.tolist()) .set_global_opts(title_optsopts.TitleOpts(title评分分布)) ) return bar.dump_options_with_quotes() if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)dump_options_with_quotes()返回的是图表的 JSON 配置前端用 ECharts 的setOption接收即可这样图表数据是动态从 CSV 读的改了数据刷新页面就更新不用重新生成 HTML 文件。debugTrue只在开发时开正式演示前记得关掉否则出错会暴露源码路径。host0.0.0.0是为了让同局域网的其他设备也能访问答辩时如果老师想用手机看这个设置有用。4.3 前端模板与图表渲染!DOCTYPE html html langzh head meta charsetUTF-8 title电影数据可视化分析/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body h1豆瓣 Top250 电影数据分析/h1 p共 {{ total }} 部平均评分 {{ avg_rating }}最高分{{ top_movie }}/p div idratingChart stylewidth:800px;height:400px;/div script fetch(/chart/rating) .then(res res.json()) .then(option { const chart echarts.init(document.getElementById(ratingChart)); chart.setOption(JSON.parse(option)); }); /script /body /html这里有个容易踩的坑dump_options_with_quotes()返回的是字符串前端要JSON.parse一次如果用的是dump_options()则返回对象不需要 parse。两者混用会导致图表空白控制台报Unexpected token。另外 ECharts 的 CDN 地址建议下载到本地static/目录答辩现场网络不稳定时 CDN 加载失败整个页面就白屏了这个后悔药我吃过一次。5. 避坑与排查答辩前最容易翻车的五件事5.1 爬虫被封或返回空列表现象脚本跑了几页之后items为空或者直接抛ConnectionError。原因请求频率过高触发限速或者 User-Agent 被识别。解决把time.sleep调到 2 秒以上并加随机抖动准备多个 User-Agent 轮换抓取前先手动访问一次确认页面结构没变。如果只是做毕设250 条数据分 10 页慢一点也就半分钟的事不要图快。5.2 中文乱码从 CSV 一路传到网页现象CSV 用 Excel 打开正常但 pandas 读进来列名变成\ufefftitle或者网页上显示问号。原因utf-8-sig写入时带了 BOM 头pandas 默认utf-8读取会把 BOM 当成列名的一部分。解决读取时显式指定pd.read_csv(movies_clean.csv, encodingutf-8-sig)或者写入时用纯utf-8并在 Excel 里用「数据-从文本导入」的方式打开。统一编码是省事的关键。5.3 Pyecharts 图表在网页里不显示现象单独打开生成的 HTML 正常嵌到 Flask 页面就空白。原因Pyecharts 默认生成的 HTML 自带一份 ECharts JS 引用和模板里的 CDN 引用冲突或者dump_options_with_quotes的返回值没 parse。解决用dump_options_with_quotes走 JSON 接口的方式前端只保留一份 ECharts 引用版本统一到 5.x。检查浏览器控制台如果有echarts is not defined就是 JS 没加载成功。5.4 年份字段混入非数字导致 astype 报错现象ValueError: invalid literal for int() with base 10: 1994年。原因正则(\d{4})在某些 info 文本里匹配到了别的四位数比如评价人数或片长。解决把正则收紧为r(\d{4})\s*/利用年份后面紧跟斜杠的特征定位或者在astype(int)前加pd.to_numeric(errorscoerce)把异常值转成 NaN 再 drop。清洗阶段多打印几行样本比事后 debug 省时间。5.5 答辩演示时端口被占用或服务起不来现象OSError: [Errno 98] Address already in use。原因上次debugTrue的进程没退干净或者 5000 端口被其他程序占了。解决换端口port5001或者用lsof -i:5000Linux/Mac和netstat -ano | findstr 5000Windows找到进程杀掉。演示前重启一次电脑是最稳妥的别问我怎么知道的。6. 进阶技巧让系统从「能跑」变成「能讲」走到这里系统已经能跑通了但毕设评分拉开差距的往往不是功能多少而是你有没有自己的分析结论。我一般会加一个「评分与评价人数的关系」散点图用votes做 x 轴、rating做 y 轴看看是不是评价人数越多评分越趋于稳定。这个图能引出一个有意思的讨论高评分电影是「小众高分」还是「大众认可」答辩时老师很吃这一套。from pyecharts.charts import Scatter from pyecharts import options as opts df pd.read_csv(movies_clean.csv, encodingutf-8-sig) scatter ( Scatter() .add_xaxis(df[votes].tolist()) .add_yaxis(评分, df[rating].tolist(), symbol_size8, label_optsopts.LabelOpts(is_showFalse)) .set_global_opts( title_optsopts.TitleOpts(title评价人数与评分关系), xaxis_optsopts.AxisOpts(name评价人数, type_log), yaxis_optsopts.AxisOpts(name评分, min_8.0), tooltip_optsopts.TooltipOpts(formatter{a}: {c}) ) ) scatter.render(votes_rating.html)type_log把 x 轴设成对数轴因为评价人数从几万到几百万跨度太大线性轴会把大部分点挤在左边。min_8.0让 y 轴从 8 分开始否则 8 到 10 分的差异被压扁看不出规律。这两个参数是散点图能不能看出门道的关键默认参数画出来就是一团糊。另一个加分项是加一个简单的搜索框输入片名返回该片的详细信息。用 Flask 加一个/search?qxxx路由pandas 做字符串包含匹配即可不需要上数据库。演示时现场搜一部老师熟悉的电影比干讲图表有说服力。最后说个我自己的习惯所有清洗和统计逻辑都写成独立函数放在utils.py里app.py只负责路由和渲染。这样改需求时不用在几百行代码里翻也方便在论文里贴代码片段。毕设这东西功能做完只是及格线能把自己的分析思路讲清楚、把踩过的坑说明白才是真正拿得出手的东西。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站