基于Python的豆瓣电影Top250数据分析可视化系统实战

基于Python的豆瓣电影Top250数据分析可视化系统实战 简介一份基于Python的豆瓣电影数据分析与可视化系统完整项目包适用于计算机专业毕业设计、课程设计及综合实训。系统采用Django框架与MySQL 5.7数据库搭建前端基于Vue实现包含后台管理端与前台展示端覆盖数据管理、统计分析、图表展示等常见功能模块有助于理解前后端分离开发与数据分析流程。压缩包共512个文件以Vue组件、Python源码、JS逻辑脚本、SVG图标和JPG/PNG图片为主同时包含SQL数据库脚本、环境配置脚本、安装运行批处理文件以及论文文档整体大小48.3MB。包内管理员账号、后台及前台访问路径均已注明并附带安装与初始化数据库脚本可降低部署门槛便于快速进入二次开发。当前已有254人学习下载适合需要快速搭建完整Web数据分析项目并进行论文撰写的读者参考。1. 豆瓣电影 Top250 这类榜单为什么是数据分析可视化练手的第一选择数据量不大不小、字段干净但又不完全干净、背后还有明确的业务含义再加上爬虫、清洗、聚合、可视化一个不少豆瓣电影 Top250 几乎成了 Python 数据分析项目里最经典的“全栈”素材。这个标题说的“基于 Python 的豆瓣电影数据分析可视化系统”本质上是把一条从网页到图表的完整数据链路打通先用爬虫抓取豆瓣电影 Top250 的片名、评分、评价人数、导演、年份、类型等字段再基于 pandas 做清洗与聚合最后用 pyecharts 或 Flask 搭出可视化页面。适合的人群很明确刚学完 Python 语法、准备做第一个数据分析项目的人以及想把自己简历上“熟悉数据分析”这个描述换成实际作品的在校生和转行者。它不涉及复杂的大数据框架也不需要分布式环境一台普通笔记本就能跑完整条链路。这个项目的价值不在爬虫本身而在于它逼着你处理真实数据里才有的脏值、缺失和类型问题。2. 先框定方案用 requests 抓取豆瓣电影 Top250还是直接找现成数据集很多刚接触这个项目的人会纠结一件事豆瓣有反爬Top250 的 HTML 结构也经常微调到底应该自己写爬虫还是直接下载一份现成的 CSV。我的建议是先自己写爬虫因为标题里就带“基于 Python”爬虫是这个系统第一个绕不开的环节。更重要的是通过抓取你能理解数据是怎么产生的这对接下来的清洗和可视化帮助很大。2.1 豆瓣 Top250 的分页规律与请求头设置豆瓣 Top250 的列表页从https://movie.douban.com/top250?start0开始每页显示 25 条start参数以 25 为步长递增一共 10 页。这意味着你只需要循环请求 10 次就能覆盖完整的 250 条数据。第一次写爬虫的人最容易忽略的是请求头里的User-Agent豆瓣对没有 UA 的请求会直接返回 418。除了 UA建议把Accept-Language设为zh-CN确保拿到的是中文页面虽然对解析影响不大但调试的时候看响应内容会更直观。import requests import time headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept-Language: zh-CN,zh;q0.9 } def fetch_page(start): url fhttps://movie.douban.com/top250?start{start} resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding utf-8 return resp.text这段代码里有几个参数值得说清楚。timeout10是必须的不设超时的话遇到网络抖动进程会一直挂起。resp.encoding utf-8是很多人的坑豆瓣页面本身是 UTF-8 编码但 requests 有时会根据响应头猜成 ISO-8859-1导致中文乱码所以这里强制指定。raise_for_status()的作用是当状态码不是 200 时主动抛出异常方便你快速定位问题而不是拿着一个 418 页面往下解析。2.2 用 CSS 选择器解析标题、评分、评价人数和导演信息拿到 HTML 之后解析方案有两种主流选择BeautifulSoup 加 lxml 解析器或者直接用 lxml 的 xpath。两者都能完成工作我倾向于 BeautifulSoup因为它的 API 对新手更友好.select()方法支持 CSS 选择器写起来直观。豆瓣 Top250 的列表项结构是稳定的每个div classitem里有一个span classtitle放中文片名span classrating_num放评分p class里是导演、主演和年份信息。注意不要用.find()这种只返回第一个匹配结果的方法去抓列表页正确做法是先定位到所有item节点再在每个节点内做二次查找。from bs4 import BeautifulSoup import csv def parse_page(html): soup BeautifulSoup(html, lxml) items soup.select(div.item) results [] for item in items: title item.select_one(span.title).text rating item.select_one(span.rating_num).text quote item.select_one(span.inq) quote_text quote.text if quote else 无 # 第二行信息形如 1994 / 美国 / 犯罪 剧情 info item.select_one(p).text.strip().split(\n)[1].strip() year info.split(/)[0].strip() results.append([title, rating, quote_text, year]) return resultsselect_one返回的是单个节点select返回的是列表这个区别要记清楚。span.inq是电影的一句话短评这个字段存在缺失有些电影没有话短评所以必须做if quote else的兜底处理这就是真实数据和教科书数据的差异之一。info.split(/)拿到的数组里第一个元素是年份第二个是制片国家第三个是类型这个结构在清洗阶段还要继续用。2.3 抓取频率与断点续爬的工程化处理爬虫写出来能跑只是第一步要把它当成一个“系统”来对待就需要考虑频率控制和失败恢复。豆瓣对高频请求的处理策略是封 IP所以每页之间加time.sleep(random.uniform(2, 4))是基本操作。但就算加了延时网络抖动或者触发反爬仍然可能导致某一页抓取失败。常见的做法是把结果先追加写入 CSV而不是全部抓完再一次性写入这样就算脚本中途崩溃已经抓到的页不会丢。另一个实用技巧是打印带页码的进度日志方便你知道挂在哪一页。import random with open(douban_top250.csv, a, newline, encodingutf-8-sig) as f: writer csv.writer(f) if f.tell() 0: writer.writerow([title, rating, quote, year]) for start in range(0, 250, 25): try: html fetch_page(start) rows parse_page(html) writer.writerows(rows) print(f完成第 {start // 25 1} 页, 累计 {len(rows)} 条) except Exception as e: print(f第 {start // 25 1} 页失败: {e}) time.sleep(random.uniform(2, 4))这里用encodingutf-8-sig而不是utf-8原因是后者不带 BOM生成的 CSV 用 Excel 打开时中文会乱码加-sig后缀会在文件头部写入 BOM 标记Excel 就能正确识别。newline是为了避免 CSV 写入时出现多余的空行这是 Windows 平台上的经典坑。3. pandas 清洗与聚合评分分布、年份趋势和类型拆分的完整流程数据落盘之后真正的分析才算开始。抓下来的 CSV 里至少有四个问题需要处理年份字段里混着空格和/分隔符、评分是字符串类型不能直接做数值计算、没有类型字段的独立列、部分电影没有短评。这一章的目标就是把这堆原始记录变成一张可以直接画图的结构化表格。3.1 从年份字符串到连续特征的转换用pd.read_csv读入后第一步永远是df.info()和df.head()。df.info()会告诉你每一列的非空计数如果quote列的非空数量明显小于 250说明有缺失值这是预期行为不用惊慌。年份列需要做一次字符串清理把1994前的空格和后面的制片国家信息剥掉。这里用str.extract配合正则表达式一步到位比手动 split 更稳健。import pandas as pd df pd.read_csv(douban_top250.csv) df[rating] pd.to_numeric(df[rating], errorscoerce) df[year] df[year].str.extract(r(\d{4})).astype(int) df[quote] df[quote].fillna(无)pd.to_numeric的errorscoerce参数会在转换失败时把该值设为NaN而不是直接抛异常这是一个很重要的防御性写法。str.extract用正则(\d{4})匹配四个连续数字这样即使年份字段里混入了别的字符也能正确提取。astype(int)的前提是上一步提取结果中没有NaN所以最好在此之前先确认一下df[year].isna().sum()如果大于 0 就需要单独处理。3.2 类型字段的多值拆分与 one-hot 编码豆瓣电影的类型是“剧情 / 爱情 / 犯罪”这种用斜杠分隔的多值字段直接放进 DataFrame 里只能算一个字符串列没法做统计。常见的做法是用str.split(/)拆开然后explode展开成多行这样每一行只保留一种类型。但要注意这样会导致同一部电影出现多次后续聚合时如果直接groupby数量会重复计数所以更稳妥的做法是用str.get_dummies生成独热编码再按原索引聚合回去。type_dummies df[type].str.get_dummies(sep/) df pd.concat([df, type_dummies], axis1) type_columns type_dummies.columns.tolist() print(type_columns)str.get_dummies(sep/)会自动按分隔符拆分并生成 0/1 编码列比手动 split 再循环赋值简洁得多。拆完之后每个类型列都是 0 或 1可以理解为一个布尔标记。后面的分析里统计类型出现次数直接对type_columns求和即可计算某类型电影的均分则可以用df[df[剧情] 1][rating].mean()这样的条件筛选。3.3 评分类别画像高分电影都有什么共同点数据整理干净之后第一个值得做的分析是“不同评分区间电影的类型构成”。把评分按 9 分以上、8 到 9 分、8 分以下分成三组再统计每一组的类型分布能直观看出什么样类型的电影更容易拿到高分。这一步用pd.cut做分箱然后用groupby聚合。bins [0, 8, 9, 10] labels [8分以下, 8到9分, 9分以上] df[rating_group] pd.cut(df[rating], binsbins, labelslabels, rightFalse) type_summary df.groupby(rating_group)[type_columns].mean() print(type_summary.round(2))pd.cut的三个参数值得解释bins是分箱边界rightFalse表示区间左闭右开也就是 8 分计入“8到9分”9 分计入“9分以上”。groupby之后取mean()而不是sum()是因为每一列都是 0/1 编码均值恰好代表该评分区间内包含此类型电影的比例。这个结果拿到之后你会看到一些有意思的现象比如 9 分以上区间里“剧情”和“犯罪”的比例显著高于整体均值这就可以作为后续可视化里讲解的重点。4. 基于 pyecharts 做豆瓣电影可视化系统从单图到组合面板数据算完了最后一步是把分析结果变成能看的东西。pyecharts 是 Python 生态里做前端可视化最顺手的库它生成的是基于 ECharts 的 HTML 文件不需要自己写 JavaScript而且图表支持交互。做可视化系统的思路不是堆图表而是先定问题你想让看的人从这些图表里得到什么信息我的答案是三个维度口碑分布、时间趋势、类型偏好。4.1 评分直方图与累计百分比双轴图评分分布的可视化首选直方图但只用柱状图看不出累积趋势所以叠加一条折线展示累计百分比。pyecharts 的Bar和Line可以组合进同一个Grid通过extend_axis共享 X 轴。from pyecharts import options as opts from pyecharts.charts import Bar, Line rating_counts df[rating].value_counts().sort_index() cum_ratio (rating_counts.cumsum() / rating_counts.sum() * 100).round(1) bar ( Bar() .add_xaxis(rating_counts.index.tolist()) .add_yaxis(电影数量, rating_counts.values.tolist()) .extend_axis(yaxisopts.AxisOpts(name累计占比, type_value, max_100)) .set_series_opts(label_optsopts.LabelOpts(is_showFalse)) ) line ( Line() .add_xaxis(rating_counts.index.tolist()) .add_yaxis(累计占比, cum_ratio.tolist(), yaxis_index1) ) grid Grid().add(bar, grid_optsopts.GridOpts(pos_left15%)).add(line, grid_optsopts.GridOpts(pos_left15%)) grid.render(rating_distribution.html)extend_axis的作用是创建第二个 Y 轴配合yaxis_index1让折线使用这个新轴。max_100把第二个轴固定到 100否则折线看起来会非常平。GridOpts的pos_left要留出空间给双轴刻度不然左侧轴标签会被截断。4.2 用 Timeline 控件做年代维度切换电影年份跨度从 1921 年到 2020 年之后如果把所有年份的评分均值画在一张图上数据点会很密而且早期年份电影数量少均分波动大直接展示容易误导。更好的方案是使用Timeline组件让用户手动切换不同年代的图表。这是 pyecharts 相对 Excel 图表的一个关键优势。from pyecharts.charts import Timeline def make_year_chart(year): year_df df[df[year] year] top5 year_df.nlargest(5, rating) bar ( Bar() .add_xaxis(top5[title].tolist()) .add_yaxis(评分, top5[rating].round(1).tolist()) .set_global_opts(title_optsopts.TitleOpts(titlef{year}年TOP5)) ) return bar timeline Timeline() for year in range(2000, 2024): if len(df[df[year] year]) 0: timeline.add(make_year_chart(year), str(year)) timeline.add_schema(play_interval2000, is_auto_playTrue) timeline.render(year_timeline.html)Timeline的核心用法是一个一个add每个时间点上挂一个独立图表实例。play_interval2000是自动播放间隔两秒is_auto_playTrue让页面打开后自动轮播。这里过滤了电影数量为 0 的年份避免出现空白页。这个组件做出来的效果很适合放在博客里当演示读的人可以自己拖动时间轴看变化。4.3 词云图展示高频短评词需要用 jieba 先分词豆瓣电影的一句话短评是很好的文本数据但原始句子不能直接画词云需要先分词。jieba 是 Python 最常用的中文分词库配合 wordcloud 库可以快速生成词云。这一步需要安装两个依赖jieba和wordcloud用pip install jieba wordcloud即可。import jieba from wordcloud import WordCloud all_quotes .join(df[quote].tolist()) words .join(jieba.cut(all_quotes)) wc WordCloud( font_pathC:/Windows/Fonts/simhei.ttf, width800, height600, background_colorwhite ).generate(words) wc.to_file(quote_wordcloud.png)jieba.cut返回一个生成器 .join把它拼成空格分隔的字符串这是 wordcloud 库要求的输入格式。font_path必须指向一个中文字体文件否则生成出来的图片全是方块Windows 下一般用simhei.ttf黑体Linux 下可以指定/usr/share/fonts/下的中文字体。词云图不追求精确它的用途是让看的人一眼感知到电影短评里最常被提到的关键词比如“经典”“人生”“爱情”。5. 验证你可视化系统的数据一致性用 10 分钟跑一次全链路自查项目做完之后比写代码更重要的是有一份自查清单。这个项目的常见翻车点集中在三个位置爬虫阶段被反爬导致数据缺失、CSV 编码问题导致 pandas 读入乱码、pyecharts 图表中文不显示。针对这几个问题有一套固定的验证方法。先验证数据完整性。250 条记录一条不能少评分范围应该在 8.0 到 9.8 之间年份范围在 1921 到 2023 年之间。超过这个范围的数据一定是清洗逻辑出了问题。用一行代码就能查完df[rating].describe()如果count小于 250说明爬虫阶段有页面没抓到或者解析时丢了解析失败的电影。此时不要急着重跑爬虫先检查 CSV 文件里是不是存在空行或缺失行很多时候是writer.writerows写入时某个列表长度不一致导致的。再验证类型统计口径。前面用str.get_dummies拆分类型之后所有类型列的数值应该只有 0 和 1不会有其他值。用df[type_columns].sum().sort_values(ascendingFalse)能看到出现次数最多的类型正常情况下“剧情”和“喜剧”应该排在前两名如果第一名变成了“无”说明清洗阶段某个字段没有正确拆分。最后验证图表数据同步你在图里看到的顶部数据和df.nlargest(5, rating)的输出必须一致pyecharts 在渲染时是读到什么画什么如果图表和 DataFrame 不一致几乎可以肯定是你在传给图表的列表里做了额外的筛选或排序。最终的发布形态有两种选择。如果你想做一个可交互的网页把多个render()生成的 HTML 文件用 Flask 的render_template包起来配一个简单的首页导航即可如果你只是要一份静态分析报告把所有 HTML 和 PNG 图片放进一个目录用浏览器逐个打开就足够完成你的数据分析可视化系统交付。本文还有配套的精品资源点击获取