媒体分析团队和创作者经济研究者常常面临一个困境: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.list | 100 单位/次 | ~100 次 |
| videos.list | 1 单位/次 | ~10,000 次 |
| commentThreads.list | 1 单位/次 | ~10,000 次 |
| channels.list | 1 单位/次 | ~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/"
}请求体通常包含 videoId、context 等字段。context.client 对象指定客户端类型和版本,YouTube 据此返回不同格式的数据。常用的客户端标识包括 WEB(网页版,clientName=1)和 ANDROID(安卓版,clientName=3)。
续页令牌机制
InnerTube 不使用传统的页码分页,而是通过续页令牌(continuation token)实现翻页:
- 首次请求返回第一批数据和一个
continuationEndpoint对象 - 后续请求将令牌放入请求体的
continuation字段 - 每次响应返回新的令牌,直到没有更多数据
这种机制意味着你必须顺序遍历——无法跳页。对于评论采集,这要求逐页请求直到达到所需深度。每个续页令牌通常对应约 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_cffi或tls_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 v3 | InnerTube 端点采集 | 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,采集仅作为补充手段
- 不得重新分发创作者的受版权保护内容,包括字幕和视频素材






