豆瓣Top250数据抓取与可视化:Python大作业完整方案
简介:这是一套面向高校学生与Python初学者的数据可视化课程设计资料,围绕豆瓣电影Top250数据集展开完整分析流程,适用于毕业设计、期末大作业与课程设计等高分场景。资源包共221个文件,约14.05MB,包含27个Python源码文件、32张可视化图表、16个XML配置、5组Shapefile地理数据(shp/dbf/shx/prj)以及ipynb笔记本、PDF文档、PPT与说明文本,覆盖数据采集、清洗、分析与图表呈现各环节。已有566人学习下载,说明其参考价值获得一定认可。源码配有详细注释,新手也能读懂,并附带文档与PPT,可直接用于答辩展示;目录中同时保留Scrapy爬虫配置与地图数据,便于扩展地理维度分析。整体部署简单,下载后即可运行,能帮助读者快速掌握Pandas、Matplotlib等工具在真实数据集上的应用,并形成一份结构完整、导师认可的高分项目方案。
1. 豆瓣Top250数据抓下来之后:一份能交作业也能讲清楚的Python可视化方案
很多人做 Python 数据可视化分析大作业,第一步就卡在数据上——豆瓣电影 Top250 页面结构稳定、字段规整,是练手数据分析与可视化的理想素材。但真正动手你会发现,把数据抓下来只是开始,后面清洗、建模、出图、写文档、做 PPT,每一步都有坑。这篇笔记面向正在做数据分析项目实践的学生和转行者,把豆瓣电影 Top250 数据分析与可视化的完整落地路径拆开讲:从数据采集、Pandas 清洗、Matplotlib 与 Pyecharts 出图,到最终怎么把源码、文档、PPT 串成一份能讲清楚的作品。读完你能照着复现一套完整流程,也能避开我踩过的那些坑。
2. 数据采集与清洗:从页面到 DataFrame 的四个关键步骤
2.1 为什么选 requests + BeautifulSoup 而不是 Selenium
豆瓣 Top250 的页面是服务端渲染的静态 HTML,每页 25 条,共 10 页。这种结构用 requests 加 BeautifulSoup 就够了,没必要上 Selenium。Selenium 启动浏览器开销大,在数据量只有 250 条的场景下属于杀鸡用牛刀。常见做法是用 requests 循环请求 10 个页面,每页解析出排名、片名、导演、主演、年份、国家、类型、评分、评价人数、短评这些字段。
请求头里必须带 User-Agent,否则豆瓣会返回 418。另外请求间隔建议设 1 到 2 秒,250 条数据分 10 次请求,加延时也就多花十几秒,但能避免被限流。我一般会在循环里用 time.sleep 加一个随机延时,比固定值更自然。
import requests from bs4 import BeautifulSoup import pandas as pd 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.0.0 Safari/537.36" } movies = [] for page in range(10): url = f"https://movie.douban.com/top250?start={page * 25}" resp = requests.get(url, headers=headers, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") items = soup.find_all("div", class_="item") for item in items: rank = item.find("em").text.strip() title = item.find("span", class_="title").text.strip() rating = item.find("span", class_="rating_num").text.strip() # 评价人数在 rating_num 后面的 span 里 people = item.find("div", class_="star").find_all("span")[-1].text.strip() info = item.find("div", class_="bd").p.text.strip() movies.append({ "rank": rank, "title": title, "rating": rating, "people": people, "info": info }) time.sleep(random.uniform(1, 2)) df = pd.DataFrame(movies) print(df.shape) print(df.head())这段代码的逻辑很直接:循环 10 个页面,每页用 BeautifulSoup 解析出电影条目,把需要的字段塞进列表,最后转成 DataFrame。参数上注意 timeout 设 10 秒,避免某个请求卡死整个流程;random.uniform(1, 2) 让每次请求间隔在 1 到 2 秒之间浮动。跑完之后 df.shape 应该是 (250, 5),如果不是,说明某页解析出了问题,需要检查页面结构是否变化。
2.2 info 字段拆解:年份、国家、类型怎么分列
上面抓下来的 info 字段是一长串文本,格式类似“1994 / 美国 / 犯罪 剧情”。这串文本里包含了年份、国家和类型,需要拆成独立列才能做后续分析。常见做法是用 split 按斜杠切分,但要注意有些条目国家不止一个,类型也可能有多个。
def parse_info(info): parts = [p.strip() for p in info.split("/")] year = parts[0] if len(parts) > 0 else "" country = parts[1] if len(parts) > 1 else "" genre = parts[2] if len(parts) > 2 else "" return year, country, genre df[["year", "country", "genre"]] = df["info"].apply( lambda x: pd.Series(parse_info(x)) ) # 评价人数去掉“人评价”并转成整数 df["people"] = df["people"].str.replace("人评价", "").astype(int) df["rating"] = df["rating"].astype(float) df["year"] = pd.to_numeric(df["year"], errors="coerce") print(df[["title", "year", "country", "genre", "rating", "people"]].head())这里用 apply 加 pd.Series 的方式把一列拆成三列,比逐行循环更简洁。参数上 errors="coerce" 让无法转换的年份变成 NaN,方便后续过滤。评价人数去掉“人评价”后转 int,评分转 float。跑完这一步,数据就规整了,可以开始做分析。
2.3 缺失值与重复值的处理边界
豆瓣 Top250 的数据质量整体不错,但仍有几个边界情况需要注意。第一,有些电影年份跨多个年份,比如“1994(中国大陆) / 1994(香港)”,直接 pd.to_numeric 会变成 NaN,需要先用正则提取第一个四位数字。第二,国家字段可能包含多个国家,用斜杠分隔,做国家维度分析时要么取第一个,要么拆成多行。第三,类型字段同理,一部电影可能同时属于剧情和爱情,做类型统计时需要 explode 拆开。
import re def extract_year(text): match = re.search(r"\d{4}", str(text)) return int(match.group()) if match else None df["year"] = df["year"].apply(extract_year) df = df.dropna(subset=["year"]) df = df.drop_duplicates(subset=["title"]) print(f"清洗后数据量: {len(df)}")这段代码用正则从年份字段里提取第一个四位数字,处理跨年份的情况。dropna 去掉年份缺失的行,drop_duplicates 按片名去重。清洗后数据量可能略少于 250,这是正常的,不用强行凑数。
3. 用 Pandas 做维度分析:评分分布、年份趋势、国家与类型
3.1 评分分布与评价人数的关系
评分和评价人数是两个最核心的数值字段。先看评分分布,用 describe 和 value_counts 快速了解数据的集中趋势。Top250 的评分集中在 8.0 到 9.7 之间,大部分在 8.5 以上。评价人数则跨度很大,从几万到几百万都有。
print(df["rating"].describe()) print(df["rating"].value_counts().sort_index()) # 评分与评价人数的相关性 corr = df["rating"].corr(df["people"]) print(f"评分与评价人数相关系数: {corr:.3f}")describe 给出均值、标准差、四分位数,value_counts 看每个评分值有多少部电影。相关系数用 corr 计算,如果结果接近 0,说明评分高低和评价人数多少没有明显线性关系——这个结论本身就可以写进分析报告,比强行找规律更有说服力。
3.2 年份趋势:哪个年代的高分电影最多
年份字段清洗后,可以按年代分组统计。常见做法是把年份映射到年代,比如 1994 年归到 1990s,然后统计每个年代的电影数量和平均评分。
df["decade"] = (df["year"] // 10) * 10 decade_stats = df.groupby("decade").agg( count=("title", "count"), avg_rating=("rating", "mean") ).reset_index() print(decade_stats)groupby 按年代分组,agg 同时算数量和平均评分。reset_index 把分组结果转回普通 DataFrame,方便后续画图。从结果能看出 1990s 和 2000s 的高分电影数量最多,这跟豆瓣用户群体的观影偏好有关,可以在文档里展开讨论。
3.3 国家与类型:拆开统计才看得清
国家和类型字段都可能是多值,直接 value_counts 会把“美国 / 英国”当成一个整体。正确做法是先按分隔符拆开,再 explode 成多行,最后统计。
# 国家拆开统计 df["country_list"] = df["country"].str.split("/") country_exploded = df.explode("country_list") country_exploded["country_list"] = country_exploded["country_list"].str.strip() country_counts = country_exploded["country_list"].value_counts().head(10) print(country_counts) # 类型拆开统计 df["genre_list"] = df["genre"].str.split(" ") genre_exploded = df.explode("genre_list") genre_counts = genre_exploded["genre_list"].value_counts().head(10) print(genre_counts)explode 把一行多值拆成多行,strip 去掉空格。国家统计取前 10,类型统计取前 10。从结果能看出美国和日本电影占比最高,剧情和爱情是最常见的类型。这些结论直接对应可视化图表,后面画图时按这个顺序排布。
4. 可视化落地:Matplotlib 静态图与 Pyecharts 交互图怎么选
4.1 Matplotlib 画评分分布直方图和年份趋势折线图
Matplotlib 适合做静态图,导出 PNG 放进文档和 PPT 里。评分分布用直方图,年份趋势用折线图,国家占比用条形图。中文字体需要设置,否则会显示方块。
import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams["font.sans-serif"] = ["SimHei"] matplotlib.rcParams["axes.unicode_minus"] = False fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 评分分布直方图 axes[0].hist(df["rating"], bins=15, color="#4C72B0", edgecolor="white") axes[0].set_title("豆瓣Top250评分分布") axes[0].set_xlabel("评分") axes[0].set_ylabel("电影数量") # 年份趋势折线图 year_counts = df.groupby("year").size() axes[1].plot(year_counts.index, year_counts.values, color="#DD8452", linewidth=2) axes[1].set_title("各年份入选电影数量") axes[1].set_xlabel("年份") axes[1].set_ylabel("数量") plt.tight_layout() plt.savefig("rating_year.png", dpi=150) plt.show()这段代码创建 1 行 2 列的子图,左边画评分直方图,右边画年份折线图。font.sans-serif 设成 SimHei 解决中文显示问题,axes.unicode_minus 设 False 解决负号显示问题。dpi=150 保证导出图片清晰度,放进 PPT 不会糊。tight_layout 自动调整子图间距,避免标签重叠。
4.2 Pyecharts 做交互式图表:词云和玫瑰图
Pyecharts 生成 HTML 文件,可以在浏览器里交互,适合放进大作业的演示环节。类型分布用玫瑰图,导演或演员用词云。Pyecharts 的配置项比较多,但核心就是准备好数据对,然后设置标题和尺寸。
from pyecharts.charts import Pie, WordCloud from pyecharts import options as opts # 类型分布玫瑰图 genre_data = genre_counts.reset_index() genre_data.columns = ["genre", "count"] pie = ( Pie(init_opts=opts.InitOpts(width="800px", height="500px")) .add( "", [list(z) for z in zip(genre_data["genre"], genre_data["count"])], radius=["30%", "70%"], rosetype="radius" ) .set_global_opts( title_opts=opts.TitleOpts(title="电影类型分布"), legend_opts=opts.LegendOpts(is_show=False) ) ) pie.render("genre_pie.html")Pie 的 rosetype 设成 radius 就是玫瑰图,radius 参数控制内外半径。add 方法里数据格式是 [(名称, 数值), ...],用 zip 加 list 转换。render 输出 HTML 文件,双击就能在浏览器打开。词云类似,把类型或导演名称和出现次数传进去即可。
4.3 图表选型对照:什么数据用什么图
| 分析目标 | 推荐图表 | 工具 | 理由 |
|---|---|---|---|
| 评分分布 | 直方图 | Matplotlib | 看集中趋势和偏态 |
| 年份趋势 | 折线图 | Matplotlib | 看时间变化 |
| 国家占比 | 条形图 | Matplotlib | 类别对比清晰 |
| 类型分布 | 玫瑰图 | Pyecharts | 交互展示占比 |
| 关键词 | 词云 | Pyecharts | 直观展示频率 |
选图的核心原则是:看分布用直方图,看趋势用折线图,看对比用条形图,看占比用饼图或玫瑰图,看频率用词云。不要为了炫技选不合适的图,比如用饼图展示年份趋势就是典型的翻车操作。
5. 避坑与排查:数据抓取和可视化里的五个血泪教训
5.1 请求返回 418 或空数据
现象:requests.get 返回状态码 418,或者返回的 HTML 里找不到 item 节点。原因:豆瓣检测到请求头缺失或请求频率过高。解决:检查 User-Agent 是否设置,请求间隔是否太短。我一般会在 headers 里加 Referer 和 Accept-Language,进一步模拟浏览器行为。如果还是不行,把延时调到 3 秒以上再试。
5.2 中文字体显示方块
现象:Matplotlib 图表里的中文全部变成方块。原因:默认字体不支持中文。解决:设置 font.sans-serif 为 SimHei 或 Microsoft YaHei,同时把 axes.unicode_minus 设为 False。如果是在 Linux 环境下跑,需要先确认系统里装了中文字体,没有的话用 font_manager 手动加载字体文件。
5.3 年份字段转换后大量 NaN
现象:pd.to_numeric 转换年份后出现大量 NaN。原因:年份字段里混有“1994(中国大陆)”这类带括号的文本。解决:先用正则提取四位数字,再转数值。正则用 r"\d{4}" 匹配第一个四位数字,match.group() 取出结果。如果一部电影有多个年份,取第一个即可,不影响整体趋势分析。
5.4 Pyecharts 图表在浏览器里空白
现象:render 生成的 HTML 打开后一片空白。原因:Pyecharts 依赖的 JS 资源加载失败,通常是网络问题。解决:把 Pyecharts 的 CDN 资源换成本地文件,或者用 pyecharts 的 snapshot 功能直接生成图片。另一个常见原因是数据格式不对,add 方法要求数据是列表,不是 DataFrame 列,需要先转换。
5.5 评价人数转 int 报错
现象:astype(int) 报 ValueError。原因:评价人数里除了“人评价”还可能有多余空格或换行符。解决:先用 str.strip() 去空格,再用 str.replace("人评价", "") 去掉后缀,最后 astype(int)。如果还有异常值,用 pd.to_numeric(errors="coerce") 兜底,把无法转换的变成 NaN 再过滤。
6. 从源码到 PPT:怎么把分析结果讲成一个完整故事
6.1 项目结构组织与文档撰写要点
一份能交出去的大作业,源码、文档、PPT 三样东西要能互相印证。源码目录建议按功能分文件夹:data 放原始数据和清洗后的数据,scripts 放爬虫和清洗脚本,charts 放可视化脚本,output 放生成的图表和 HTML 文件。文档用 Markdown 写,结构跟分析流程一致:数据来源、清洗步骤、分析维度、可视化结果、结论。PPT 不要堆代码,每页放一张图加一句结论,讲的时候展开说。
我一般会在文档里附一个 requirements.txt,列出用到的库和版本。这样别人拿到源码能直接 pip install -r requirements.txt 跑起来。版本号不用写太死,用 >= 就行,避免因为小版本差异装不上。
6.2 验证分析结果是否靠谱的三个方法
第一个方法是对比法:把豆瓣 Top250 的评分分布跟豆瓣整体电影评分分布做对比,看 Top250 是否明显偏高。第二个方法是交叉验证:用不同年份区间分组,看结论是否一致。比如 1990s 和 2000s 的高分电影类型分布是否相似。第三个方法是敏感性分析:把评价人数阈值调高或调低,看国家占比和类型占比是否发生显著变化。如果结论对参数不敏感,说明分析结果比较稳健。
# 敏感性分析示例:不同评价人数阈值下的类型分布 for threshold in [10000, 50000, 100000]: subset = df[df["people"] >= threshold] subset_genre = subset["genre"].str.split(" ").explode() top3 = subset_genre.value_counts().head(3) print(f"评价人数 >= {threshold}: {list(top3.index)}")这段代码用三个不同的评价人数阈值过滤数据,分别看类型分布的前三名是否一致。如果三个阈值下前三名都是剧情、爱情、犯罪,说明类型分布结论比较稳。如果差异很大,就需要在文档里说明原因。
6.3 一个让 PPT 加分的技巧:用 Pyecharts 生成可交互演示
PPT 里放静态图有个问题:观众看不清细节。我的习惯是在 PPT 里放一张静态图,同时附一个二维码或者本地 HTML 文件路径,让观众可以自己打开交互图看细节。Pyecharts 生成的 HTML 文件可以直接用浏览器打开,鼠标悬停能看到具体数值,缩放拖拽都支持。演示的时候先讲静态图的整体趋势,再让观众自己操作交互图看细节,比单纯放一张图效果好得多。
具体做法是在 Pyecharts 的 render 方法里指定输出路径,然后在 PPT 最后一页放上文件路径和操作说明。如果教室电脑没装 Python,可以把 HTML 文件和依赖的 JS 文件一起打包,用 U 盘拷过去直接打开。这个技巧我用了好几次,每次都能让演示环节更顺畅。
做这类大作业,最深的体会是:数据清洗花的时间永远比画图多,但清洗干净了,后面的分析和可视化就是水到渠成的事。别急着出图,先把数据搞扎实。希望帮到你。
本文还有配套的精品资源,点击获取