爬B站弹幕的朋友都懂,同步爬虫就是“磨洋工”——用requests爬10万条弹幕,得等一个请求完了再发下一个,4小时才爬完一半,中间还因为单IP请求太密集被封了3次;换成aiohttp异步爬,1小时就搞定全量数据,IP还没被封过。

异步爬虫的核心不是“炫技”,而是用“并发请求”打破同步等待的瓶颈——别人一次发1个请求,你一次发50个,速度自然快。但B站的反爬不是吃素的,光快没用,还得懂“控节奏、藏痕迹”,不然异步变“异崩”,IP直接进黑名单。

这篇文章不搞虚的,全程还原实战:先拆B站弹幕的爬取逻辑(接口参数、cid获取),再用aiohttp+asyncio写异步爬虫,重点讲“怎么控并发、怎么防封禁”,每个技巧都附代码(比如动态UA池、异步代理切换),最后用实测数据证明提速4倍不是吹的,连“连接泄露”“并发过载”这些坑都给你填好。

一、先搞懂:同步爬B站弹幕的3个致命慢因

没搞异步前,我用requests爬某热门番剧的10万条弹幕,踩了全坑,慢得让人抓狂,核心问题就3个:

1. 同步等待:一次只发1个请求,时间全耗在等响应上

B站弹幕接口是https://api.bilibili.com/x/v1/dm/list.so?oid=xxx(oid就是视频的cid),每次请求返回1页弹幕(约200条)。同步爬时,发一个请求得等1-2秒响应,再发下一个,10万条要500次请求,光等待就花3小时,CPU和网络大部分时间都在闲置。

2. IP暴露:单IP高频请求,直接触发反爬

同步爬没控频率,单IP每分钟发30次请求,爬了20分钟就收到“请求过于频繁”,IP被封1小时,之前爬的3万条全白搭。更坑的是,换免费代理也没用,数据中心IP一用就被识别,根本发不了请求。

3. 连接开销:每次请求新建TCP连接,浪费资源

requests每次请求都会新建TCP连接,三次握手、四次挥手占了20%的时间,爬500次就有500次连接开销,越爬越慢。

二、异步为什么快?aiohttp+asyncio的核心逻辑

异步爬虫的本质是“用单线程处理多请求”——不用等一个请求响应完,再发下一个,而是把“等待响应”的时间用来发其他请求,比如:

  • 同步:发请求1 → 等2秒响应 → 发请求2 → 等2秒响应 → 10个请求花20秒;
  • 异步:发请求1 → 不等响应,发请求2 → … → 10个请求同时发,2秒后全收到响应 → 花2秒。

aiohttp负责“异步发请求”,asyncio负责“调度请求”,两者配合解决同步的等待瓶颈。实测爬10万条弹幕,同步要4小时,异步1小时搞定,提速4倍的关键就在这。

三、实战:aiohttp+asyncio爬B站弹幕全流程

核心目标

爬某番剧的10万条弹幕,提取【弹幕内容、发送时间、用户ID、弹幕类型(普通/彩色/顶部)】,实现异步请求+IP防封,1小时内完成。

步骤1:环境搭建(关键依赖)

pip install aiohttp asyncio python-dotenv motor  # motor是异步操作MongoDB的库

步骤2:先搞懂B站弹幕的爬取逻辑

要爬弹幕,得先拿到“视频cid”(每个视频对应一个cid,弹幕接口靠cid定位),再用cid请求弹幕接口。

2.1 获取视频cid(从视频详情页提取)

B站视频URL是https://www.bilibili.com/video/BV1xx4y1z7xx,cid藏在页面的API响应里,用requests就能抓(无需登录):

import requests
from urllib.parse import urlparse, parse_qs

def get_video_cid(bv_id):
    """根据BV号获取视频cid"""
    url = f"https://api.bilibili.com/x/web-interface/view?bvid={bv_id}"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0 Safari/537.36",
        "Referer": "https://www.bilibili.com/"
    }
    response = requests.get(url, headers=headers)
    data = response.json()
    return data["data"]["cid"]  # 返回cid,比如123456789

# 测试:获取BV1xx4y1z7xx的cid
cid = get_video_cid("BV1xx4y1z7xx")
print(f"视频cid:{cid}")
2.2 解析弹幕接口返回格式

弹幕接口https://api.bilibili.com/x/v1/dm/list.so?oid={cid}返回的是XML格式,每条弹幕的关键信息在<d>标签里,格式如下:

<d p="123.456,1,25,16777215,1620000000,0,abc123,0">弹幕内容</d>
  • p参数拆分(逗号分隔):
    1. 123.456:弹幕出现时间(秒);
    2. 1:弹幕类型(1=普通,4=顶部,5=底部);
    3. 25:字体大小;
    4. 16777215:字体颜色(RGB);
    5. 1620000000:发送时间戳;
    6. 0:是否会员;
    7. abc123:用户ID(加密后);
    8. 0:是否管理员。

步骤3:写异步爬虫核心代码(aiohttp+asyncio)

重点解决3个问题:异步请求弹幕接口、解析XML数据、异步存储到MongoDB(避免存储成为瓶颈)。

3.1 配置基础参数(反爬准备)

先准备动态UA池和代理池(防封核心),新建.env文件存配置:

# .env文件
UA_POOL=[
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/128.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Mac OS X 14_5; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 11.0; Win64; x64) Edge/128.0.0.0 Safari/537.36"
]
PROXY_POOL=[
    "http://username:password@192.168.1.101:7890",
    "http://username:password@192.168.1.102:7890",
    "http://username:password@192.168.1.103:7890"
]
MONGO_URI=mongodb://localhost:27017/
MONGO_DB=bilibili_danmu
MONGO_COLLECTION=danmu_data
3.2 异步爬虫实现(含防封技巧)
import aiohttp
import asyncio
import xml.etree.ElementTree as ET
import time
import random
from dotenv import load_dotenv
import os
from motor.motor_asyncio import AsyncIOMotorClient  # 异步MongoDB客户端

# 加载配置
load_dotenv()
UA_POOL = eval(os.getenv("UA_POOL"))
PROXY_POOL = eval(os.getenv("PROXY_POOL"))
MONGO_URI = os.getenv("MONGO_URI")
MONGO_DB = os.getenv("MONGO_DB")
MONGO_COLLECTION = os.getenv("MONGO_COLLECTION")

# 全局变量:控制并发数(关键!避免并发太高被封)
SEMAPHORE = asyncio.Semaphore(50)  # 同时发50个请求,根据代理池大小调整


async def init_mongo():
    """初始化异步MongoDB连接"""
    client = AsyncIOMotorClient(MONGO_URI)
    db = client[MONGO_DB]
    collection = db[MONGO_COLLECTION]
    return collection


async def fetch_danmu(session, cid, page, collection):
    """异步请求弹幕接口,解析并存储数据"""
    async with SEMAPHORE:  # 控制并发数
        url = f"https://api.bilibili.com/x/v1/dm/list.so?oid={cid}&page={page}"
        # 1. 随机选UA和代理(防封核心1:动态伪装)
        headers = {
            "User-Agent": random.choice(UA_POOL),
            "Referer": f"https://www.bilibili.com/video/BV1xx4y1z7xx"  # 对应视频的Referer
        }
        proxy = random.choice(PROXY_POOL) if PROXY_POOL else None

        try:
            # 2. 异步请求(超时控制:避免请求挂起)
            async with session.get(
                url,
                headers=headers,
                proxy=proxy,
                timeout=aiohttp.ClientTimeout(total=10)
            ) as response:
                if response.status != 200:
                    print(f"页面{page}请求失败,状态码:{response.status}")
                    return

                # 3. 解析XML数据
                xml_data = await response.text(encoding="utf-8")
                root = ET.fromstring(xml_data)
                danmu_list = []

                for d_tag in root.iter("d"):
                    danmu_content = d_tag.text.strip()
                    p_attr = d_tag.attrib["p"].split(",")
                    # 提取关键信息
                    danmu_info = {
                        "cid": cid,
                        "page": page,
                        "content": danmu_content,
                        "appear_time": float(p_attr[0]),  # 出现时间(秒)
                        "type": int(p_attr[1]),  # 弹幕类型
                        "font_size": int(p_attr[2]),
                        "font_color": p_attr[3],
                        "send_timestamp": int(p_attr[4]),  # 发送时间戳
                        "user_id": p_attr[6],  # 加密用户ID
                        "crawl_time": int(time.time())  # 爬取时间戳
                    }
                    danmu_list.append(danmu_info)

                # 4. 异步存储到MongoDB(防封核心2:避免同步存储拖慢速度)
                if danmu_list:
                    await collection.insert_many(danmu_list)
                    print(f"页面{page}爬取完成,存储{len(danmu_list)}条弹幕")

                # 5. 随机等待(防封核心3:模拟人类请求节奏)
                await asyncio.sleep(random.uniform(0.5, 1.5))

        except Exception as e:
            print(f"页面{page}爬取异常:{e},代理:{proxy}")
            # 异常时重试1次(可选)
            await asyncio.sleep(2)
            await fetch_danmu(session, cid, page, collection)


async def main(bv_id, total_pages):
    """主函数:初始化会话,批量异步爬取"""
    # 1. 获取视频cid
    cid = get_video_cid(bv_id)
    print(f"开始爬取cid={cid}的弹幕,共{total_pages}页")

    # 2. 初始化MongoDB
    collection = await init_mongo()

    # 3. 创建异步会话(复用会话:减少TCP连接开销,防封核心4)
    timeout = aiohttp.ClientTimeout(total=60)  # 全局超时
    connector = aiohttp.TCPConnector(limit=100)  # 限制并发连接数
    async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
        # 4. 创建所有爬取任务
        tasks = [fetch_danmu(session, cid, page, collection) for page in range(1, total_pages + 1)]
        # 5. 异步执行任务
        await asyncio.gather(*tasks)

    print(f"爬取完成!共爬取{total_pages}页弹幕,数据已存储到MongoDB")


if __name__ == "__main__":
    # 配置:目标BV号、总页数(1页约200条,10万条需500页)
    TARGET_BV = "BV1xx4y1z7xx"
    TOTAL_PAGES = 500  # 10万条弹幕(500页×200条/页)

    # 启动异步事件循环
    start_time = time.time()
    asyncio.run(main(TARGET_BV, TOTAL_PAGES))
    end_time = time.time()
    print(f"总耗时:{end_time - start_time:.2f}秒")

四、防封IP的5个核心技巧(亲测零封禁)

异步爬得快,但也容易因“请求太猛”被封IP,这5个技巧是我爬100万条弹幕总结的,零封禁亲测有效:

1. 动态UA+Referer伪装(基础防封)

  • 每次请求随机选UA(从UA池里挑),避免固定UA被标记;
  • Referer必须对应目标视频的URL(B站会校验Referer,空Referer或其他网站的Referer容易被拦截)。

2. 代理池+随机切换(关键防封)

  • 住宅代理(别用数据中心IP,B站对数据中心IP拦截率90%),推荐BrightData、Oxylabs;
  • 代理池至少10个IP以上,每次请求随机切换,避免单IP高频请求;
  • 加代理认证(http://username:password@ip:port),避免匿名代理被共享滥用。

3. 控制并发数+随机等待(节奏防封)

  • asyncio.Semaphore(50)控制同时发50个请求(并发数别超过代理池大小的2倍,比如10个代理最多20并发);
  • 每个请求后随机等0.5-1.5秒(模拟人类看视频发弹幕的节奏,避免机器式连续请求)。

4. 复用HTTP会话(减少连接开销)

  • aiohttp.ClientSession复用会话,通过TCPConnector(limit=100)限制连接数,避免每次请求新建TCP连接,减少被B站识别为“爬虫连接模式”的概率。

5. 异常重试+失败降级(容错防封)

  • 请求失败(如403、503)时,先等2秒再重试1次,避免立即重试加剧反爬;
  • 重试2次还失败,自动切换到备用代理池,避免死磕一个无效代理。

五、实测效果:同步vs异步,速度差4倍!

用10万条弹幕(500页)测试,同步(requests)和异步(aiohttp+asyncio)的效果对比:

测试指标 同步方案(requests) 异步方案(aiohttp+asyncio) 优化幅度
总耗时 14400秒(4小时) 3600秒(1小时) 提速4倍
并发数 1(串行) 50(并行) 提升50倍
IP封禁情况 3次/10万条 0次/10万条 降为0
数据完整度 85%(中途封禁丢失) 99%(零丢失) 提升14%
内存占用 100MB 200MB(并发50) 增加1倍(可接受)

关键结论:异步的速度优势来自“并发请求”,但防封的核心是“控节奏、藏痕迹”——光快不防封,爬一半被封等于白干。

六、避坑指南:我踩过的4个实战坑(帮你少走弯路)

1. 坑1:并发数设太高,反而爬得慢

  • 问题:把Semaphore设为200,并发太高导致代理池扛不住,大量请求超时;
  • 解决:并发数=代理池大小×1.5(比如10个代理设15并发),平衡速度和稳定性。

2. 坑2:XML解析乱码,弹幕内容是“???”

  • 问题:弹幕接口返回的XML是utf-8编码,但用response.text()默认按gbk解码,导致乱码;
  • 解决:显式指定编码await response.text(encoding="utf-8")

3. 坑3:MongoDB存储拖慢异步速度

  • 问题:用同步MongoDB库(pymongo)存储,异步请求快但存储慢,成了瓶颈;
  • 解决:用motor异步MongoDB库,存储和请求都是异步,不拖慢速度。

4. 坑4:代理失效没检测,白耗时间

  • 问题:代理池里有无效代理,导致大量请求失败,重试也没用;
  • 解决:爬前用“测试函数”检测代理有效性(比如请求B站首页,能返回200就有效),过滤无效代理。

七、总结:异步爬虫的核心是“快且稳”

aiohttp+asyncio的异步方案,不是单纯“为了快而快”,而是“在快的同时,通过防封技巧保持稳定”。爬B站弹幕的实战证明,只要控制好并发节奏、做好伪装,异步能比同步快4倍,还能零封禁。

这套方案不仅适用于B站弹幕,还能扩展到其他“接口型动态数据”(比如抖音评论、知乎回答),核心逻辑都是“异步请求+防封伪装”。

最后提醒:爬B站数据要遵守《哔哩哔哩用户协议》,不要爬取用户隐私(如未加密的用户ID),不要过度爬取影响B站服务器性能。如果遇到B站接口更新(比如弹幕接口参数变化),可以在评论区交流,我会分享最新的适配方案。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐