抖音数据采集方案研究:从 API 逆向到 WebSocket 环境注入

1. 现状背景与痛点

在针对抖音平台进行视频播放量、评论等数据抓取时,传统的爬虫方案面临极其严峻的风控挑战:

  • API 协议高度加密 :核心接口(如 detailcomment/list)强制校验动态参数 msTokena_bogus
  • 算法迭代快:即便成功还原了旧版 JS 逆向逻辑,官方也会频繁更新加密算法导致代码失效。
  • 风控检测严:即使参数生成正确,若请求缺少真实的浏览器指纹或动态维护的 Cookie,依然会被拦截返回空数据或触发人机验证。

2. 核心思路:WebSocket 环境中继方案

与其费力去还原复杂的加密算法,不如**"借力打力"**。

通过建立一个 WebSocket (WS) 通道,将后端爬虫逻辑与真实的浏览器环境连接起来。利用浏览器原生环境自动补全加密参数和状态,实现"无感知"的数据抓取。

方案优势

  1. 避开逆向难题 :直接在浏览器内发起请求,由浏览器原生 JS 自动生成 a_bogus 等加密字段,无需手动还原。
  2. 原生状态保持:请求自动携带当前浏览器的真实 Cookie,解决了登录态失效和指纹检测问题。
  3. 多节点扩展:支持通过一个后端服务端连接多个浏览器(多账号、多设备),实现任务的统一分发与结果聚合。

3. 技术实现

3.1 服务端 (Python)

使用 websockets 库搭建中控台,负责任务下发和数据接收。

python 复制代码
import asyncio
import websockets
import json
import logging

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

async def server_handler(websocket, path):
    logger.info("📡 浏览器客户端已连接")
    # 模拟任务分发逻辑
    # await websocket.send(json.dumps({"type": "get_detail", "aweme_id": "71234567890"}))
    
    async for message in websocket:
        data = json.loads(message)
        if data['type'] in ["detail_result", "comments_result"]:
            # 处理回传的数据
            logger.info(f"✅ 收到数据回传 | 类型: {data['type']} | ID: {data['aweme_id']}")
            # 在此处编写存库逻辑(如写入 MongoDB/MySQL)
            # save_data(data['payload'])

async def main():
    # 启动 WebSocket 服务
    async with websockets.serve(server_handler, "127.0.0.1", 8765):
        logger.info("🚀 自动监控分发服务已启动,等待浏览器连接...")
        await asyncio.Future()  # 永久运行

if __name__ == "__main__":
    asyncio.run(main())

客户端注入脚本 (JavaScript)

js 复制代码
(function() {
    const WS_URL = "ws://127.0.0.1:8765";
    let socket;

    function connect() {
        socket = new WebSocket(WS_URL);
        
        socket.onopen = () => {
            console.log("%c✅ 浏览器全自动代发服务已就绪", "color: #2ecc71; font-weight: bold;");
        };

        socket.onmessage = (event) => {
            const task = JSON.parse(event.data);
            const commonParams = `device_platform=webapp&aid=6383&channel=channel_pc_web&pc_client_type=1&version_code=190500&browser_name=Chrome&browser_version=144.0.0.0`;
            let url = "";

            // 根据任务类型构造 URL
            if (task.type === "get_detail") {
                url = `https://www.douyin.com/aweme/v1/web/aweme/detail/?${commonParams}&aweme_id=${task.aweme_id}`;
            } else if (task.type === "get_comments") {
                url = `https://www.douyin.com/aweme/v1/web/comment/list/?${commonParams}&aweme_id=${task.aweme_id}&cursor=${task.cursor || 0}&count=20`;
            }

            if (url) {
                const xhr = new XMLHttpRequest();
                xhr.open("GET", url, true);
                xhr.withCredentials = true; // 关键:自动携带当前域名的原生 Cookie
                xhr.setRequestHeader("accept", "application/json, text/plain, */*");
                
                xhr.onreadystatechange = function() {
                    if (xhr.readyState === 4 && xhr.status === 200) {
                        // 将获取的 JSON 数据通过 WebSocket 回传给服务端
                        socket.send(JSON.stringify({
                            type: task.type === "get_detail" ? "detail_result" : "comments_result",
                            aweme_id: task.aweme_id,
                            payload: JSON.parse(xhr.responseText)
                        }));
                    }
                };
                xhr.send();
            }
        };

        socket.onclose = () => {
            console.warn("🔌 连接断开,3秒后尝试重连...");
            setTimeout(connect, 3000);
        };
    }

    connect();
})();

通过这种 Socket 服务模式,我们成功避开了复杂的反爬算法分析。它将 数据逻辑(Python)与环境渲染(浏览器) 完美解耦:

Python 负责:任务调度、频率控制、数据清洗、入库。

浏览器负责:解决签名、携带 Cookie、绕过环境检测。

这种方式非常适合小规模的高质量数据采集,且由于请求发自真实浏览器,被封禁的风险显著降低。