Python爬虫实战:B站弹幕与QQ音乐热评抓取及反爬优化

📅 发布时间:2026/10/9 17:37:30
Python爬虫实战:B站弹幕与QQ音乐热评抓取及反爬优化
1. 从两个爬虫项目说起为什么弹幕和热评值得单独做弹幕和热评看起来都是“文字”但真正抓过的人都知道这两类数据的脾气完全不一样。B站的弹幕是高并发、短生命周期、强时序的数据流一条弹幕可能只有几个字但同一秒内会有几十上百条涌进来QQ音乐的热评则是低频、长文本、强互动的数据一条评论可能几百字点赞数从个位数到几十万不等。把这两个场景放在一起做其实是一个很聪明的选择——它逼着你把爬虫的两套核心能力都练一遍一套是协议逆向与高频请求另一套是动态渲染与结构化提取。我自己最早做这类项目的时候踩过的坑能写满一页纸。比如B站的弹幕接口早期是明文XML后来改成protobuf压缩再后来又加了各种校验参数QQ音乐的热评接口则长期依赖动态token和请求签名直接拿浏览器里复制的URL去请求十有八九返回的是空数据或者“参数错误”。所以这篇文章不会只给你两段能跑的代码而是把为什么这么写、参数怎么来的、哪里容易翻车讲清楚。适合已经会一点Python基础、想从“能跑通”进阶到“跑得稳”的人也适合完全没接触过爬虫、但愿意跟着一步步操作的新手。核心关键词我先摆出来python爬虫、bilibili弹幕、qq音乐热评、requests、scrapy。后面所有内容都围绕这几个词展开不跑题。2. 整体方案设计为什么我选requests打底、scrapy做扩展2.1 两个场景的技术选型对比很多人一上来就问“用scrapy还是requests”其实这个问题本身就不太对。工具没有优劣只有合不合适。我先把两个场景的特征拆开看维度B站弹幕QQ音乐热评数据量级单视频几千到几十万条单歌曲几百到几万条请求频率极高需要分片拉取中等分页拉取数据格式protobuf压缩二进制JSON明文反爬强度中等靠参数校验较高靠签名和token是否需要登录部分接口需要大部分需要适合框架requests 多线程requests 会话保持看这张表就能明白B站弹幕的核心矛盾是吞吐量QQ音乐热评的核心矛盾是身份校验。所以我的方案是——两个项目都用requests打底因为requests对会话、header、cookie的控制最直接调试成本最低。等你把单机跑通了再考虑用scrapy做分布式扩展那是后话。提示不要一上来就上scrapy。scrapy的调试曲线比requests陡得多尤其是遇到需要手动构造签名参数的接口时scrapy的中间件机制反而会增加排查难度。先用requests把逻辑跑通再迁移。2.2 为什么不用selenium或playwright热搜词里出现了“scrapy playwright 动态 iframe”说明很多人第一反应是上浏览器自动化。我的建议是能抓接口就别渲染页面。原因有三点。第一浏览器自动化的资源消耗是纯请求的几十倍。一个chromium实例起步就是几百MB内存你开十个并发机器直接卡死。第二动态渲染的稳定性极差页面改一个class名你的选择器就全废了。第三B站和QQ音乐的核心数据都是通过XHR接口返回的页面只是渲染层直接抓接口拿到的数据更干净、更完整。那什么时候才需要playwright只有当接口的签名算法复杂到无法逆向或者数据确实只存在于DOM里的时候。这两个项目都不属于这种情况。2.3 项目整体架构我把整个项目拆成四个模块每个模块独立可测请求层封装session、header管理、重试逻辑、代理池可选解析层B站弹幕用protobuf解析QQ音乐热评用JSON解析存储层先落CSV再考虑SQLite或MongoDB调度层单机用线程池分布式再上scrapy-redis这个分层的好处是任何一层出问题你都能快速定位。比如弹幕抓下来是乱码那肯定是解析层的问题如果请求直接403那就是请求层的header或cookie没配对。3. B站弹幕爬取从接口逆向到protobuf解析3.1 弹幕接口的定位与参数拆解B站弹幕的核心接口藏在视频播放页的XHR请求里。你打开一个视频按F12进Network面板筛选“segment”或者“dm”就能看到类似这样的请求https://api.bilibili.com/x/v2/dm/web/seg.so?type1oid视频cidpid视频aidsegment_index1这里有几个关键参数必须搞清楚oid视频的cid注意不是aid。aid是视频的av号cid是弹幕池的编号两者不一样。获取cid的方法是请求https://api.bilibili.com/x/player/pagelist?aidxxx返回的JSON里每个分P都有cid。pid就是aid视频的av号。segment_index弹幕分片索引。B站的弹幕是按6分钟一片切分的一个视频有多少片取决于视频时长。比如一个60分钟的视频就有10片segment_index从1到10。type固定为1表示视频弹幕。注意这个接口返回的是protobuf格式的二进制数据不是JSON。你直接用浏览器打开会看到一堆乱码这是正常的。3.2 获取cid的完整流程很多人卡在第一步——不知道cid怎么来。我写一段可直接跑的代码import requests def get_cid(aid): url fhttps://api.bilibili.com/x/player/pagelist?aid{aid} headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: fhttps://www.bilibili.com/video/av{aid} } resp requests.get(url, headersheaders) data resp.json() if data[code] ! 0: raise Exception(f获取cid失败: {data[message]}) # 返回第一个分P的cid多P视频需要遍历 return data[data][0][cid]这段代码里Referer是必须的。B站的接口对Referer校验很严不带的话大概率返回-400。User-Agent也要伪装成正常浏览器否则会被识别为爬虫。3.3 protobuf解析为什么不能直接当文本读弹幕接口返回的数据是protobuf序列化后的二进制。protobuf是Google搞的一种高效数据交换格式优点是体积小、解析快缺点是必须有对应的.proto定义文件才能解析。B站弹幕的.proto结构大致是这样的社区逆向出来的message DanmakuElem { int64 id 1; int32 progress 2; // 弹幕出现时间毫秒 int32 mode 3; // 弹幕类型 int32 fontsize 4; uint32 color 5; string midHash 6; string content 7; // 弹幕内容 int64 ctime 8; int32 weight 9; string action 10; int32 pool 11; string idStr 12; }解析的时候你需要用protobuf库把二进制数据反序列化成对象。如果你不想自己编译.proto文件也可以用社区维护的bilibili-api或者dmproto这类库直接调用现成的解析函数。我个人的做法是自己编译一份.proto因为这样最可控不依赖第三方库的更新。编译命令是protoc --python_out. danmaku.proto生成的danmaku_pb2.py就可以直接import使用了。3.4 分片拉取与并发控制一个视频的弹幕可能分散在几十个分片里串行拉取太慢。我的做法是用ThreadPoolExecutor做并发但并发数要控制好。from concurrent.futures import ThreadPoolExecutor import requests def fetch_segment(cid, aid, index): url https://api.bilibili.com/x/v2/dm/web/seg.so params { type: 1, oid: cid, pid: aid, segment_index: index } headers { User-Agent: Mozilla/5.0 ..., Referer: fhttps://www.bilibili.com/video/av{aid} } resp requests.get(url, paramsparams, headersheaders) return resp.content def fetch_all_segments(cid, aid, total_segments): results [] with ThreadPoolExecutor(max_workers5) as executor: futures [executor.submit(fetch_segment, cid, aid, i) for i in range(1, total_segments 1)] for f in futures: results.append(f.result()) return results并发数建议控制在3到5之间。我试过开到10结果触发了B站的限流连续返回412错误。412是B站的风控状态码一旦触发你的IP会被临时封禁一段时间。所以宁可慢一点也不要贪快。3.5 弹幕数据的清洗与存储原始弹幕数据里有很多噪音比如重复弹幕、纯表情、广告。我一般会做几层过滤去掉长度小于2的弹幕去掉包含URL的弹幕用collections.Counter统计重复弹幕出现次数超过阈值的可能是刷屏存储方面先用CSV最省事import csv def save_danmaku(danmaku_list, filename): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([时间(ms), 内容, 发送者hash, 颜色]) for d in danmaku_list: writer.writerow([d.progress, d.content, d.midHash, d.color])注意encodingutf-8-sig这样用Excel打开不会乱码。这个细节很小但能省你很多事。4. QQ音乐热评爬取签名、token与分页策略4.1 热评接口的定位QQ音乐的热评数据在歌曲详情页的评论区。打开一首歌F12筛选XHR找comment相关的请求你会看到类似https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg?g_tkxxxsongidxxxpagenum0pagesize25这个接口返回JSON但有几个坑g_tk这是一个基于cookie计算的签名参数不是固定的。songid歌曲的mid不是数字ID。pagenum从0开始不是1。pagesize每页条数最大25。4.2 g_tk的计算逻辑g_tk是QQ系产品通用的签名算法核心逻辑是从cookie里取skey或p_skey经过一次哈希运算得到。具体算法是def get_g_tk(skey): hash_val 5381 for c in skey: hash_val (hash_val 5) ord(c) return hash_val 0x7fffffff这个算法看起来简单但skey必须是从登录态cookie里取的。如果你没登录拿到的skey是空的算出来的g_tk就是5381请求会返回“未登录”错误。所以QQ音乐热评爬取的第一步是手动登录一次把cookie导出。我一般用浏览器插件导出cookie存成JSON然后在代码里加载。4.3 会话保持与请求头构造QQ音乐对请求头的校验比B站更细。除了User-Agent和Referer还要注意Referer必须是https://y.qq.com/开头Cookie必须包含完整的登录态Origin部分接口需要我用requests.Session来保持会话import requests session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Referer: https://y.qq.com/, }) # 加载cookie cookies load_cookies_from_json(qq_cookies.json) session.cookies.update(cookies)提示cookie是有有效期的一般几天到几周。如果突然开始返回空数据第一件事就是检查cookie是否过期。4.4 分页拉取与去重QQ音乐热评的分页逻辑是pagenum从0开始每次加1直到返回的评论列表为空。但实际测试下来热门歌曲的评论可能超过几千页全量拉取不现实。我的策略是只拉前50页约1250条按点赞数排序取Top 500用评论ID去重def fetch_comments(songid, g_tk, max_pages50): all_comments [] seen_ids set() for page in range(max_pages): url https://c.y.qq.com/base/fcgi-bin/fcg_global_comment_h5.fcg params { g_tk: g_tk, songid: songid, pagenum: page, pagesize: 25, cmd: 6, reqtype: 2 } resp session.get(url, paramsparams) data resp.json() comments data.get(comment, {}).get(commentlist, []) if not comments: break for c in comments: cid c.get(commentid) if cid not in seen_ids: seen_ids.add(cid) all_comments.append(c) return all_comments4.5 热评数据的结构化提取QQ音乐评论的JSON结构比较深关键字段有字段含义commentid评论唯一IDrootcommentcontent评论正文praisenum点赞数nick昵称time时间戳replynum回复数提取的时候要注意rootcommentcontent里可能包含表情符号的转义字符需要做一次清洗。我一般用正则把[em]xxx[/em]这种标记去掉。5. 反爬对抗与稳定性优化5.1 请求频率控制这是最容易被忽视、也最容易翻车的地方。我的经验是B站弹幕单IP每秒不超过2个请求QQ音乐热评单IP每秒不超过1个请求超过这个频率短则返回412长则IP被封。控制频率最简单的方法是用time.sleep但更优雅的做法是用令牌桶算法import time class RateLimiter: def __init__(self, rate): self.rate rate self.tokens rate self.last_time time.time() def acquire(self): now time.time() self.tokens (now - self.last_time) * self.rate self.tokens min(self.tokens, self.rate) self.last_time now if self.tokens 1: time.sleep((1 - self.tokens) / self.rate) self.tokens 0 else: self.tokens - 15.2 重试与异常处理网络请求失败是常态关键是要有分级重试策略网络超时立即重试最多3次429/412等待指数退避时间后重试403检查header和cookie不盲目重试def request_with_retry(url, params, max_retries3): for i in range(max_retries): try: resp session.get(url, paramsparams, timeout10) if resp.status_code 200: return resp elif resp.status_code in (412, 429): time.sleep(2 ** i) else: break except requests.RequestException: time.sleep(1) return None5.3 常见问题速查表问题现象可能原因解决方法返回412请求频率过高降低并发加sleep返回-400Referer缺失或错误补全Referer弹幕乱码未做protobuf解析用.proto文件反序列化热评为空cookie过期重新导出cookieg_tk错误skey为空确认登录态分页重复未去重用commentid去重6. 从单机到分布式scrapy的接入时机6.1 什么时候该上scrapy单机requests跑得稳之后如果你需要同时抓取几千个视频的弹幕需要断点续爬需要多机协同那就可以考虑scrapy了。但要注意scrapy的Downloader Middleware和Spider Middleware需要重新设计尤其是cookie管理和签名参数的注入。6.2 scrapy-redis的简单接入scrapy-redis的核心是把请求队列放到redis里多个爬虫实例共享。配置很简单# settings.py SCHEDULER scrapy_redis.scheduler.Scheduler DUPEFILTER_CLASS scrapy_redis.dupefilter.RFPDupeFilter REDIS_URL redis://localhost:6379但实际用的时候去重逻辑要自己写因为scrapy-redis默认的去重是基于请求URL的而弹幕和热评的URL可能相同但参数不同。6.3 数据存储的扩展单机用CSV没问题分布式就要上数据库了。我的建议是弹幕数据MongoDB因为字段不固定写入快热评数据MySQL或PostgreSQL因为需要做统计分析7. 实操心得与避坑清单最后分享几条我踩过坑之后总结的经验都是文档里不会写的。第一条不要用免费的代理IP池。我试过十几个免费代理源可用率不到5%而且很多是被目标网站标记过的。与其花时间维护代理池不如把请求频率降下来用单IP慢慢跑。第二条cookie要定期更新。QQ音乐的cookie一般7天左右失效B站的cookie如果只是游客态可能几小时就失效。我的做法是写一个定时任务每天检查一次cookie有效性失效就提醒手动更新。第三条弹幕的时间戳是毫秒不是秒。这个坑我踩过存到数据库里发现时间全是错的后来才发现要除以1000。第四条热评的点赞数会变。你今天抓的Top 100明天可能就变了。如果要做趋势分析必须记录抓取时间否则数据没有意义。第五条不要忽略robots.txt。虽然技术上可以绕过但从合规角度建议只抓取公开数据不要碰需要登录才能看的内容。这个边界要自己把握好。第六条代码要加日志。我早期写的爬虫没有日志出了问题只能靠print效率极低。后来改用logging模块把每个请求的URL、状态码、耗时都记下来排查问题快了很多。第七条数据要备份。有一次我跑了三个小时的弹幕抓取结果程序崩溃数据全丢了。从那以后我改成每抓100条就写一次文件虽然慢一点但安全。这个项目后续还可以扩展的方向很多比如把弹幕做成词云、把热评做情感分析、把两个数据源做交叉分析。但那是另一个话题了先把爬取这一环做扎实后面的分析才有意义。