简介:面向Python爬虫与数据分析入门者,这份CSDN下载包以哔哩哔哩番剧数据为实战对象,完整演示从网页爬取、数据清洗到可视化的流程。资源共23个文件,主要包含3个ipynb代码文件、2个xlsx数据集、10张png可视化结果图、5个xml工程配置以及1份Markdown说明文档,压缩包约3.58MB,结构清晰,便于对照学习。目前已有2978人学习下载。包内覆盖总榜番剧信息爬取、逐一访问详情页获取追番人数与评分、数据预处理与历年趋势分析等环节,既有可直接运行的爬虫与分析代码,也有整理好的电子表格和成品图表,Readme中对各项细节作了说明,适合作为Python爬虫入门、数据分析实战的参考资料。
1. 拆一套Python爬虫,把B站动漫从抓取做到可视化
很多人以为爬虫项目就是把网页抓下来存成 CSV,真正跑通全链路才发现,B站动漫这类数据的大头在后半段:数据清洗、维度统计、可视化呈现。这套Python爬虫资源以番剧索引数据为主线,用 requests 拿动漫的评分、追番人数、类型、地区和更新时间,落库后交给 pandas 做分析,最后用 pyecharts 生成能鼠标悬停看数值的 HTML 看板。适合刚入门想完整走一遍数据项目的新手,也适合拿去改造成作品集。我不绕概念,按实战顺序拆开,把请求参数、清洗口径、绘图配置和踩坑记录都过一遍,保证照着能跑。
2. 请求到落库:从番剧索引接口拿数据并写进 CSV
2.1 为什么直接调接口而不是解析 HTML
老脚本最常犯的错,是直接用 requests 抓 B 站番剧频道的 HTML 源码,再用 XPath 去搜索某个 class 名,试图把评分和追番数从 DOM 里抠出来。这种路子不是不能用,问题是番剧列表很多内容是异步渲染的,HTML 里只有一个空壳,爬下来才发现想要的字段根本不在源代码里,白费半天。
常见做法是直接请求番剧索引接口。它返回 JSON,结构稳定,字段直接可用。接口响应里主要是 result.list 下的对象数组,每个对象包含标题、评分、追番人数、类型标签、地区、发布时间等,恰好覆盖数据分析需要的核心维度。比起每几周就改版的页面 DOM,接口字段的容错性好得多。这个方案也不涉及登录态,不用维护 cookie,适合做轻量级数据采集。
提示:走公开接口时保持常规请求频率,单机、分几页慢慢抓,不要瞬时并发打满接口,风控才会把你当正常人。
2.2 请求头与参数配置:先把最小可用参数调通
这里贴出我的常用请求配置,URL 指向番剧索引接口,参数先保持最少,能出数据再逐步加。
import requests import time API_URL = "https://api.bilibili.com/pgc/season/index/result" 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/", "Accept": "application/json, text/plain, */*", } base_params = { "season_version": 1, # 1=连载中,2=已完结,按需切换 "area": -1, # -1 表示不限地区 "order": 3, # 3=按评分排序,4=按追番人数排序 "page": 1, "pagesize": 20, # 单页条数,接口常见上限是 20 }逻辑说明:User-Agent用来声明浏览器身份,Referer指向番剧频道页,这是最常规的两道身份标识,少了很容易被拒绝。base_params里只保留四个字段,先把第一页数据拉通,再去调season_version和order看结果差异。注意order=3在接口里的语义是“按评分排序”,不同接口版本可能会有细微差别,首次请求时打印返回 JSON 的键名确认一下最稳妥。
参数没必要一开始全带上,比如地区筛选、是否独家、是否完结,这些是后来按分析需求加的。参数越少,出问题时越容易定位,是哪个字段导致请求失败,把参数一个个加回去对比就能查出来。
2.3 分页抓取与字段解析:从 JSON 里提炼行的数据
确定参数后写一个分页抓取函数,把每页的 list 抽出来,再逐条转成一行字典。
def fetch_one_page(api_url, headers, params, page): p = params.copy() p["page"] = page resp = requests.get(api_url, params=p, headers=headers, timeout=15) resp.raise_for_status() data = resp.json() if data.get("code") != 0: raise RuntimeError(f"接口业务异常 code={data.get('code')} msg={data.get('message')}") return data.get("result", {}).get("list", []) rows = [] for page in range(1, 6): items = fetch_one_page(API_URL, headers, base_params, page) if not items: break for item in items: order = item.get("order", {}) rows.append({ "title": item.get("title", ""), "score": order.get("score", 0), "follow": order.get("member_count", 0), "styles": "/".join(s.get("name", "") for s in item.get("styles", [])), "area": item.get("areaName", ""), "pub_time": item.get("pubinfo", ""), }) time.sleep(1.5)逻辑说明:resp.json()会把响应解析成字典,code字段为 0 才表示业务成功,负数是接口主动拒绝,比如 -412 代表触发风控。order是嵌套字典,里面放着score和member_count,member_count就是追番人数。styles是字典列表,每项有一个name,用斜杠拼成字符串,是为了后面 pandas 分析时方便做标签展开。
time.sleep(1.5)是限速,本地测试跑 5 页大约十几秒,数据量足够撑起后续的可视化了。真要跑全量数据,再把页数调大,但建议每加一页就观察一次返回条数和响应码。
2.4 落盘 CSV:编码和空行两个细节别踩
抓到的数据最终要写文件,如果这一步不处理好,后面 pandas 读进来会出一堆莫名其妙的问题。
import csv with open("bilibili_anime.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter( f, fieldnames=["title", "score", "follow", "styles", "area", "pub_time"] ) writer.writeheader() writer.writerows(rows)参数说明:newline=""是 csv 模块在 Windows 上必写的参数,不写会在每行后面多出一个空行,Excel 打开时数据行之间全是缝隙。encoding="utf-8-sig"比utf-8多一个 BOM 头,Windows 的 Excel 默认按 GBK 打开文件,没有 BOM 就会乱码。fieldnames的顺序决定 CSV 的列顺序,我一般先写一个测试版本打印前三条记录,确认字典里没有缺字段再落盘,避免后面读盘时报 KeyError 或只有表头没有数据。
到这里,爬虫部分已经完成了从请求到持久化的闭环,拿到的 CSV 大约包含 100 条番剧记录,字段够干净,可以进入分析环节。
3. 数据清洗与维度分析:pandas 统计前的口径处理
3.1 CSV 读入与类型规整
爬虫落盘的数据还比较糙,score和follow在接口里可能是字符串,也可能存在空值。直接拿去画图会报类型错误,所以我第一步永远是转数值类型。
import pandas as pd df = pd.read_csv("bilibili_anime.csv") df["score"] = pd.to_numeric(df["score"], errors="coerce") df["follow"] = pd.to_numeric(df["follow"], errors="coerce") df = df.dropna(subset=["title"]) df["score"] = df["score"].fillna(0) df["follow"] = df["follow"].fillna(0) print(df.shape) print(df.dtypes)逻辑说明:pd.to_numeric配合errors="coerce",会把不能转换的值变成NaN,而不是直接抛异常,这样我们能先看到数据坏在哪。dropna(subset=["title"])去掉标题为空的垃圾行,保留其他字段有空白的行。fillna(0)把评分和追番数的空值补零,但这个处理方式值得讨论:如果做口碑分析,未开播的番剧评分本来就是空,补 0 会把平均分拉低;如果只是统计上榜数量,补 0 反而安全。
这里有一个细节:fillna(0)前一定要确认score是浮点类型,否则字符串拼接会变成 "8.5" 和 0 混在一起。跑一下df.dtypes,看到float64才算过。
3.2 单维统计:评分分布、追番 Top 与类型偏好
清洗完先做三个最常用的单维统计:评分落在哪个区间、哪些番剧追番人数最多、哪些类型标签出现频率最高。
score_dist = df["score"].value_counts(bins=[0, 6, 7, 8, 9, 10], sort=False) print(score_dist) top_follow = df.nlargest(10, "follow")[["title", "follow", "score"]] print(top_follow) style_count = df["styles"].str.split("/", expand=True).stack().value_counts() print(style_count.head(15))参数说明:value_counts(bins=[...])是 pandas 1.x 之后的写法,直接用区间切分评分,sort=False保证区间顺序不乱。nlargest(10, "follow")返回追番人数最大的 10 行。str.split("/", expand=True)会把类型列拆成多列,再.stack()变成一行一个标签,最后value_counts()就是类型标签频次表。
这三组数基本能回答“这批番剧的整体质量如何”“哪部最受欢迎”“观众偏好什么类型”三个问题,是后续可视化的数据基础。
3.3 交叉分析:评分与追番人数的相关性
单维统计只能描述现象,交叉分析才能找到关系。我一般会算一下评分和追番人数的相关系数。
sub = df[df["follow"] > 0] # 过滤掉尚未开播、追番人数为 0 的样本 corr_coef = sub["score"].corr(sub["follow"]) print("评分与追番人数相关系数:", round(corr_coef, 3))逻辑说明:corr()默认算 Pearson 相关系数,范围在 -1 到 1。大于 0.7 算强相关,说明高评分往往伴随高追番量;0.3 到 0.7 中等相关;低于 0.3 基本没有线性关系。这里过滤掉follow=0的行很重要,大量未开播且无人追番的样本会把相关系数人为拉低。
如果跑出来相关系数只有 0.3 左右,不用慌,这是正常现象。B站动漫市场里有一部分小众口碑作品,评分高但追番基数小,这是数据分析里值得单独拎出来讲的结论,而不是一个需要修正的“bug”。
4. 可视化呈现:pyecharts 拼一张可交互看板
4.1 可视化选型:为什么不用纯 matplotlib
matplotlib 出图方便,但它是静态图片,想看具体数值得回到代码里重新打印,发给别人也没法交互。这套项目我选 pyecharts 2.x,因为它直接输出 HTML,图表支持鼠标悬停、缩放、拖拽,一份报告就是一个文件,浏览器打开就能看,不用起服务。
另一个理由是 pyecharts 的 API 设计适合批量生成:初始化图表、add数据、set_global_opts配置标题坐标轴,最后统一render。像我这种要把评分榜、地区分布、类型偏好拼到同一份报告的场景,比 matplotlib 的 subplot 省事得多。
4.2 评分 TOP10 柱状图与地区饼图
评分榜用柱状图,地区分布用环形饼图,这两个图能覆盖“谁最好看”和“产自哪里”两个核心问题。
from pyecharts.charts import Bar, Pie from pyecharts import options as opts top10_score = df.nlargest(10, "score")[["title", "score"]] bar = ( Bar(init_opts=opts.InitOpts(width="900px", height="500px")) .add_xaxis(top10_score["title"].tolist()) .add_yaxis("评分", top10_score["score"].round(2).tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="B站动漫评分 TOP10"), xaxis_opts=opts.AxisOpts(rotate=15), ) ) area_count = df["area"].value_counts() pie = ( Pie(init_opts=opts.InitOpts(width="700px", height="400px")) .add("地区分布", [list(z) for z in zip(area_count.index, area_count.values)], radius=["20%", "60%"], center=["50%", "55%"]) .set_global_opts(title_opts=opts.TitleOpts(title="B站动漫地区分布")) .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {d}%")) )参数说明:rotate=15是横轴标签旋转角度,番剧标题通常很长,不旋转会叠在一起。radius=["20%", "60%"]是环形图的内外半径,把普通饼图变成环形,中间留白让视觉更干净。formatter="{b}: {d}%"是 pyecharts 的模板字符串,{b}是数据项名称,{d}是百分比,必须用这种占位符,不要自己先拼好字符串传进去。
4.3 Page 组合看板与单 HTML 导出
两张图分开是两个文件,用 Page 组件合成一份报告,方便整体查看。
from pyecharts.charts import Page page = Page(layout=Page.SimplePageLayout) page.add(bar, pie) page.render("anime_report.html")参数说明:Page.SimplePageLayout是内置的简单上下布局,图之间自动留白,不会互相挤压。page.add可以按顺序继续加图,render只调用一次,输出一个完整 HTML。我一般会把包括类型偏好词云、评分分布直方图在内的图表都加到 Page 里,控制在 6 张以内,再多文件体积会膨胀到几 MB,浏览器滚动明显变卡。
到这里,爬虫和分析的成果已经变成可见的看板,流程闭环了。但可视化一旦依赖真实数据,“数据本身脏”的问题就会暴露,下面的章把最容易踩的坑集中列出来。
5. 避坑与常见问题:这 5 个坑我几乎每次都踩一遍
5.1 接口返回 code=-412,请求被风控拦截
现象:脚本跑到第 4 页或第 5 页时突然抛出 RuntimeError,打印出 code=-412,后面数据全部中断。
原因:-412 是接口返回的“请求被拦截”业务码,常见于请求频率过高、UA 缺失、或者缺少 Referer。很多人只带一个 UA 就开跑,短时间内连打几十页,触发风控。
解决:补全User-Agent、Referer、Accept三个请求头,并在每页之间加time.sleep(1.5)甚至更长。我一般不推荐多线程并发同一接口,轻量数据采集根本不需要并发,串行加限速最稳。如果项目非要提速,用requests.Session()复用连接,能降低一部分被识别概率。
5.2 score 列出现大量 NaN,图表断档
现象:条形图的某个柱子突然消失,饼图扇区对不上,打印df["score"]发现一堆 NaN。
原因:部分番剧还在预热期或刚开播,接口里根本没有评分字段,JSON 解析后是None,pandas 读进来变成 NaN。如果直接astype(float)会 ValueError。
解决:先pd.to_numeric(df["score"], errors="coerce"),把非数值统一转成 NaN,再根据分析目标决定填充还是剔除。做“整体榜单”可以fillna(0),做“口碑分析”要先把未开播的筛掉,否则补 0 会扭曲平均分。我在做这套看板时,评分榜和平均分统计用的是两套数据口径,这个区分比代码本身更重要。
5.3 CSV 在 Excel 里乱码
现象:pandas 可以正常读,但用 Excel 打开 CSV,中文显示成一堆乱码汉字。
原因:写入时用了encoding="utf-8",Windows 的 Excel 默认按 GBK 解析文本文件,编码对不上,中文全乱。
解决:写文件统一用encoding="utf-8-sig",它会在文件头写入 BOM 标记,Excel 识别后自动用 UTF-8 解码。注意这个 BOM 在 pandas 读取时不会造成字段名异常,可以放心用。已经乱码的文件不用手工改,用 pandas 重新读入再另存一次即可。
5.4 pyecharts 饼图百分比全部显示同一串文字
现象:饼图所有扇区标签都显示“地区 12%”之类的一模一样的文字,鼠标悬停时数值也对不上。
原因:LabelOpts(formatter=...)里用了 f-string,在 Python 侧就把文字拼成了静态字符串,渲染时每个扇区都复用同一个值。比如写成formatter=f"{'地区'}: {12}%",结果所有标签都一样。
解决:formatter 必须用模板字符串formatter="{b}: {d}%",{b}和{d}是图表渲染时动态替换的占位符,交给 pyecharts 处理,不要在 Python 侧拼最终文案。这是 pyecharts 从基层到专业用户都容易翻车的点。
5.5 同一批番剧重复累计,统计数据翻倍
现象:脚本跑了几轮之后,类型统计的总次数明显比上一版多,追番人数合计也翻倍。
原因:抓取范围里同一部番剧可能存在多个条目,也可能因为重复运行脚本时用mode="a"追加写入,导致 CSV 里的行重复。
解决:写入模式固定用mode="w"覆盖,不做追加。另外在清洗阶段加一行df.drop_duplicates(subset=["title"], keep="last"),keep="last"表示遇到重复标题时保留最后一次抓取的结果,因为评分和追番人数是随时间变化的,新值覆盖旧值才是正确的更新逻辑。
6. 进阶:增量更新与看板自动刷新
6.1 合并历史数据,用新值覆盖旧条目
爬虫和分析脚本跑通之后,下一步就是把流程固化下来,每天定时抓一次,让看板数据自动更新。这里的关键是合并历史和新抓到的数据。
import os import pandas as pd def merge_history(old_csv: str, new_df: pd.DataFrame) -> pd.DataFrame: if not os.path.exists(old_csv): return new_df old_df = pd.read_csv(old_csv) merged = pd.concat([old_df, new_df], ignore_index=True) merged = merged.drop_duplicates(subset=["title"], keep="last").reset_index(drop=True) return merged new_df = pd.DataFrame(rows) final_df = merge_history("bilibili_anime.csv", new_df) final_df.to_csv("bilibili_anime.csv", index=False, encoding="utf-8-sig")逻辑说明:pd.concat把历史数据和新增数据纵向拼接,drop_duplicates(subset=["title"], keep="last")是关键,keep="last"保证新抓到的评分和追番数能覆盖昨天的旧值。reset_index(drop=True)让行号重新从 0 开始,避免去重后索引断层。这样每天跑一次,CSV 里永远是最新状态,而不是无限堆积。
6.2 用断言做数据完整性校验
自动化最怕静默失败:脚本跑了但数据实际是空的,看板悄悄变成空白。我习惯在覆盖报告之前加两道轻量断言。
assert len(final_df) >= 10, "抓取结果不足10条,检查接口或请求头" assert final_df["title"].nunique() == len(final_df), "存在重复标题,未正确去重" fresh = pd.read_csv("bilibili_anime.csv") assert fresh["score"].notna().sum() > len(fresh) * 0.8, "有效评分占比低于80%"参数说明:第一道保证条数不是 0,第二道保证标题唯一性,第三道保证有效评分占比。任何一个断言失败都会让脚本抛异常退出,后续的渲染代码不会执行,也就不会生成一份空报告覆盖掉旧看板。
6.3 定时任务与固化习惯
把爬取、合并、清洗、可视化的逻辑封装成一个run_pipeline.py,然后在 Linux 上用 crontab 挂定时任务。
30 3 * * * cd /opt/anime_report && /usr/bin/python3 run_pipeline.py >> logs/run.log 2>&1含义是每天凌晨 3 点 30 分执行一次,标准输出和错误日志都写入logs/run.log。第二天早上去看日志,grep -i error logs/run.log就能知道昨晚跑没跑通。Windows 上则对应“任务计划程序”,触发器设每天凌晨,操作指向 python.exe 和脚本路径。
从那以后我每次跑数据分析脚本,都会强制走一遍“条数校验 + 空值校验 + 同环比看主数值”三步,哪怕只是个几十行的练习脚本也不跳过。这套B站动漫爬虫项目从 requests 抓接口开始,到 pyecharts 生成可交互看板,再到定时增量更新,已经是一套能日常运行的小型数据管线。希望帮到你。
本文还有配套的精品资源,点击获取