使用代理采集YouTube公开数据:完整实战指南

深入对比YouTube Data API v3配额限制与InnerTube端点采集,提供Python和Node.js住宅代理轮换代码,助力媒体分析与创作者经济研究大规模获取视频元数据、评论与字幕。

使用代理采集YouTube公开数据:完整实战指南

媒体分析团队和创作者经济研究者常常面临一个困境:YouTube 拥有海量公开数据,但官方 API 的配额限制让大规模采集几乎不可能。当你需要追踪数万个视频的观看趋势、批量采集评论线程、或监控广告投放时,YouTube Data API v3 的每日 10,000 配额单位很快就会耗尽。本文将详细解析如何通过 YouTube 内部的 InnerTube JSON 端点和住宅代理,合法合规地采集公开数据,填补官方 API 留下的空白。

法律与合规声明:本文仅讨论对公开可访问数据的合法采集。请尊重 YouTube 的服务条款(ToS)以及适用法律,包括美国的《计算机欺诈和滥用法》(CFAA)和欧盟的 GDPR。不得采集需要登录才能访问的数据,不得绕过付费墙或年龄限制,不得重新分发创作者的受版权保护内容(如完整字幕或视频内容)。如果 YouTube 提供了满足需求的官方 API,应优先使用。

YouTube Data API v3:何时够用,何时不够

YouTube Data API v3 是获取结构化 YouTube 数据的官方途径,对许多轻量级用例完全足够。但它的配额机制决定了其在大规模场景下的局限性。

配额与成本结构

每个 Google Cloud 项目默认获得每日 10,000 个配额单位。不同操作消耗的单位数量差异极大:

操作配额消耗每日上限(约)
search.list100 单位/次~100 次
videos.list1 单位/次~10,000 次
commentThreads.list1 单位/次~10,000 次
channels.list1 单位/次~10,000 次

问题显而易见:search.list 一次调用消耗 100 单位,一个项目每天只能执行约 100 次搜索。对于需要监控数千个关键词的媒体分析团队来说,这远远不够。虽然可以通过创建多个 Google Cloud 项目来扩展配额,但这增加了运维复杂度,且 Google 对滥用配额有检测机制。

API 够用的场景

  • 监控少量频道(<50)的视频发布和元数据更新
  • 获取特定视频的评论(数量在数千条以内)
  • 个人研究项目或原型验证
  • 需要官方 SLA 保障的生产环境

需要采集补充的场景

  • 评论线程大规模采集:API 的 commentThreads.list 有分页上限,且深度评论树的配额消耗极快
  • 早期趋势检测:需要高频轮询视频的实时观看数,API 配额无法支撑每分钟检查数千个视频
  • 广告监控:API 不提供广告相关数据,只有页面端才能看到广告投放信息
  • 字幕/转录文本批量获取:API 不直接提供字幕文本内容
  • 推荐算法研究:API 不暴露推荐列表和个性化内容

无需登录即可访问的 YouTube 数据

YouTube 的公开页面包含大量结构化数据,无需登录即可访问。理解哪些数据是公开的,是合规采集的前提:

  • 视频元数据:标题、描述、发布时间、时长、分类标签、缩略图
  • 观看统计:观看数、点赞数、评论数(实时更新,API 有延迟)
  • 频道页面:订阅者数、总观看数、频道描述、社交链接、频道标签
  • 评论线程:顶级评论和回复(通过 InnerTube 端点获取)
  • 字幕/转录:自动生成和手动上传的字幕文件(创作者设为公开的)
  • 推荐/相关视频:侧边栏推荐列表
  • 播放列表:公开播放列表的视频顺序和元数据

这些数据通过 YouTube 的内部 InnerTube API 以 JSON 格式返回,比解析 HTML 页面更可靠、更结构化。

InnerTube API 深度解析

InnerTube 是 YouTube 前端应用使用的内部 API,所有 YouTube 客户端(网页版、移动端、智能电视)都通过它获取数据。它返回的是结构化 JSON,而非渲染后的 HTML,这使得数据提取更加稳定。

关键端点

端点用途请求方法
/youtubei/v1/next视频页的评论、推荐视频、视频详情POST
/youtubei/v1/player视频播放详情、流信息、字幕列表POST
/youtubei/v1/browse浏览频道页面、播放列表POST
/youtubei/v1/search搜索视频、频道、播放列表POST

所有端点的基础 URL 为 https://www.youtube.com/youtubei/v1/{endpoint},某些端点可能需要附加查询参数如 ?key=...&prettyPrint=false

请求格式

InnerTube 端点需要特定的请求头和 JSON 请求体。关键头部包括:

{
  "Content-Type": "application/json",
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
  "X-YouTube-Client-Name": "1",
  "X-YouTube-Client-Version": "2.20240101",
  "Origin": "https://www.youtube.com",
  "Referer": "https://www.youtube.com/"
}

请求体通常包含 videoIdcontext 等字段。context.client 对象指定客户端类型和版本,YouTube 据此返回不同格式的数据。常用的客户端标识包括 WEB(网页版,clientName=1)和 ANDROID(安卓版,clientName=3)。

续页令牌机制

InnerTube 不使用传统的页码分页,而是通过续页令牌(continuation token)实现翻页:

  1. 首次请求返回第一批数据和一个 continuationEndpoint 对象
  2. 后续请求将令牌放入请求体的 continuation 字段
  3. 每次响应返回新的令牌,直到没有更多数据

这种机制意味着你必须顺序遍历——无法跳页。对于评论采集,这要求逐页请求直到达到所需深度。每个续页令牌通常对应约 20 条评论。

为什么大规模采集需要住宅代理

Google 对来自数据中心 IP 范围的请求有严格的检测和限制机制。当你从 AWS、GCP、Azure 等云服务商的 IP 发起大量请求时,几乎立即会触发验证码(CAPTCHA)或 429 Too Many Requests 错误。

数据中心 IP 的问题

  • IP 段可识别:Google 维护了主要云服务商的 IP 段数据库,ASN 归属一目了然
  • 触发频率极高:DC IP 通常在几十次请求后就会被标记,远低于住宅 IP 的容忍度
  • CAPTCHA 难以自动化解决:Google 的 reCAPTCHA v3 对自动化请求极为敏感,且持续验证会导致 IP 被彻底封锁

住宅代理的优势

住宅代理使用真实 ISP 分配的 IP 地址,请求看起来就像来自普通用户。这大幅降低了被标记的概率:

  • IP 来自真实住宅网络,不在 DC 黑名单中
  • 支持地理位置定位,匹配目标受众区域
  • 可轮换 IP,分散请求压力至不同子网
  • 会话粘滞功能确保同一任务使用同一 IP,避免异常跳转

对于 YouTube 数据采集,住宅代理几乎是大规模操作的必要条件。数据中心代理可用于低频、低风险的元数据查询,但一旦请求量上升,住宅代理是唯一可靠的选择。了解更多代理采集用例

Python 实战:使用住宅代理采集 YouTube 数据

以下代码示例使用 ProxyHat 住宅代理,演示三种常见的采集场景。所有示例均使用 gate.proxyhat.com:8080 作为 HTTP 代理网关。

1. 通过 InnerTube 获取视频元数据和评论

import requests
import json
import random
import time

# ProxyHat 住宅代理配置
PROXY_URL = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY_URL, "https": PROXY_URL}

HEADERS = {
    "Content-Type": "application/json",
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/120.0.0.0 Safari/537.36"
    ),
    "X-YouTube-Client-Name": "1",
    "X-YouTube-Client-Version": "2.20240101",
    "Origin": "https://www.youtube.com",
    "Referer": "https://www.youtube.com/",
}

def fetch_video_data(video_id: str) -> dict:
    """获取视频元数据和初始评论数据"""
    url = "https://www.youtube.com/youtubei/v1/next"
    payload = {
        "videoId": video_id,
        "context": {
            "client": {
                "clientName": "WEB",
                "clientVersion": "2.20240101",
                "hl": "en",
                "gl": "US",
            }
        },
    }
    resp = requests.post(
        url, json=payload, headers=HEADERS,
        proxies=PROXIES, timeout=30
    )
    resp.raise_for_status()
    return resp.json()

# 使用示例
data = fetch_video_data("dQw4g9WgXcQ")
print(json.dumps(data, indent=2)[:500])

2. 使用续页令牌遍历评论线程

def fetch_comments(video_id: str, max_pages: int = 10) -> list:
    """通过续页令牌逐页获取评论"""
    url = "https://www.youtube.com/youtubei/v1/next"
    comments = []
    continuation_token = None

    payload = {
        "videoId": video_id,
        "context": {
            "client": {
                "clientName": "WEB",
                "clientVersion": "2.20240101",
            }
        },
    }

    for page in range(max_pages):
        if continuation_token:
            payload = {
                "continuation": continuation_token,
                "context": {
                    "client": {
                        "clientName": "WEB",
                        "clientVersion": "2.20240101",
                    }
                },
            }

        resp = requests.post(
            url, json=payload, headers=HEADERS,
            proxies=PROXIES, timeout=30
        )
        data = resp.json()

        # 从响应中提取评论和续页令牌
        on_response = data.get("onResponseReceivedEndpoints", [])
        for endpoint in on_response:
            items = (
                endpoint.get("reloadContinuationItemsCommand", {})
                .get("continuationItems", [])
                or endpoint.get("appendContinuationItemsAction", {})
                .get("continuationItems", [])
            )
            for item in items:
                comment = (
                    item.get("commentThreadRenderer", {})
                    .get("comment", {})
                    .get("commentRenderer", {})
                )
                if comment:
                    comments.append({
                        "author": comment.get("authorText", {}).get("simpleText"),
                        "text": comment.get("contentText", {}).get("runs", [{}])[0].get("text", ""),
                        "likes": comment.get("voteCount", {}).get("simpleText", "0"),
                    })

        # 查找下一个续页令牌
        next_token = None
        for endpoint in on_response:
            cont_items = (
                endpoint.get("appendContinuationItemsAction", {})
                .get("continuationItems", [])
            )
            for ci in cont_items:
                token = (
                    ci.get("continuationItemRenderer", {})
                    .get("continuationEndpoint", {})
                    .get("continuationCommand", {})
                    .get("token")
                )
                if token:
                    next_token = token
                    break

        if not next_token:
            break
        continuation_token = next_token

        # 随机延迟,避免触发限制
        time.sleep(random.uniform(2, 5))

    return comments

# 使用示例
comments = fetch_comments("dQw4g9WgXcQ", max_pages=5)
print(f"采集到 {len(comments)} 条评论")

3. 使用 youtube-transcript-api 获取字幕

from youtube_transcript_api import YouTubeTranscriptApi
import os

# ProxyHat 住宅代理配置
PROXY_URL = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080"
os.environ["http_proxy"] = PROXY_URL
os.environ["https_proxy"] = PROXY_URL

def fetch_transcript(video_id: str, languages: list = None) -> list:
    """获取视频字幕/转录文本"""
    if languages is None:
        languages = ["en", "zh"]

    try:
        transcript = YouTubeTranscriptApi.get_transcript(
            video_id, languages=languages
        )
        return transcript
    except Exception as e:
        print(f"获取字幕失败: {e}")
        return []

# 使用示例
transcript = fetch_transcript("dQw4g9WgXcQ", languages=["en"])
for entry in transcript[:5]:
    print(f"[{entry['start']:.1f}s] {entry['text']}")

4. 带会话粘滞的代理轮换

对于需要保持会话一致性的场景(如逐页遍历评论),使用粘滞会话确保同一会话使用同一 IP,避免 IP 跳变触发风控:

import uuid

def get_sticky_proxy(session_id: str = None, country: str = "US") -> dict:
    """获取粘滞会话代理,同一 session_id 使用同一 IP"""
    if session_id is None:
        session_id = str(uuid.uuid4())[:8]

    proxy_url = (
        f"http://user-country-{country}-session-{session_id}"
        f":PASSWORD@gate.proxyhat.com:8080"
    )
    return {
        "http": proxy_url,
        "https": proxy_url,
        "session_id": session_id,
    }

# 同一视频的评论遍历使用同一会话
session = get_sticky_proxy(country="US")
resp = requests.post(
    "https://www.youtube.com/youtubei/v1/next",
    json=payload,
    headers=HEADERS,
    proxies={"http": session["http"], "https": session["https"]},
    timeout=30,
)

Node.js 示例:InnerTube 视频元数据采集

const axios = require("axios");
const { HttpsProxyAgent } = require("https-proxy-agent");

const PROXY_URL = "http://user-country-US:PASSWORD@gate.proxyhat.com:8080";
const agent = new HttpsProxyAgent(PROXY_URL);

const HEADERS = {
  "Content-Type": "application/json",
  "User-Agent":
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
    "AppleWebKit/537.36 (KHTML, like Gecko) " +
    "Chrome/120.0.0.0 Safari/537.36",
  "X-YouTube-Client-Name": "1",
  "X-YouTube-Client-Version": "2.20240101",
  Origin: "https://www.youtube.com",
  Referer: "https://www.youtube.com/",
};

async function fetchVideoData(videoId) {
  const payload = {
    videoId,
    context: {
      client: {
        clientName: "WEB",
        clientVersion: "2.20240101",
        hl: "en",
        gl: "US",
      },
    },
  };

  const res = await axios.post(
    "https://www.youtube.com/youtubei/v1/next",
    payload,
    { headers: HEADERS, httpsAgent: agent, timeout: 30000 }
  );
  return res.data;
}

// 使用示例
fetchVideoData("dQw4g9WgXcQ")
  .then((data) => console.log("请求成功,数据长度:", JSON.stringify(data).length))
  .catch((err) => console.error("请求失败:", err.message));

速率限制与指纹风险

即使使用住宅代理,不合理的请求模式仍会被检测。以下是关键的风险点和应对策略:

请求频率

  • 单个 IP 建议每分钟不超过 30-60 次请求
  • 评论遍历场景下,页间间隔 2-5 秒
  • 使用随机延迟(加抖动)而非固定间隔,模拟人类行为
  • 批量采集视频元数据时,每批 50-100 个视频后暂停 30-60 秒

浏览器指纹

Google 不仅检查 IP,还会分析请求的隐性特征:

  • TLS 指纹(JA3/JA4):Python 的 requests 库的 TLS 握手特征与浏览器不同。考虑使用 curl_cffitls_client 库来模拟浏览器的 TLS 指纹
  • 请求头顺序:浏览器发送的头部顺序是固定的,requests 库会按字母序排列——这个差异可被检测
  • HTTP/2 支持:现代浏览器使用 HTTP/2,而 requests 默认使用 HTTP/1.1。考虑使用 httpx 的 HTTP/2 支持
  • 行为模式:真实用户不会在短时间内访问数千个不相关的视频页面

IP 轮换策略对比

策略适用场景ProxyHat 配置风险等级
每请求轮换视频元数据批量采集默认轮换(不指定 session)
粘滞会话评论遍历、频道浏览user-session-{id}:pass
国家级定位区域特定内容采集user-country-{CC}:pass
城市级定位本地化广告监控user-country-US-city-nyc:pass

三种数据获取方式的对比

维度YouTube Data API v3InnerTube 端点采集youtube-transcript-api
数据覆盖视频、频道、评论、搜索视频、评论、推荐、频道、广告字幕/转录文本
配额限制每日 10,000 单位无官方限制,但受风控无官方限制,受风控
数据新鲜度有缓存延迟近乎实时依赖字幕可用性
稳定性高(有 SLA)中(内部 API 可变更)中(依赖页面结构)
合规风险可能违反 ToS可能违反 ToS
代理需求不需要大规模需要住宅代理批量需要住宅代理

何时使用官方 API 而非采集

采集并非万能药。在以下场景中,官方 API 仍然是更好的选择:

  • 数据量小:每日数百次请求即可满足需求
  • 需要官方保障:生产环境中需要 SLA 和稳定性保证
  • 搜索功能:API 的搜索结果比 InnerTube 更结构化、更可预测
  • 频道管理:上传、编辑、删除等写操作必须通过 API
  • 合规要求严格:企业级应用中需要确保完全合规

最佳实践是混合使用:用 API 处理低量高优先级请求,用采集补充 API 无法覆盖的大规模场景。查看 ProxyHat 定价了解住宅代理方案,或查看 全球代理节点覆盖范围。

伦理与法律考量

数据采集能力带来责任。以下是必须遵守的底线:

  • 尊重创作者所有权:不得重新分发采集到的字幕、视频内容或受版权保护的素材。字幕文本属于创作者的原创内容,仅可用于个人分析
  • 遵守服务条款:YouTube ToS 第 III 条明确禁止自动化数据采集,需评估法律风险并咨询法律顾问
  • 数据最小化:只采集业务必需的数据,不囤积、不超范围采集
  • GDPR 合规:评论中包含用户生成内容(UGC),属于个人数据范畴。如涉及欧盟用户,需遵守 GDPR 的合法性、目的限制和数据最小化原则
  • CCPA 合规:加州用户有权要求删除其个人信息,采集的评论数据可能包含此类信息
  • 优先使用官方 API:当 API 能满足需求时,不应选择采集

核心原则:采集公开数据用于内部分析是可以接受的,但重新分发或商业化他人创作的原始内容则越界。字幕文本、视频画面、音频内容均受版权保护,仅可用于个人或团队内部分析,不得转售、公开共享或用于训练面向公众的 AI 模型。

关键要点

  • YouTube Data API v3 的每日 10,000 配额单位在大规模场景下远远不够,尤其是 search.list 每次消耗 100 单位
  • InnerTube API(/youtubei/v1/next/youtubei/v1/player/youtubei/v1/browse)提供结构化 JSON 数据,无需解析 HTML
  • 续页令牌(continuation token)是 InnerTube 的分页机制,必须顺序遍历,无法跳页
  • 数据中心 IP 在 YouTube 上几乎立即被标记,住宅代理是大规模采集的必要条件
  • 使用 ProxyHat 住宅代理时,通过 user-country-{CC}-session-{id} 格式实现地理定位和会话粘滞
  • 注意 TLS 指纹、请求头顺序和行为模式等隐性检测维度,考虑使用 curl_cffi 或 httpx
  • 始终优先使用官方 API,采集仅作为补充手段
  • 不得重新分发创作者的受版权保护内容,包括字幕和视频素材

准备开始了吗?

通过AI过滤访问148多个国家的5000多万个住宅IP。

查看价格住宅代理
← 返回博客