简介:这是一套面向数据科学、市场研究与舆情分析方向的B站全平台数据采集与分析项目源码,基于Python构建分布式爬虫框架,可自动化获取视频数据、用户信息、弹幕、评论、点赞、收藏、分享、播放量、粉丝数、关注列表、历史投稿、直播数据及专栏文章等多维信息,适合具备一定Python基础、希望实践分布式爬虫与数据分析的学习者。压缩包共112个文件,约5.29MB,以js、png、ipynb、md、json、py、cpp、yml、css、html等为主,涵盖前端页面、Jupyter分析笔记、配置脚本与说明文档,并附有docx说明文件辅助安装与配置。目前已有146人学习下载。项目完整呈现了从数据采集、清洗存储到Pandas、NumPy分析及可视化展示的流程,读者可借此理解分布式任务调度、异步数据抓取与反爬应对思路,并参考其目录结构快速搭建自己的视频平台数据采集与分析环境。
1. 从一份“混装”压缩包说起:B站全平台数据采集到底能拿到什么
如果你拿到一个压缩包,解压后看到dllmain.c、iodata.cpp、bulma.css、iodata.dll和一份附赠资源.docx混在一起,第一反应大概是“这到底是爬虫还是桌面程序”。这正是 B站全平台数据爬取与分析系统 的真实面貌:它不是单一脚本,而是一套把采集、清洗、存储、分析串起来的工程化资源包。核心能力覆盖视频数据、用户信息、弹幕、评论、点赞、收藏、分享、播放量、粉丝数、关注列表、历史投稿、直播数据和专栏文章,适合做内容分析、舆情观察或推荐策略验证的从业者。Python 分布式爬虫框架负责调度,C++ 编译出的iodata.dll承担高频 IO 或协议解析,前端用 Bulma 搭可视化面板。新手能照着跑通单机采集,熟手能拆出分布式节点做横向扩展。这一章先把“能解决什么”说清楚,后面再落到具体命令和参数。
2. 分布式爬虫框架拆解:从任务队列到 iodata.dll 的职责边界
2.1 为什么不是单机 requests + BeautifulSoup 就够
单机爬虫在 B站这种量级下会撞上三个硬墙:请求频率限制、动态渲染内容、以及数据落库时的 IO 瓶颈。分布式框架把“取任务—发请求—解析—入库”拆成独立角色,用消息队列做缓冲,节点可以水平加。常见做法是 Redis 做任务队列和去重集合,Scrapy-Redis 或自研调度器消费,MySQL 存结构化字段,MongoDB 存弹幕和评论这类半结构化文本。iodata.dll在这里不是装饰品,它通常封装了签名参数生成、二进制协议解析或本地缓存写入,用 C++ 换 Python 在高频调用下的性能差。iodata.cpp和stdafx.cpp是它的源码,iodata.exp是导出符号表,iodata.vcxproj.filters说明它原本在 Visual Studio 里编译。如果你只跑 Python 部分,这个 dll 可以先用纯 Python 实现替代,但吞吐会降一个档次。
2.2 环境搭建与依赖安装
先确认 Python 版本,建议 3.9 到 3.11,太新的版本某些异步库轮子不全。建虚拟环境,装核心依赖。
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install scrapy scrapy-redis redis pymysql pymongo pandas numpy requests逻辑说明:scrapy-redis把 Scrapy 的调度器和去重指纹搬到 Redis,多个爬虫进程共享同一个队列。pymysql和pymongo分别对应 MySQL 和 MongoDB 的写入。参数上,Redis 默认连localhost:6379,MySQL 默认3306,MongoDB 默认27017,如果本机没起这些服务,先装 Docker 镜像或本地服务,否则爬虫启动就会在连接阶段报ConnectionRefusedError。
2.3 任务队列与去重指纹配置
在 Scrapy 项目的settings.py里改调度器类,并指定 Redis 地址。
# settings.py SCHEDULER = "scrapy_redis.scheduler.Scheduler" DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" SCHEDULER_PERSIST = True REDIS_URL = "redis://127.0.0.1:6379/0" SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.PriorityQueue"逻辑说明:SCHEDULER_PERSIST = True让队列在爬虫重启后不丢,适合长时间采集。PriorityQueue按优先级出队,可以把“视频详情页”设高优先级,“用户历史投稿列表”设低优先级。去重指纹默认用请求 URL 的 SHA1,如果同一视频有多个入口 URL,需要重写request_fingerprint方法归一化参数,否则会重复采集。常见坑是 Redis 内存涨太快,去重集合没有过期时间,跑几天就占满,建议定期EXPIRE或换布隆过滤器。
2.4 数据落库:MySQL 与 MongoDB 的分工
结构化字段走 MySQL,弹幕和评论原文走 MongoDB。建表时注意播放量、点赞数用BIGINT,时间字段用DATETIME并加索引。
CREATE TABLE video_stat ( bvid VARCHAR(20) PRIMARY KEY, title VARCHAR(255), play_count BIGINT DEFAULT 0, like_count INT DEFAULT 0, coin_count INT DEFAULT 0, favorite_count INT DEFAULT 0, share_count INT DEFAULT 0, publish_time DATETIME, crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_publish (publish_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:bvid做主键天然去重,重复插入用INSERT ... ON DUPLICATE KEY UPDATE更新计数。utf8mb4是为了存 emoji 和特殊弹幕字符,用utf8会在写入时报Incorrect string value。MongoDB 那边建danmaku集合,按bvid加索引,单条文档存{bvid, time_offset, text, send_time},方便按时间轴回放。
3. 核心数据字段采集实战:视频、用户、弹幕、评论四条线
3.1 视频详情与播放量增量更新
视频详情页返回的 JSON 里,stat对象包含view、like、coin、favorite、share、danmaku。采集时不要只存快照,要按时间序列存增量,否则分析播放量增长曲线时没有后悔药。
import requests, json, pymysql def fetch_video_stat(bvid): url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}" headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} resp = requests.get(url, headers=headers, timeout=10) data = resp.json() if data["code"] != 0: raise ValueError(f"API error: {data['message']}") stat = data["data"]["stat"] return { "bvid": bvid, "title": data["data"]["title"], "play_count": stat["view"], "like_count": stat["like"], "coin_count": stat["coin"], "favorite_count": stat["favorite"], "share_count": stat["share"], "publish_time": data["data"]["pubdate"] }逻辑说明:code != 0时直接抛异常,常见-404是视频被删,-403是权限不足。pubdate是 Unix 时间戳,入库前转成DATETIME。参数上timeout=10防止单请求卡死拖垮整个节点,生产环境建议加代理池和随机 UA,但注意频率控制在合理范围,别把对方服务打挂。
3.2 用户信息与关注列表分页
用户空间接口返回粉丝数、关注数、投稿数。关注列表是分页的,每页 50 条,用pn翻页。
def fetch_user_followings(mid, max_page=10): results = [] for pn in range(1, max_page + 1): url = f"https://api.bilibili.com/x/relation/followings?vmid={mid}&pn={pn}&ps=50" resp = requests.get(url, headers=HEADERS, timeout=10) data = resp.json() if data["code"] != 0: break items = data["data"]["list"] if not items: break results.extend(items) return results逻辑说明:max_page是保护参数,防止无限翻页。ps=50是接口上限,改大无效。返回的list里每条含mid、uname、sign,可以继续递归采集二级关注,但要注意深度控制,否则任务队列会爆炸。常见坑是接口对未登录请求返回-352风控码,需要带SESSDATA或buvid3等 Cookie 字段,具体从浏览器开发者工具里复制,别硬编码在代码里。
3.3 弹幕采集:分段拉取与 XML 解析
弹幕不是一次性全量返回,而是按时间段分片。老接口用cid拉 XML,新接口用segment_index分段。
import xml.etree.ElementTree as ET def fetch_danmaku(cid, date): url = f"https://api.bilibili.com/x/v2/dm/web/history/seg.so?type=1&oid={cid}&date={date}" resp = requests.get(url, headers=HEADERS, timeout=10) # 返回 protobuf 或 XML,视接口版本而定 root = ET.fromstring(resp.content) danmaku_list = [] for d in root.findall("d"): danmaku_list.append({ "time_offset": float(d.get("p").split(",")[0]), "mode": int(d.get("p").split(",")[1]), "text": d.text }) return danmaku_list逻辑说明:p属性是逗号分隔的字符串,第一段是出现时间(秒),第二段是弹幕模式(滚动、顶部、底部)。date参数格式YYYY-MM-DD,只能拉历史弹幕,当天弹幕走实时接口。常见坑是seg.so返回的是 protobuf 二进制,直接ET.fromstring会报解析错误,需要先判断Content-Type,或者用bilibili-api这类封装库处理。
3.4 评论采集与嵌套回复
评论接口分一级评论和二级回复。一级评论用next游标翻页,二级回复用root和parent定位。
def fetch_comments(oid, next_cursor=0): url = "https://api.bilibili.com/x/v2/reply/main" params = {"type": 1, "oid": oid, "next": next_cursor, "mode": 3} resp = requests.get(url, headers=HEADERS, params=params, timeout=10) data = resp.json() if data["code"] != 0: return [], 0 replies = data["data"].get("replies") or [] cursor = data["data"].get("cursor", {}).get("next", 0) return replies, cursor逻辑说明:mode=3是按热度排序,mode=2是按时间。next为 0 时表示没有下一页。每条评论里的rpid是评论 ID,二级回复用https://api.bilibili.com/x/v2/reply/reply?oid=...&root=...&ps=20拉。常见坑是评论接口对高频请求返回空列表但不报错,需要加time.sleep或令牌桶限速,别以为是代码写错了。
4. 避坑与排查:采集链路里最容易翻车的五个点
4.1 现象:爬虫跑几分钟后全部超时,日志显示 412 或 429
原因:请求频率触发风控,对方返回412 Precondition Failed或429 Too Many Requests。解决:在下载中间件里加随机延迟,DOWNLOAD_DELAY设 1 到 3 秒,并开启AUTOTHROTTLE。如果还不行,换 IP 池或降低并发数,CONCURRENT_REQUESTS从默认 16 降到 4。
4.2 现象:弹幕写入 MongoDB 后中文乱码
原因:requests返回的content是 bytes,直接str()转换会按 ASCII 解码。解决:用resp.content.decode('utf-8')或resp.text,MongoDB 连接串加?charset=utf8。MySQL 那边确认库和表都是utf8mb4。
4.3 现象:分布式节点重复采集同一批视频
原因:去重指纹只用了 URL,但同一视频有bvid和aid两种入口。解决:重写request_fingerprint,把bvid归一化后做哈希。或者在入库时用INSERT IGNORE兜底,但队列里的重复任务还是会浪费带宽。
4.4 现象:iodata.dll 加载失败,报找不到模块
原因:dll 是 32 位编译的,Python 是 64 位,或者缺少 VC++ 运行库。解决:用dumpbin /headers iodata.dll看架构,统一成 64 位。或者干脆用ctypes加载时指定完整路径,别依赖系统 PATH。如果只是跑 Python 采集,可以先不加载这个 dll。
4.5 现象:播放量字段偶尔为 0 或负数
原因:接口返回的view在某些情况下是字符串,直接入库时类型不匹配被截断。解决:入库前做int()转换并捕获ValueError,负数说明接口异常,直接丢弃该条记录并记日志,别让脏数据污染分析结果。
5. 从采集到分析:Pandas 做播放量与点赞相关性验证
数据落库后,用 Pandas 做一轮快速验证,确认采集质量。读 MySQL 用pd.read_sql,读 MongoDB 用pymongo转 DataFrame。
import pandas as pd from sqlalchemy import create_engine engine = create_engine("mysql+pymysql://user:pass@127.0.0.1:3306/bili_data?charset=utf8mb4") df = pd.read_sql("SELECT play_count, like_count, coin_count, favorite_count FROM video_stat WHERE play_count > 0", engine) # 计算相关系数矩阵 corr = df.corr() print(corr) # 按播放量分桶看点赞率 df["like_rate"] = df["like_count"] / df["play_count"] df["play_bucket"] = pd.cut(df["play_count"], bins=[0, 1000, 10000, 100000, 1000000, float("inf")]) print(df.groupby("play_bucket")["like_rate"].mean())逻辑说明:corr()默认算皮尔逊相关系数,播放量和点赞数通常高度正相关,但如果发现某段数据相关系数骤降,说明采集期间接口字段有变动。pd.cut分桶后看点赞率均值,能快速判断“高播放是否等于高互动”。参数上play_count > 0过滤掉异常记录,float("inf")处理超大播放量。这一步不需要复杂模型,先确认数据本身没被污染,再谈情感分析或预测。
可视化部分,bulma.css和style.css是前端面板的样式文件,通常配合 Flask 或 Django 模板渲染图表。如果只想快速出图,用matplotlib或pyecharts生成 HTML 报告更省事。附赠资源.docx里一般写了接口字段说明和配置示例,遇到字段含义不确定时先翻它,比猜接口返回值靠谱。
进阶用法上,可以把采集任务按mid哈希分片到多个 Redis 队列,每个节点只消费自己负责的片,避免全局锁竞争。验证方法很简单:起两个节点,看同一批bvid是否只被其中一个消费。我一般会在settings.py里加一行SCHEDULER_QUEUE_KEY = "bili:requests:%(spider)s",让不同爬虫名走不同队列,互不干扰。从那以后我每次扩展节点前都强制走一遍队列隔离检查,不然重复采集的流量账单会让你怀疑人生。希望帮到你。
本文还有配套的精品资源,点击获取