news 2026/10/14 17:13:02

Python爬虫实战:B站动漫数据抓取与可视化分析全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:B站动漫数据抓取与可视化分析全流程

简介:这是一份面向Python爬虫初学者与数据分析爱好者的实战项目资源,以B站番剧为切入点,串联爬虫抓取、数据清洗、统计分析与可视化全流程。项目先爬取总榜获取动漫粗略信息与直达链接,再逐条访问详情页补充数据,最终结合追番人数与评分等指标观察历年ACG文化发展脉络。压缩包共23个文件、约3.58MB,包含3个ipynb代码笔记、2个xlsx数据集、10张png可视化结果图及若干xml配置与说明文档,覆盖爬虫、数据处理、数据分析三个环节,并附有readme对实现细节做系统讲解。目前已有2980人学习下载。读者可据此掌握请求构造、页面解析、数据持久化与图表呈现的完整思路,也可直接复用数据集与可视化结果进行二次分析,适合作为爬虫入门与数据分析练手的参考案例。

1. 从番剧列表到可视化看板:Python爬虫做B站动漫数据分析的完整落地路径

很多人第一次想拿B站动漫数据做分析,卡住的地方不是爬虫写不出来,而是爬到一半发现字段对不上、翻页翻不动、可视化出来一堆看不懂的图。这个标题讲的事情其实很具体:用 Python 把 B 站动漫(番剧)的公开列表数据抓下来,清洗成结构化表格,再做播放量、评分、追番人数这些维度的可视化分析。它适合两类人——一类是刚学完 requests 和 pandas、想找一个真实场景练手的新手;另一类是想给内容选题、番剧推荐、舆情观察做数据支撑的从业者。整条链路的核心难点不在"抓",而在"抓什么字段、怎么翻页、怎么把评分和播放量放在同一张图里讲清楚"。

2. 先想清楚抓什么:B站动漫数据的字段拆解与接口选型

2.1 番剧列表页到底有哪些可用字段

做动漫数据分析,第一步不是打开编辑器,而是打开浏览器开发者工具,把番剧索引页的请求看一遍。B站番剧索引页(番剧区)通常按"全部 / 连载 / 完结"分类,支持按播放数、追番数、评分、更新时间排序。你在页面上能看到的每一个数字,背后都是一个可抓取的字段。

我一般会先列一张字段清单,明确哪些是列表页直接给的、哪些必须进详情页才能拿到:

字段名含义来源是否必抓
media_id / season_id番剧唯一标识列表接口是
title番剧名称列表接口是
order_type排序类型(播放/追番/评分)列表接口是
score评分(0-10,部分未开分)列表接口是
play_count播放量列表接口是
follow_count追番人数列表接口是
danmaku_count弹幕数详情接口否
pub_time开播时间详情接口否
tags标签(热血/日常/恋爱等)详情接口否

这里有个血泪经验:评分字段在未开分时返回的是 0 或者空字符串,如果你直接拿它算平均值,会把整体评分拉低一大截。所以清洗阶段必须把 score 为 0 的记录单独标记,而不是当成"0 分"处理。

2.2 列表接口 vs 详情接口:选型理由

列表接口的好处是一次能拿几十条,字段够做宏观分析(播放量分布、评分分布、追番转化率);缺点是标签、开播时间这类维度拿不到。详情接口字段全,但请求量大,几十条还好,上千条就要考虑限速和缓存。

我的建议是分两层抓:第一层用列表接口把全量番剧的基础字段拉下来,存成 CSV;第二层只对分析需要的子集(比如评分 Top 100 或追番 Top 100)补抓详情。这样既控制了请求量,又保证了分析维度够用。常见做法是先用列表接口跑通全流程,确认字段没问题,再决定要不要补详情。

2.3 翻页与排序参数怎么设

列表接口一般靠page和page_size控制分页,靠order_type控制排序。这里最容易翻车的是:不同排序方式下,同一部番剧的排名会变,但总数不变。如果你按播放量抓一遍、再按追番抓一遍,会得到两份顺序不同但内容重叠的数据,直接 concat 会出现重复行。

正确做法是选定一个主排序(比如按播放量),把全量抓完,排序维度作为分析时的操作,而不是抓取时的操作。翻页时要注意:当某一页返回空列表或条数明显少于 page_size,说明已经到底,应该停止,而不是死循环翻下去。

3. 用 requests + pandas 把番剧列表抓成结构化表格

3.1 最小可运行抓取脚本

下面这段代码是我常用的骨架,抓的是番剧索引的列表数据。注意:接口地址和参数名会随站点调整,实际以你开发者工具里看到的为准,这里用占位说明结构。

import requests import pandas as pd import time import random # 请求头:带上 UA 和 Referer,模拟正常浏览器访问 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36", "Referer": "https://www.bilibili.com/anime/index/", } # 列表接口地址(以实际抓包为准,此处为结构示意) LIST_API = "https://api.bilibili.com/pgc/season/index/result" def fetch_page(page: int, page_size: int = 20, order_type: int = 3): """抓取单页番剧列表,order_type=3 通常表示按播放量排序""" params = { "order": order_type, # 排序方式 "page": page, # 页码,从 1 开始 "page_size": page_size, # 每页条数 "season_type": 1, # 1 一般代表番剧 } resp = requests.get(LIST_API, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() data = resp.json() # 不同接口返回结构不同,这里取 list 字段 return data.get("data", {}).get("list", []) def crawl_all(max_page: int = 50): """翻页抓取,遇到空页停止""" rows = [] for page in range(1, max_page + 1): items = fetch_page(page) if not items: print(f"第 {page} 页为空,停止抓取") break for it in items: rows.append({ "media_id": it.get("media_id"), "title": it.get("title"), "score": it.get("score"), "play_count": it.get("play_count"), "follow_count": it.get("follow_count"), }) print(f"第 {page} 页抓取 {len(items)} 条") # 随机休眠,降低被封风险 time.sleep(random.uniform(1.0, 2.5)) return pd.DataFrame(rows) if __name__ == "__main__": df = crawl_all(max_page=30) df.to_csv("anime_list_raw.csv", index=False, encoding="utf-8-sig") print("总条数:", len(df))

逻辑说明:fetch_page负责单页请求,把排序、页码、每页条数作为参数传进去;crawl_all负责翻页循环,遇到空列表就 break,避免无意义请求。time.sleep(random.uniform(1.0, 2.5))是关键的限速手段,固定间隔容易被识别,随机间隔更接近真人操作。

参数说明:order_type决定排序维度,常见取值里 3 是播放量、4 是追番数、5 是评分(具体以抓包为准);page_size不要设太大,20-30 比较稳;max_page是安全上限,防止接口异常时无限翻页。

3.2 数据清洗:评分、播放量、追番数的三个坑

抓下来的原始表通常不能直接用,至少要处理三类问题。

第一类是评分缺失。未开分的番剧 score 可能是 0 或空,直接算均值会失真。处理方式是加一列标记:

import pandas as pd df = pd.read_csv("anime_list_raw.csv") # 评分缺失标记:0 或空都视为未开分 df["score_valid"] = df["score"].apply(lambda x: pd.notna(x) and float(x) > 0) # 只对有效评分算均值 valid_score_mean = df.loc[df["score_valid"], "score"].astype(float).mean() print("有效评分均值:", round(valid_score_mean, 2)) print("未开分番剧数:", (~df["score_valid"]).sum())

第二类是播放量单位。接口返回的 play_count 通常是纯数字,但页面上显示的是"1.2亿""3456.7万"。如果你从页面文本抓,必须先做单位换算;从接口抓就省了这一步,但要注意数值类型可能是字符串。

第三类是重复。同一部番剧可能因为多季、多语言版本出现多条记录,media_id 相同或标题高度相似。去重时优先按 media_id,其次按标题归一化(去掉"第X季""国语版"等后缀)后再判断。

3.3 存成 CSV 还是 SQLite:小数据量的选择

几百到几千条数据,CSV 完全够用,pandas 读写都方便,还能直接用 Excel 打开检查。数据量上万、或者需要多次增量更新时,SQLite 更合适,可以用 media_id 做主键做 upsert,避免重复抓取。

我一般先用 CSV 跑通分析,确认字段和清洗逻辑没问题,再迁到 SQLite 做增量。迁移成本很低,pandas 的to_sql一行就能写进去。不要一上来就上数据库,会拖慢调试节奏。

4. 可视化怎么做才有信息量:评分、播放量、追番数的组合分析

4.1 评分分布直方图与播放量对数散点

可视化最容易犯的错是"图很漂亮但看不出结论"。做动漫数据分析,我建议先做两张基础图:评分分布直方图、播放量-追番数散点图。

import matplotlib.pyplot as plt import numpy as np # 设置中文字体,避免乱码 plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False df_valid = df[df["score_valid"]].copy() df_valid["score"] = df_valid["score"].astype(float) df_valid["play_count"] = pd.to_numeric(df_valid["play_count"], errors="coerce") df_valid["follow_count"] = pd.to_numeric(df_valid["follow_count"], errors="coerce") fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 左图:评分分布 axes[0].hist(df_valid["score"], bins=20, color="#4C72B0", edgecolor="white") axes[0].set_title("番剧评分分布") axes[0].set_xlabel("评分") axes[0].set_ylabel("番剧数量") # 右图:播放量 vs 追番数(对数坐标) axes[1].scatter(df_valid["play_count"], df_valid["follow_count"], alpha=0.5, s=20, color="#DD8452") axes[1].set_xscale("log") axes[1].set_yscale("log") axes[1].set_title("播放量 vs 追番数(对数坐标)") axes[1].set_xlabel("播放量") axes[1].set_ylabel("追番人数") plt.tight_layout() plt.savefig("anime_overview.png", dpi=150) plt.show()

逻辑说明:评分分布用直方图看整体集中在哪个区间;播放量和追番数跨度极大,必须用对数坐标,否则大部分点会挤在左下角。散点能直观看出"高播放是否一定高追番",很多番剧播放高但追番低,说明是路人盘而非核心粉。

参数说明:bins=20控制直方图柱子数量,太少看不出分布形状,太多会碎;alpha=0.5让重叠点可见;dpi=150保证导出图清晰。

4.2 追番转化率:比播放量更能说明粘性

单看播放量只能说明曝光,追番数/播放量的比值才是粘性指标。我一般会算一个"追番转化率",再按这个指标排序看 Top 20。

# 追番转化率 = 追番人数 / 播放量 df_valid = df_valid[df_valid["play_count"] > 0] df_valid["follow_rate"] = df_valid["follow_count"] / df_valid["play_count"] top20 = df_valid.nlargest(20, "follow_rate")[["title", "follow_rate", "play_count", "follow_count"]] plt.figure(figsize=(10, 8)) plt.barh(top20["title"], top20["follow_rate"], color="#55A868") plt.xlabel("追番转化率") plt.title("追番转化率 Top 20") plt.gca().invert_yaxis() plt.tight_layout() plt.savefig("follow_rate_top20.png", dpi=150) plt.show()

这个指标能帮你发现一些"播放不高但粉丝极黏"的番剧,对内容选题和推荐策略很有参考价值。注意:播放量极低的番剧转化率可能虚高,分析时要加一个播放量下限过滤,比如只保留播放量大于 100 万的记录。

4.3 标签维度聚合:用 groupby 看哪类题材更吃香

如果补抓了详情接口的 tags 字段,就能做题材维度的聚合分析。标签通常是列表,需要先展开再 groupby。

# 假设 df_detail 里有 media_id 和 tags(列表形式) exploded = df_detail.explode("tags") agg = exploded.groupby("tags").agg( anime_count=("media_id", "count"), avg_score=("score", "mean"), avg_play=("play_count", "mean"), ).reset_index() # 只看样本量足够的标签 agg = agg[agg["anime_count"] >= 5].sort_values("avg_play", ascending=False) print(agg.head(15))

逻辑说明:explode把一行多标签拆成多行,每个标签单独参与聚合;anime_count >= 5是过滤样本太少的标签,避免"某标签只有 1 部番剧但播放极高"这种噪声结论。

参数说明:agg里可以换任意聚合函数,比如中位数median比均值更抗极端值;排序字段按分析目标选,看热度用 avg_play,看口碑用 avg_score。

5. 避坑与排查:抓取和分析阶段最容易翻车的 5 个点

5.1 现象:抓了几页后返回空数据或 412

原因:请求频率过高,触发了站点的风控,或者请求头缺少关键字段(Referer、Cookie)。解决:把休眠时间调大,加上随机抖动;补齐 Referer;如果接口需要登录态,手动从浏览器复制 Cookie 到请求头。不要用固定 1 秒间隔硬刷,那是典型的翻车姿势。

5.2 现象:评分均值明显偏低

原因:把未开分的 0 分记录当成了真实评分。解决:清洗阶段先标记 score_valid,只对有效评分做统计。这个坑非常隐蔽,因为 0 分看起来像个正常数值,不检查就会一直错下去。

5.3 现象:翻页抓到重复数据

原因:换了排序方式重新抓,或者接口在翻页时因为数据更新导致顺序漂移。解决:抓取时只用一种排序,抓完存原始数据;去重按 media_id 做 drop_duplicates;增量抓取时用 media_id 做 upsert 而不是直接 append。

5.4 现象:可视化中文显示成方块

原因:matplotlib 默认字体不含中文。解决:设置plt.rcParams["font.sans-serif"] = ["SimHei"],并加axes.unicode_minus = False处理负号。Linux 环境下如果没有 SimHei,换成系统里已有的中文字体,比如 WenQuanYi 系列。

5.5 现象:散点图所有点挤在一起

原因:播放量和追番数跨度几个数量级,线性坐标下小值全挤在原点附近。解决:对数和线性坐标切换,set_xscale("log")和set_yscale("log")一起用。注意对数坐标不能有 0 值,清洗时要把 0 过滤掉,否则会报错或丢点。

6. 把分析做成可复用的脚本:参数化与增量更新

做到这一步,你已经能跑通"抓取-清洗-可视化"的完整链路。但真正让这套东西有价值的是可复用——下次想换排序维度、换时间范围、换分析指标,不用重写代码。

我的习惯是把配置抽成一个字典放在脚本顶部:

CONFIG = { "order_type": 3, # 3=播放量, 4=追番数, 5=评分 "max_page": 50, "page_size": 20, "sleep_range": (1.0, 2.5), "min_play_for_rate": 1_000_000, # 转化率分析的最低播放量 "output_dir": "./output", }

抓取函数、清洗函数、绘图函数都从这个 CONFIG 读参数,换分析目标时只改配置不改逻辑。增量更新则靠 media_id 判断:读入已有 CSV,把新抓的数据按 media_id 合并,保留最新字段值。

还有一个我踩过的坑:不要把抓取和分析写在同一个脚本里。抓取脚本负责产出原始 CSV,分析脚本只读 CSV。这样抓取失败不会影响已有数据的分析,分析逻辑调整也不用重新抓一遍。两个脚本之间用文件解耦,是最省心的做法。

验证这套流程是否可靠,有个简单办法:换一个排序维度重抓一次,对比两次结果的 media_id 集合是否一致。如果一致,说明翻页逻辑没问题;如果差很多,说明中途有页丢失或重复,需要检查 break 条件和休眠策略。

最后说个我自己的教训:早期做这类分析,我总想一次抓全、一次做完美,结果脚本越写越复杂,一个报错就全盘卡住。后来改成"先抓 3 页跑通全流程,再放大到全量",调试效率高了很多。数据分析和爬虫一样,小步验证比一步到位靠谱。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:12:14

信息学竞赛复赛试题解析:从线性枚举到前缀和与约瑟夫环

简介:第18届绍兴市少儿信息学竞赛复赛试题已整理为一份Word文档,面向小学阶段的信息学爱好者与编程初学者,既可用于备赛练习,也可作为课堂或社团的测评资料。卷面包含“好朋友”“统计人口”“卫星”“粉刷匠”四道经典题目&#…

作者头像 李华
网站建设 2026/10/14 17:10:15

OpenCode Go额度又提升了

10 月 12 日 17:14,OpenCode 官方账号在 X 上发了条帖子。Operation Cheepseek 第三阶段,$10的GO套餐,DeepSeek V4.1 Flash 使用额度提升到 $80。 X 上 OpenCode 官方账号的公告帖,两行字,603K 次查看,6.9K…

作者头像 李华
网站建设 2026/10/14 17:10:06

木材科学类论文的图表与排版:从一块板倒推该放哪些数据

木材科学与工程方向的论文,逻辑起点往往是一件具体的产品——一块人造板、一件家具部件。从成品倒推该测什么、该放什么图、该怎么排版,比从概念出发更实用。 说明:木材科学与工程不属于平台官网列明的学科专项方向(官网专项为生信…

作者头像 李华
网站建设 2026/10/14 17:10:00

面向对象数据库(OODB):对象即存储的原生持久化方案

1. 为什么今天还要谈“面向对象数据库”——一个被低估的架构选择很多人第一次听到“面向对象数据库”(Object-Oriented Database,简称OODB)时,下意识反应是:“这东西不是90年代就淘汰了吗?现在不都用关系型…

作者头像 李华
网站建设 2026/10/14 17:09:48

Unity边缘检测原理与工业级Shader实现指南

1. 项目概述:为什么边缘检测不是“加个描边”那么简单Unity Shader里的“边缘检测”,听起来像是美术同学随口提的一句需求:“模型轮廓加个黑边,显得更卡通一点。”但真要动手写,很多人卡在第一行代码——不是不会调用_…

作者头像 李华
网站建设 2026/10/14 17:08:49

无需GPU的轻量级日语OCR:开源工具NDLOCR-Lite完整介绍

【免费下载链接】ndlocr-lite NDLOCR-Liteアプリケーションのリポジトリ(ソースコードを含む)NDLOCR‑Lite application repository (including source code) 项目地址: https://gitcode.com/gh_mirrors/nd/ndlocr-lite 点击查看 免费下载 N…

作者头像 李华