
简介这是一套面向数据科学、市场研究与舆情分析方向的B站全平台数据采集与分析项目源码适合具备Python基础、希望上手分布式爬虫与数据分析实战的开发者与研究者。系统覆盖视频数据、用户信息、弹幕、评论、点赞、收藏、分享、播放量、粉丝数、关注列表、历史投稿、直播数据与专栏文章等多类目标并配套数据清洗、数据库存储、可视化与机器学习分析思路。资源包共112个文件约5.29MB以js、png、md、ipynb、py、json、yml、html、css等为主涵盖前端页面、Notebook分析、配置脚本与说明文档另附docx说明与txt使用指引便于快速理解项目结构与运行方式。目前已有146人学习下载可帮助读者掌握分布式爬虫框架搭建、异步与动态渲染数据处理、数据清洗入库及多维度分析流程并了解反爬适配与合规采集要点。1. 全平台数据采集系统到底在采什么从一个反直觉的结论说起很多人第一次做视频平台数据采集直觉是“把播放量、点赞数抓下来就完事了”。但真正跑过一轮全平台数据采集系统的人会告诉你播放量是最没用的字段。原因很简单播放量是平台侧聚合后的结果指标它既不能反推用户行为路径也无法做跨分区对比——一个鬼畜区的百万播放和知识区的百万播放背后的用户画像、完播率、互动结构完全不同。基于 Python 的分布式爬虫框架要解决的恰恰是把这个“结果指标”拆成可分析的行为链路用户信息、弹幕、评论、点赞、收藏、分享、播放量、粉丝数、关注列表、历史投稿、直播数据、专栏文章这些字段组合起来才构成一个可做归因分析的数据底座。这套系统适合两类人一类是做内容运营分析、想搞清楚某个分区为什么起量的从业者另一类是想练分布式爬虫工程能力、但苦于找不到真实中等规模数据源的开发者。它不追求“爬全站”而是追求在可控资源下把一条完整的数据链路跑通——从任务分发、去重、限速、断点续爬到落库、清洗、可视化。下面按我实际搭过的一套方案把选型、代码、参数和踩过的坑讲清楚。2. 分布式爬虫框架选型为什么我最终用 Scrapy-Redis 而不是自己写调度2.1 三种常见架构的取舍做视频平台数据采集绕不开三个选择单机多线程、Scrapy-Redis 分布式、以及基于消息队列的自研调度。单机多线程在数据量小于十万级时够用但一旦涉及弹幕和评论这种长尾接口单机 IP 很快触发风控。自研调度灵活但去重、断点续爬、任务重试这些基础设施要自己写工期至少翻三倍。我最终选 Scrapy-Redis核心原因是它把请求队列和去重指纹都放在 Redis 里多个爬虫节点共享同一个队列天然支持横向扩容和断点续爬。代价是 Redis 成为单点需要自己做持久化配置。常见做法是 Redis 开 AOF 持久化爬虫节点无状态挂了直接重启即可。方案去重断点续爬扩容成本适用数据量单机多线程内存集合不支持高 10 万Scrapy-RedisRedis 集合支持低10 万千万自研 MQ 调度自实现支持中任意2.2 环境搭建与依赖安装先建虚拟环境把依赖锁在 requirements 里。注意 Scrapy-Redis 对 Redis 版本有要求低于 5.0 的 Redis 在大量指纹写入时性能下降明显。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install scrapy scrapy-redis redis pymysql pandas pip freeze requirements.txt逻辑说明scrapy 是爬虫核心scrapy-redis 提供分布式调度组件redis 是 Python 客户端pymysql 负责落库pandas 留给后续清洗。参数上没有特殊配置但建议把 Redis 服务端单独部署不要和爬虫节点混部否则内存竞争会导致指纹写入延迟。2.3 Scrapy-Redis 核心配置项在 settings.py 里改四个地方这是分布式能不能跑起来的关键。# settings.py REDIS_HOST 127.0.0.1 REDIS_PORT 6379 REDIS_PARAMS {db: 0, password: } # 调度器换成 Redis 队列 SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter SCHEDULER_PERSIST True # 断点续爬的关键False 则每次清空队列 # 请求队列用优先队列方便给不同接口设优先级 SCHEDULER_QUEUE_CLASS scrapy_redis.queue.PriorityQueue # 下载延迟视频平台接口建议不低于 1.5 秒 DOWNLOAD_DELAY 1.5 RANDOMIZE_DOWNLOAD_DELAY True CONCURRENT_REQUESTS_PER_DOMAIN 4逻辑说明SCHEDULER_PERSIST True是断点续爬的开关设为 False 时爬虫重启会清空 Redis 里的请求队列之前排队的任务全丢。PriorityQueue让弹幕、评论这类高频接口和用户信息这类低频接口可以分优先级避免低频接口被高频接口饿死。DOWNLOAD_DELAY配合RANDOMIZE_DOWNLOAD_DELAY做随机延迟比固定延迟更难被识别为爬虫。注意CONCURRENT_REQUESTS_PER_DOMAIN不要超过 8实测超过 8 之后接口返回空数据的概率明显上升这是血泪经验。3. 核心数据字段的采集实现从视频详情到弹幕分片3.1 视频详情与互动数据的接口组织视频详情接口通常返回播放量、点赞、收藏、分享、投币等聚合字段但评论和弹幕是独立接口。我的做法是把一个视频拆成三类任务详情任务、评论任务、弹幕任务分别入队互不阻塞。import scrapy from scrapy_redis.spiders import RedisSpider class VideoDetailSpider(RedisSpider): name video_detail redis_key video:detail:start_urls def parse(self, response): data response.json() item { bvid: data[data].get(bvid), title: data[data].get(title), play: data[data][stat].get(view), like: data[data][stat].get(like), coin: data[data][stat].get(coin), favorite: data[data][stat].get(favorite), share: data[data][stat].get(share), reply: data[data][stat].get(reply), danmaku: data[data][stat].get(danmaku), owner_mid: data[data][owner].get(mid), } yield item # 详情拿到后把评论和弹幕任务推入队列 bvid item[bvid] self.server.lpush(video:comment:start_urls, fcomment:{bvid}) self.server.lpush(video:danmaku:start_urls, fdanmaku:{bvid})逻辑说明RedisSpider从 Redis 列表里取起始 URLparse里解析 JSON 后 yield item 落库同时把评论和弹幕任务推入各自的队列。参数上stat字段里的互动数据是聚合值不需要翻页owner.mid是后续抓用户信息的关联键必须存下来。3.2 弹幕分片采集与时间轴对齐弹幕接口通常按时间段分片返回一个视频的弹幕可能分散在多个分片里。常见做法是先请求一次拿到分片总数再按分片并发拉取。class DanmakuSpider(RedisSpider): name danmaku redis_key video:danmaku:start_urls def parse(self, response): bvid response.meta[bvid] data response.json() for d in data.get(elems, []): yield { bvid: bvid, time_offset: d[progress], # 毫秒弹幕出现时间 mode: d[mode], # 弹幕类型滚动/顶部/底部 font_size: d[fontsize], color: d[color], content: d[content], send_time: d[ctime], }逻辑说明progress是弹幕相对视频开头的毫秒偏移做时间轴分析时必须保留。mode区分弹幕类型做情感分析时滚动弹幕和底部弹幕的权重可以不同。参数上弹幕接口一般有分页需要在 parse 里判断是否还有下一页有则 yield 新 Request 并带上bvid到 meta。3.3 用户信息与关注列表的递归采集用户信息接口返回粉丝数、关注数、历史投稿数关注列表和历史投稿是分页接口。这里要控制递归深度否则容易从一个种子用户扩散到全站。class UserSpider(RedisSpider): name user redis_key user:info:start_urls max_depth 2 # 只递归两层避免无限扩散 def parse(self, response): data response.json() mid data[data][mid] yield { mid: mid, name: data[data][name], fans: data[data][fans], following: data[data][following], archive_count: data[data][archive_count], } depth response.meta.get(depth, 0) if depth self.max_depth: # 把关注列表里的用户推入队列深度加一 for f in data[data].get(following_list, []): self.server.lpush(user:info:start_urls, fuser:{f[mid]})逻辑说明max_depth是防止递归扩散的硬约束实测不设这个参数一个种子用户能在几小时内扩散出几十万任务。参数上fans和following是快照值做趋势分析需要定时重采建议用(mid, date)做联合主键。4. 数据落库与去重MySQL 表设计和 Redis 指纹的配合4.1 表结构设计视频、用户、弹幕、评论分四张主表互动数据用宽表存快照。CREATE TABLE video_snapshot ( bvid VARCHAR(20) NOT NULL, stat_date DATE NOT NULL, play INT DEFAULT 0, like_count INT DEFAULT 0, coin INT DEFAULT 0, favorite INT DEFAULT 0, share INT DEFAULT 0, reply INT DEFAULT 0, danmaku INT DEFAULT 0, PRIMARY KEY (bvid, stat_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE danmaku ( id BIGINT AUTO_INCREMENT PRIMARY KEY, bvid VARCHAR(20), time_offset INT, mode TINYINT, content VARCHAR(500), send_time INT, INDEX idx_bvid (bvid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明video_snapshot用(bvid, stat_date)联合主键同一天重复采集会覆盖而不是插入新行天然去重。danmaku表用自增主键因为弹幕内容可能重复不能用内容做唯一键。参数上content给 500 字符足够超长弹幕极少。4.2 Redis 指纹与 MySQL 去重的分工Scrapy-Redis 的RFPDupeFilter只对请求 URL 去重不保证数据不重复入库。我的做法是请求层用 Redis 指纹去重数据层用 MySQL 联合主键或INSERT IGNORE兜底。import pymysql def save_item(item): conn pymysql.connect(host127.0.0.1, userroot, password, dbbili_data, charsetutf8mb4) sql INSERT IGNORE INTO video_snapshot (bvid, stat_date, play, like_count, coin, favorite, share, reply, danmaku) VALUES (%s, CURDATE(), %s, %s, %s, %s, %s, %s, %s) with conn.cursor() as cur: cur.execute(sql, (item[bvid], item[play], item[like], item[coin], item[favorite], item[share], item[reply], item[danmaku])) conn.commit() conn.close()逻辑说明INSERT IGNORE在联合主键冲突时静默跳过避免重复采集导致数据膨胀。参数上CURDATE()让同一天多次采集只保留第一次的值如果要做日内趋势需要把stat_date换成DATETIME并调整主键。5. 避坑与排查五个让我返工三次的坑5.1 弹幕接口返回空数组但状态码 200现象弹幕接口返回{code:0,data:{elems:[]}}状态码 200但实际有弹幕。原因分片参数没传对或者该分片确实为空但代码没做空判断直接跳过。解决先请求一次拿分片总数再逐片拉取对空数组做日志记录统计空片比例超过 30% 说明分片参数有问题。5.2 Redis 内存暴涨导致爬虫卡死现象跑了几小时后爬虫节点全部卡住Redis 内存占用超过 80%。原因RFPDupeFilter的指纹集合只增不减千万级 URL 后内存占用很大。解决给 Redis 设maxmemory-policy allkeys-lru或者定期用脚本清理已完成任务的指纹。更稳妥的做法是把指纹单独放一个 Redis 实例和任务队列隔离。5.3 评论接口翻页到后面返回重复数据现象评论翻页到第 20 页后返回的数据和前面几页重复。原因评论接口的游标是基于时间戳的翻页过程中有新评论插入会导致游标偏移。解决记录每次翻页的最后一个评论 ID 作为游标而不是用页码同时接受一定程度的重复靠 MySQL 唯一键去重。5.4 用户信息接口触发风控返回 412现象连续请求用户信息接口后返回 412 状态码持续几十分钟。原因请求频率过高且 User-Agent 固定。解决DOWNLOAD_DELAY调到 2 秒以上User-Agent 池至少准备 20 个配合RANDOMIZE_DOWNLOAD_DELAY。412 出现后立即暂停该接口任务等 30 分钟再恢复。5.5 分布式节点时钟不一致导致快照日期错乱现象不同节点写入的stat_date差一天同一天的数据被拆成两行。原因节点服务器时区或时钟不同步。解决所有节点统一用 UTC 时间落库时在应用层转换时区不要依赖数据库的CURDATE()。6. 从采集到分析用 Pandas 做互动率归因的一个具体技巧数据落库只是第一步真正有价值的是把互动数据做成可对比的指标。我常用的一个技巧是互动率归一化把点赞、收藏、分享分别除以播放量得到三个比率再用这三个比率做聚类比直接看绝对值靠谱得多。import pandas as pd df pd.read_sql(SELECT * FROM video_snapshot WHERE stat_date CURDATE(), conn) df[like_rate] df[like_count] / df[play].replace(0, 1) df[coin_rate] df[coin] / df[play].replace(0, 1) df[fav_rate] df[favorite] / df[play].replace(0, 1) # 按互动率中位数分四象限 like_med df[like_rate].median() fav_med df[fav_rate].median() df[quadrant] df.apply( lambda r: 高赞高藏 if r[like_rate] like_med and r[fav_rate] fav_med else 高赞低藏 if r[like_rate] like_med else 低赞高藏 if r[fav_rate] fav_med else 低赞低藏, axis1) print(df.groupby(quadrant)[play].agg([count, mean]))逻辑说明replace(0, 1)防止除零。四象限划分后高赞低藏通常对应情绪型内容高赞高藏对应工具型内容低赞高藏往往是收藏夹吃灰型。参数上中位数比均值更抗极端值如果数据量小于 1000 条建议改用四分位数。验证方法很简单拿同一分区一周的数据跑一遍看四象限的播放量均值是否符合直觉。如果低赞低藏的播放量反而最高说明播放量有刷量嫌疑需要回到采集层检查是否有异常账号。我自己踩过最深的坑是早期没做互动率归一化直接按播放量排序做内容分析结果被几个刷量视频带偏了整整一周的结论。后来养成习惯任何聚合指标先做比率化再看分布最后才看绝对值。希望帮到你。本文还有配套的精品资源点击获取