简介:这是一份面向Python爬虫初学者与数据分析爱好者的实战项目资料,围绕B站番剧数据展开,解决从数据采集到可视化呈现的完整链路学习问题。资源共23个文件,包含3个ipynb代码文件、2个xlsx数据集、10张png可视化结果图及若干xml配置与说明文档,压缩包约3.58MB,结构清晰便于按模块学习。已有2980人学习下载,热度较高。内容上,爬虫部分先抓取总榜获取动漫粗略信息与直达链接,再逐条访问详情页补充数据;数据处理与数据分析代码分别独立成文件,配合已处理数据集可直接复现分析流程;可视化结果图直观展示历年番剧追番人数与评分分布,帮助理解B站ACG文化发展脉络。随包提供的readme文档详细说明了爬虫细节、数据处理与可视化思路,适合作为爬虫入门与数据分析练手的参考案例。
1. 从零拆解一份 Python 爬虫与动漫数据分析可视化资源
如果你手头正好有一份「Python爬虫-B站动漫数据分析与可视化」的资源包,却不确定它到底能不能跑、数据从哪来、图表怎么出,这篇笔记就是替你踩一遍坑。它本质上是一条完整的数据链路:用 Python 爬虫抓取动漫条目信息,清洗成结构化数据,再用可视化把播放量、评分、追番人数这些维度画出来。适合两类人:一类是刚学完 requests 和 pandas,想找个真实场景练手的;另一类是做运营或内容分析,想快速搭一套可复用的数据看板。资源里通常包含爬虫脚本、数据文件、分析 notebook 和图表模板,但真正决定成败的不是代码本身,而是你对目标站点结构、反爬边界和字段口径的理解。下面按「能跑通 → 能改参数 → 能排错」的顺序拆开讲。
2. 爬虫链路拆解:请求构造、字段定位与数据落盘
2.1 为什么先定字段再写请求
很多人一上来就requests.get,结果抓回来一堆 HTML 却不知道要提取什么。正确的顺序是反过来的:先确定你要哪些字段,再去看页面里这些字段藏在哪。动漫条目页常见字段包括标题、评分、播放量、追番人数、简介、标签、更新状态。这些字段有的在静态 HTML 里,有的通过接口返回 JSON。判断方法很简单:打开浏览器开发者工具,刷新页面,看 Network 面板里有没有返回结构化数据的 XHR 请求。如果有,优先走接口,因为接口返回的 JSON 比解析 HTML 稳定得多,字段名也清晰。
我一般会先建一张字段映射表,把「业务字段名 → 页面位置 → 数据类型 → 是否必填」写清楚。这张表后面直接决定 DataFrame 的列结构,也决定清洗脚本怎么写。
| 业务字段 | 常见来源 | 数据类型 | 是否必填 |
|---|---|---|---|
| 标题 | 接口 title 或 HTML 标题节点 | string | 是 |
| 评分 | 接口 rating 或页面评分节点 | float | 是 |
| 播放量 | 接口 view 或播放数节点 | int | 否 |
| 追番人数 | 接口 follow 或追番节点 | int | 否 |
| 标签 | 接口 tags 数组 | list | 否 |
| 简介 | 接口 summary | string | 否 |
这张表不是摆设。后面写解析函数时,每个字段对应一个提取分支,缺字段就填 None,不要直接跳过整条记录,否则最后统计时你会发现样本量对不上。
2.2 请求构造与分页参数
假设目标接口是分页返回的,常见参数有page、page_size、order。下面是一个最小可跑的请求骨架,注意 headers 里必须带 User-Agent 和 Referer,否则大概率返回 403 或空数据。
import requests import time import random BASE_URL = "https://example.com/api/anime/list" # 替换为实际接口地址 HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://example.com/anime", # 必须与目标站点同域 "Accept": "application/json, text/plain, */*", } def fetch_page(page: int, page_size: int = 20) -> dict: params = { "page": page, "page_size": page_size, "order": "follow", # 按追番人数排序,可按需改成 view / rating } resp = requests.get(BASE_URL, headers=HEADERS, params=params, timeout=10) resp.raise_for_status() # 非 200 直接抛异常,避免把错误页当数据 return resp.json() def crawl_all(max_page: int = 50) -> list: records = [] for p in range(1, max_page + 1): data = fetch_page(p) items = data.get("data", {}).get("list", []) if not items: break # 没有数据说明翻到头了 records.extend(items) time.sleep(random.uniform(0.5, 1.5)) # 随机间隔,降低被封风险 return records逻辑说明:fetch_page负责单页请求,raise_for_status是关键,很多人省掉这行,结果 403 页面被当成 JSON 解析,报一堆看不懂的错。crawl_all里用random.uniform做随机间隔,比固定sleep(1)更自然。max_page是保护参数,防止死循环。参数方面,page_size不要设太大,常见接口限制在 20 到 50 之间,设 100 可能直接返回空。order字段决定排序口径,如果你要做「追番人数 Top 100」,就用 follow;要做「评分最高」,就换 rating。
2.3 解析、清洗与落盘
拿到 JSON 后不要直接塞进 DataFrame,先做一层字段提取和类型转换。常见问题是播放量返回的是「1.2亿」这种字符串,评分可能是「9.8」字符串,标签可能是逗号拼接的字符串而不是数组。
import pandas as pd import re def parse_count(text) -> int: """把 '1.2亿' '3456.7万' '1234' 统一转成整数""" if text is None: return 0 text = str(text).strip() if "亿" in text: return int(float(text.replace("亿", "")) * 100_000_000) if "万" in text: return int(float(text.replace("万", "")) * 10_000) return int(float(text)) if text else 0 def normalize(records: list) -> pd.DataFrame: rows = [] for r in records: rows.append({ "title": r.get("title", "").strip(), "rating": float(r.get("rating", 0) or 0), "view": parse_count(r.get("view")), "follow": parse_count(r.get("follow")), "tags": ",".join(r.get("tags", [])) if isinstance(r.get("tags"), list) else str(r.get("tags", "")), "summary": re.sub(r"\s+", " ", r.get("summary", "") or ""), }) df = pd.DataFrame(rows) df = df.drop_duplicates(subset=["title"]) # 按标题去重 df = df[df["title"] != ""] # 去掉空标题 return df if __name__ == "__main__": raw = crawl_all(max_page=30) df = normalize(raw) df.to_csv("anime_data.csv", index=False, encoding="utf-8-sig") print(f"落盘完成,共 {len(df)} 条")逻辑说明:parse_count处理中文数量单位,这是动漫数据里最常见的脏数据来源。normalize里对每个字段做兜底,drop_duplicates按标题去重,因为分页接口有时会重复返回同一条。encoding="utf-8-sig"是为了 Excel 打开不乱码,这个细节很多人踩过。参数方面,max_page=30按每页 20 条算大约 600 条,足够做基础分析;如果要全量,先跑一页看返回结构里有没有total字段,有的话按总数算页数。
3. 数据分析与可视化:从字段口径到图表选型
3.1 分析前先统一口径
数据落盘后,最容易翻车的地方是口径不一致。比如「播放量」在不同条目里可能包含预告片,也可能只算正片;「追番人数」可能是当前值,也可能是历史峰值。资源包里如果带了分析 notebook,先看它的字段说明,没有说明就自己按下面三条原则处理:第一,所有数量字段统一转成整数,单位统一为「次」或「人」;第二,评分统一保留一位小数,缺失值填 0 并单独标记;第三,标签做拆分后统计频次,不要直接对拼接字符串做词频。
我一般会在 notebook 开头写一个config单元格,把口径参数集中管理:
CONFIG = { "min_follow": 1000, # 追番人数低于此值不参与排名 "min_rating": 6.0, # 评分低于此值不参与推荐 "top_n": 20, # 图表展示条数 "tag_split": ",", # 标签分隔符 }这样后面改口径只改一处,不用满篇找数字。
3.2 四类核心图表与参数
动漫数据分析常见的图表有四类:追番人数 Top N 条形图、评分分布直方图、播放量与追番人数散点图、标签词频条形图。下面用 matplotlib 和 seaborn 各给一个可复现的例子。
import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei"] # 中文字体 plt.rcParams["axes.unicode_minus"] = False df = pd.read_csv("anime_data.csv") # 图1:追番人数 Top 20 top = df[df["follow"] >= CONFIG["min_follow"]].nlargest(CONFIG["top_n"], "follow") fig, ax = plt.subplots(figsize=(10, 8)) sns.barplot(data=top, y="title", x="follow", ax=ax, palette="viridis") ax.set_xlabel("追番人数") ax.set_ylabel("") ax.set_title(f"追番人数 Top {CONFIG['top_n']}") plt.tight_layout() plt.savefig("top_follow.png", dpi=150) plt.close()逻辑说明:nlargest比sort_values().head()更直观,palette控制配色,dpi=150保证导出清晰度。tight_layout防止中文标签被截断,这个坑几乎每个人都踩过。参数方面,min_follow用来过滤掉数据异常的低值条目,避免图表被长尾拉偏。
# 图2:评分分布 fig, ax = plt.subplots(figsize=(8, 5)) sns.histplot(df[df["rating"] > 0]["rating"], bins=20, kde=True, ax=ax, color="#4c72b0") ax.set_xlabel("评分") ax.set_ylabel("条目数") ax.set_title("评分分布") plt.tight_layout() plt.savefig("rating_dist.png", dpi=150) plt.close()逻辑说明:bins=20是经验值,评分范围 0 到 10,20 个区间每个 0.5 分,粒度合适。kde=True叠加密度曲线,方便看分布形状。过滤rating > 0是为了排除缺失值,否则 0 分会把分布拉出一个假峰。
# 图3:播放量与追番人数散点 fig, ax = plt.subplots(figsize=(8, 6)) sns.scatterplot(data=df, x="view", y="follow", alpha=0.6, ax=ax) ax.set_xscale("log") # 播放量跨度大,用对数轴 ax.set_yscale("log") ax.set_xlabel("播放量(对数轴)") ax.set_ylabel("追番人数(对数轴)") ax.set_title("播放量与追番人数关系") plt.tight_layout() plt.savefig("view_follow_scatter.png", dpi=150) plt.close()逻辑说明:播放量和追番人数通常跨几个数量级,用对数轴才能看出趋势。alpha=0.6让重叠点可见。如果散点呈现明显分层,说明数据里混了不同量级的条目,需要回到清洗阶段检查。
# 图4:标签词频 Top 15 tags = df["tags"].str.split(CONFIG["tag_split"]).explode().str.strip() tag_counts = tags[tags != ""].value_counts().head(15) fig, ax = plt.subplots(figsize=(10, 6)) sns.barplot(x=tag_counts.values, y=tag_counts.index, ax=ax, palette="magma") ax.set_xlabel("出现次数") ax.set_ylabel("") ax.set_title("标签词频 Top 15") plt.tight_layout() plt.savefig("tag_freq.png", dpi=150) plt.close()逻辑说明:explode把标签列从字符串拆成多行,再做value_counts。这一步的前提是清洗阶段已经用统一分隔符拼接,否则拆分结果会乱。参数方面,head(15)控制展示条数,标签太多图表会挤。
3.3 可视化选型的三个判断
第一,比较类用条形图,不要用饼图,饼图超过 5 个分类就看不清。第二,分布类用直方图或箱线图,箱线图适合对比多组分布。第三,关系类用散点图,如果点太多就加透明度或改用 hexbin。资源包里如果带了图表模板,先跑通默认参数,再按自己的数据量调整figsize和dpi。不要一上来就改配色,先把数据跑对。
4. 避坑与排查:五个真实翻车记录
4.1 请求返回 200 但数据为空
现象:resp.status_code是 200,但resp.json()里data.list是空数组。原因通常是 headers 缺少 Referer 或 Cookie,或者page_size超过接口限制。解决:先补全 Referer,再把page_size降到 20 试一次;如果还不行,检查接口是否需要签名参数,这类参数一般在页面 JS 里,搜sign或token能找到。
4.2 中文乱码
现象:CSV 用 Excel 打开是乱码。原因:to_csv默认编码是 utf-8,Excel 在部分系统下按 GBK 解析。解决:写文件时加encoding="utf-8-sig",这个参数会写入 BOM 头,Excel 能正确识别。如果已经写错了,用 pandas 读回来重新写一遍即可。
4.3 数量字段转换报错
现象:parse_count遇到「1.2亿」正常,遇到「--」或空字符串直接抛ValueError。原因:没有做异常兜底。解决:在函数开头加if not text or text == "--": return 0,并且用try/except包住float转换,异常时返回 0 并打印原始值,方便定位。
4.4 图表中文显示方块
现象:标题和轴标签里的中文变成方块。原因:matplotlib 默认字体不含中文。解决:设置plt.rcParams["font.sans-serif"] = ["SimHei"],同时加axes.unicode_minus = False解决负号显示问题。如果系统没有 SimHei,换成Microsoft YaHei或WenQuanYi Micro Hei。
4.5 去重后样本量骤降
现象:抓了 600 条,去重后只剩 300 条。原因:分页接口在排序字段有并列值时,可能重复返回同一条记录,或者标题字段包含特殊字符导致看起来不同实际相同。解决:去重时不要只按标题,可以组合title + rating做键;同时打印重复记录的标题,确认是接口问题还是清洗问题。如果是接口问题,改用id字段去重,前提是接口返回了唯一 id。
5. 进阶技巧:把一次性脚本改成可复用管线
跑通一次不难,难的是下次换个排序口径或换个时间范围还能快速复用。我的习惯是把整条链路拆成三个独立模块:fetcher.py只管请求和重试,parser.py只管字段提取和清洗,analyzer.py只管统计和绘图。三者之间用 CSV 或 Parquet 文件传递数据,不直接互相 import。这样做的好处是,爬虫挂了不影响分析,分析改口径不用重爬。
具体做法是在fetcher.py里加一个重试装饰器:
from functools import wraps def retry(times: int = 3, delay: float = 2.0): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: print(f"第 {i+1} 次失败: {e}") time.sleep(delay * (i + 1)) # 退避递增 raise RuntimeError(f"{func.__name__} 重试 {times} 次仍失败") return wrapper return decorator逻辑说明:delay * (i + 1)做线性退避,第一次等 2 秒,第二次 4 秒,第三次 6 秒。比固定间隔更合理,因为临时故障通常几秒内恢复。参数times默认 3 次,超过就抛异常,不要无限重试。
另一个技巧是给数据文件加时间戳,每次运行生成anime_data_20250101.csv,避免覆盖历史数据。分析时用glob读取最新文件:
import glob import os files = sorted(glob.glob("anime_data_*.csv")) latest = files[-1] df = pd.read_csv(latest) print(f"使用数据文件: {os.path.basename(latest)}")这样即使隔几周再跑,也能快速定位到最近一次的数据。验证方法很简单:改一次order参数,重新跑fetcher,看新文件的记录数和排序是否变化;再跑analyzer,确认图表跟着更新。如果没变,说明中间有缓存或文件没写成功。
从那以后我每次改爬虫参数,都强制先跑一页验证字段结构,再全量抓取,最后用df.describe()扫一遍数值范围,确认没有异常值混进去。这套习惯帮我省掉了至少三次返工。希望帮到你。
本文还有配套的精品资源,点击获取