资讯动态

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

发布时间:2026/10/15 17:37:39 来源:尧图企业网站定制
简介这是一份面向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否简介接口 summarystring否这张表不是摆设。后面写解析函数时每个字段对应一个提取分支缺字段就填 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, headersHEADERS, paramsparams, timeout10) 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_page30) df normalize(raw) df.to_csv(anime_data.csv, indexFalse, encodingutf-8-sig) print(f落盘完成共 {len(df)} 条)逻辑说明parse_count处理中文数量单位这是动漫数据里最常见的脏数据来源。normalize里对每个字段做兜底drop_duplicates按标题去重因为分页接口有时会重复返回同一条。encodingutf-8-sig是为了 Excel 打开不乱码这个细节很多人踩过。参数方面max_page30按每页 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(datatop, ytitle, xfollow, axax, paletteviridis) ax.set_xlabel(追番人数) ax.set_ylabel() ax.set_title(f追番人数 Top {CONFIG[top_n]}) plt.tight_layout() plt.savefig(top_follow.png, dpi150) plt.close()逻辑说明nlargest比sort_values().head()更直观palette控制配色dpi150保证导出清晰度。tight_layout防止中文标签被截断这个坑几乎每个人都踩过。参数方面min_follow用来过滤掉数据异常的低值条目避免图表被长尾拉偏。# 图2评分分布 fig, ax plt.subplots(figsize(8, 5)) sns.histplot(df[df[rating] 0][rating], bins20, kdeTrue, axax, color#4c72b0) ax.set_xlabel(评分) ax.set_ylabel(条目数) ax.set_title(评分分布) plt.tight_layout() plt.savefig(rating_dist.png, dpi150) plt.close()逻辑说明bins20是经验值评分范围 0 到 1020 个区间每个 0.5 分粒度合适。kdeTrue叠加密度曲线方便看分布形状。过滤rating 0是为了排除缺失值否则 0 分会把分布拉出一个假峰。# 图3播放量与追番人数散点 fig, ax plt.subplots(figsize(8, 6)) sns.scatterplot(datadf, xview, yfollow, alpha0.6, axax) 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, dpi150) plt.close()逻辑说明播放量和追番人数通常跨几个数量级用对数轴才能看出趋势。alpha0.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(xtag_counts.values, ytag_counts.index, axax, palettemagma) ax.set_xlabel(出现次数) ax.set_ylabel() ax.set_title(标签词频 Top 15) plt.tight_layout() plt.savefig(tag_freq.png, dpi150) 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-8Excel 在部分系统下按 GBK 解析。解决写文件时加encodingutf-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第 {i1} 次失败: {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()扫一遍数值范围确认没有异常值混进去。这套习惯帮我省掉了至少三次返工。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑