公开的 YouTube 频道是一个隐藏在眼前的紧凑数据集。打开任何创作者的"视频"标签,你可以看到每个上传视频的标题、播放量、发布时间、时长,以及到观看页面的链接。汇聚整个频道的这些公开元数据,可以驱动竞争性内容分析、发布节奏研究和趋势追踪,而这一切都无需触碰任何私人账号。

本指南展示如何用 JavaScript 和 Node.js 构建一个 YouTube 频道爬虫。你将构建一个小型的、可运行的爬虫,通过 Crawling API 启用 JavaScript 渲染来抓取公开频道的视频页面,用 cheerio 解析每个视频卡片,并将结果导出为 JSON 或 CSV。整个教程限定在公开的、非个人数据范围内:任何人都可以打开的频道上的视频列表,绝不涉及私人播放列表、评论或任何需要登录才能访问的内容。本文靠近末尾的法律部分并非样板文字,请在将此方法用于任何真实规模的抓取之前阅读它。

你将构建什么

一个 Node.js 脚本,接受一个公开 YouTube 频道的"视频"页面 URL,通过 Crawling API 获取 JavaScript 渲染后的 HTML,并为该页面上的每个视频提取一条结构化记录。我们使用一个公开频道的"视频"标签作为运行示例,并为每个视频提取以下字段:

  • 标题(Title) 卡片上显示的视频标题,例如"Official Trailer"。
  • 播放量(Views) 公开的播放量文本,如"56K views"。
  • 上传日期(Upload date) 相对发布日期,如"6 days ago"。
  • 时长(Duration) 缩略图上的时长角标,如"2:14"。
  • 链接(Link) 到单个观看页面的绝对 URL。

我们还会读取少量频道级别的上下文(频道标题和 handle),使每次导出都能自我描述,然后将视频列表写出为 JSON 和 CSV。

为什么普通请求在 YouTube 上会失败

如果你用裸 HTTP 客户端请求频道的"视频"URL,你会得到一个 200 状态码,但几乎没有你想要的数据。YouTube 在浏览器中构建视频网格:初始 HTML 是一个薄壳,真正的卡片标记是在页面脚本运行并加载观看数据之后才通过 JavaScript 注入的。原始的 fetch 在这一切发生之前就捕获了页面,所以 cheerio 找到的是一个空网格。

除了渲染,YouTube 还会监视自动化流量。来自数据中心的 IP 和不像真实浏览器的请求模式会很快被出验证或限速,所以即使是在裸服务器上的无头浏览器也往往会停滞。因此,一个可运行的频道爬虫需要在一次请求中具备两样东西:一个真正渲染网格的浏览器,以及一个平台认为是真实访客的 IP。你可以自己用无头浏览器加上轮换住宅代理池来组合这些,但将它们拼在一起并保持健康运转就是工作的大部分。Crawling API 将两者折叠进一次调用:你向它发送带有 JavaScript 渲染启用的 URL,它在可信 IP 后面渲染页面,并返回供你解析的完整 HTML。

为什么需要渲染页面

Crawlbase 提供两种请求类型。普通请求抓取静态 HTML;JavaScript 请求则先在真实浏览器中渲染页面。YouTube 在客户端构建视频网格,所以 JavaScript 请求才能给你一个填充了内容的页面。普通请求只返回空壳,让 cheerio 无从解析。ajax_waitpage_wait 选项让你可以等待网格完成加载。

前置条件

在编写任何代码之前,你需要准备好以下几样东西。每一样都不需要太长时间。

基础的 JavaScript 和 Node.js 知识。你应该能够编写和运行 Node 脚本,并用 npm 安装包。如果你是 Node 新手,官方文档和任何入门课程都能让你达到本教程所需的水平。更完整的教程,请参阅我们关于如何用 Node.js 构建网络爬虫的指南。

Node.js 16 或更高版本。node --version 确认你的版本。如果没有,请从 Node.js 官网或通过 nvm 等版本管理器安装。

Crawlbase 账号和 token。注册后,打开你的仪表板,从账号文档页面复制你的 JavaScript 请求 token。像对待密码一样对待这个 token:它用于验证你的请求,所以不要将其纳入版本控制。新账号包含一批免费请求,且只为成功的请求付费,完全足够跟着本教程操作。

设置项目

创建一个项目文件夹,初始化它,并安装爬虫所需的两个库。

bash
node --version

mkdir youtube-channel-scraper && cd youtube-channel-scraper
npm init -y

npm install crawlbase cheerio

两个依赖完成主要工作:crawlbase 是 Crawling API 的官方 Node 客户端,cheerio 使用类 jQuery 的 API 解析返回的 HTML,让你可以用 CSS 选择器提取单个字段。如果你对选择器不熟悉,我们关于爬取 JavaScript 网站的入门文章与本指南配合得很好。

第 1 步:抓取渲染后的视频页面

从获取完整页面开始。导入 CrawlingAPI 类,用你的 token 初始化它,并请求频道的"视频"URL。由于网格在客户端渲染,传入 ajax_waitpage_wait,使晚加载的卡片在页面被捕获之前出现。在解析之前检查状态码,使失败变得响亮而非静默。

javascript
const { CrawlingAPI } = require('crawlbase');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

async function crawl(pageUrl) {
  const options = { ajax_wait: 'true', page_wait: 7000 };
  const response = await api.get(pageUrl, options);
  if (response.statusCode === 200) {
    return response.body;
  }
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

const channelUrl = 'https://www.youtube.com/@Netflix/videos';
crawl(channelUrl).then((html) => {
  console.log(html ? html.slice(0, 500) : 'No HTML returned');
});

两个等待选项对像这样的客户端渲染目标很重要。ajax_wait 告诉 API 等待异步内容完成加载,page_wait 在加载后固定等待一段毫秒数,使晚渲染的视频网格在页面被捕获之前出现。7 秒对 YouTube 来说是一个合理的起点;如果网格返回空,可以增大该值。用 node scraper.js 运行脚本,你应该看到真实的频道标记,而不是精简的外壳。这在你编写任何选择器之前确认了渲染正常工作。

Crawlbase Crawling API

那个 YouTube 网格需要在可信 IP 后面渲染页面,在一次调用中完成。Crawling API 接受你的 token,在真实浏览器中运行页面,等待你刚刚设置的 ajax_waitpage_wait 窗口,通过服务端住宅 IP 轮换,并返回完成的 HTML,让你省去自己运行无头浏览器队列和代理池的步骤。先用免费额度指向一个公开频道试试看。

第 2 步:用 cheerio 解析每个视频

手中有渲染后的 HTML,将其加载到 cheerio 并遍历视频卡片。渲染后的"视频"标签以重复的网格渲染器排列每个上传,所以你选择每张卡片,然后从其中读取标题、播放量、上传日期、时长和观看链接。防御性地读取每个字段,使一个缺失的值不会使整个运行崩溃,而标题锚点上的相对 URL 需要转换为绝对 URL。

javascript
const cheerio = require('cheerio');

function parseChannel(html) {
  const $ = cheerio.load(html);
  const channel = {
    title: $('#inner-header-container .ytd-channel-name .ytd-channel-name:first').text().trim(),
    channelHandle: $('.meta-item #channel-handle').text().trim(),
    videos: [],
  };

  $('#contents ytd-rich-item-renderer').each((_, el) => {
    const card = $(el);
    const title = card.find('#video-title').text().trim();
    if (!title) return;

    const meta = card.find('#metadata-line span');
    const href = card.find('a#video-title-link').attr('href')
      || card.find('a#thumbnail').attr('href');

    channel.videos.push({
      title,
      views: $(meta).eq(0).text().trim() || null,
      uploadDate: $(meta).eq(1).text().trim() || null,
      duration: card.find('#overlays #text').text().trim() || null,
      link: href ? new URL(href, 'https://www.youtube.com').href : null,
    });
  });

  return channel;
}

有几个细节让这段代码更具弹性。视频网格使用 YouTube 的 ytd-rich-item-renderer 卡片,每张卡片内 #video-title 存放标题,#metadata-line span 存放两个元数据 span:第一个是播放量,第二个是相对上传日期。时长角标位于 #overlays #text 下的缩略图覆盖层中。观看链接通过 attr 从锚点的 href 读取,而非其文本,然后通过 URL 构造函数传递,使相对的 /watch?v=... 路径变成绝对链接。当元素缺失时,每个字段回退到 null

选择器会漂移

YouTube 的元素 id 和渲染器标签(ytd-rich-item-renderer#video-title#metadata-line 等)会在没有通知的情况下发生变化,而且"视频"标签、Shorts 和频道主页的标记也各不相同。将上面的选择器视为起始模板,而非合同。当某个字段返回 null 时,在浏览器开发者工具中重新检查实时渲染的页面并更新选择器。定期的选择器维护对任何生产爬虫来说都是正常的,而不是出了什么问题的信号。

第 3 步:整合起来

现在将抓取和解析串联成一个可运行的脚本。抓取渲染后的 HTML,传给解析器,并打印结构化的频道记录。

javascript
const { CrawlingAPI } = require('crawlbase');
const cheerio = require('cheerio');

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_TOKEN' });

async function crawl(pageUrl) {
  const options = { ajax_wait: 'true', page_wait: 7000 };
  const response = await api.get(pageUrl, options);
  if (response.statusCode === 200) return response.body;
  console.error(`Request failed: ${response.statusCode}`);
  return null;
}

function parseChannel(html) {
  const $ = cheerio.load(html);
  const channel = {
    title: $('#inner-header-container .ytd-channel-name .ytd-channel-name:first').text().trim(),
    channelHandle: $('.meta-item #channel-handle').text().trim(),
    videos: [],
  };
  $('#contents ytd-rich-item-renderer').each((_, el) => {
    const card = $(el);
    const title = card.find('#video-title').text().trim();
    if (!title) return;
    const meta = card.find('#metadata-line span');
    const href = card.find('a#video-title-link').attr('href')
      || card.find('a#thumbnail').attr('href');
    channel.videos.push({
      title,
      views: $(meta).eq(0).text().trim() || null,
      uploadDate: $(meta).eq(1).text().trim() || null,
      duration: card.find('#overlays #text').text().trim() || null,
      link: href ? new URL(href, 'https://www.youtube.com').href : null,
    });
  });
  return channel;
}

async function main() {
  const channelUrl = 'https://www.youtube.com/@Netflix/videos';
  const html = await crawl(channelUrl);
  if (!html) return;
  const channel = parseChannel(html);
  console.log(`${channel.title} (${channel.channelHandle}): ${channel.videos.length} videos`);
  console.log(JSON.stringify(channel.videos.slice(0, 3), null, 2));
}

main();

输出结果是什么样的

node scraper.js 运行完整脚本,你会得到一个干净的记录数组,每个视频一条,可以写入 JSON、CSV 或数据库。播放量和上传日期是 YouTube 渲染时完全一致的公开字符串。

json
[
  {
    "title": "Ilary Blasi: The one and only | Official Trailer | Netflix",
    "views": "56K views",
    "uploadDate": "6 days ago",
    "duration": "2:14",
    "link": "https://www.youtube.com/watch?v=pNY3NkGX6e8"
  },
  {
    "title": "Verified Stand-Up | Official Trailer | Netflix",
    "views": "50K views",
    "uploadDate": "11 days ago",
    "duration": "1:58",
    "link": "https://www.youtube.com/watch?v=gMIvGpHd2dk"
  }
]

导出为 JSON 和 CSV

打印到控制台适合做检查,但你通常需要将数据保存到磁盘。写入 JSON 只需一行;一个小型 CSV 写入器将每个视频保存在各自的行上,并对字段进行转义,使标题中的逗号不会破坏列结构。

javascript
const fs = require('fs');

function saveJson(channel, file) {
  fs.writeFileSync(file, JSON.stringify(channel, null, 2));
}

function saveCsv(videos, file) {
  const headers = ['title', 'views', 'uploadDate', 'duration', 'link'];
  const cell = (v) => `"${(v ?? '').replace(/"/g, '""')}"`;
  const rows = videos.map((v) => headers.map((h) => cell(v[h])).join(','));
  fs.writeFileSync(file, [headers.join(','), ...rows].join('\n'));
}

// in main(), after parseChannel():
saveJson(channel, 'channel.json');
saveCsv(channel.videos, 'channel.csv');

cell 辅助函数将每个值用引号包裹,并将嵌入的引号双写,这是标准的 CSV 转义,使包含逗号或引号的标题保持在单个列中。JSON 保留嵌套的频道对象;CSV 只展平视频行,这是大多数电子表格和 BI 工具所期望的格式。

处理完整的频道列表

渲染后的"视频"页面给你的是最近一批上传,而非整个历史目录,因为 YouTube 会随着你滚动而懒加载更多卡片。一个渲染后的页面对于演示来说很充分,而且通常正是你进行近期活动研究所需要的全部。如果需要深入挖掘一个频道,有几个选项可以保持相同的抓取-解析模式。

  • 随渲染滚动。传入更长的 page_wait 并使用 Crawling API 的滚动选项,使页面在被捕获之前加载更多行,然后对更大的网格运行相同的 parseChannel
  • 遍历每个视频链接。从解析后的列表中获取每个 link,并通过相同的 crawl 函数抓取那个观看页面,用来自视频页面本身的公开细节丰富每行记录,然后为那种布局写一个小型解析器。

无论你选择哪种方式,结构都不会改变:渲染,然后解析。保持你的请求量适度,使你是在读取一个频道,而不是对它进行压力测试。

保持不被封锁

即使渲染已处理好,YouTube 仍会监视爬虫形态的流量。以下几个习惯能保持运行健康,而且它们适用于任何高防护的热门目标。

  • 控制请求节奏。在紧密循环中轰炸频道是被限速的最快方式。将请求分散开来,而不是以最高速度爬取一条路径。
  • 依赖轮换。住宅 IP 池将请求分散到许多真实用户地址上,使没有单一地址触发速率限制。Crawling API 为你处理这个问题;如果你自己搭建技术栈,这是需要做对的部分。
  • 读懂状态码。一个开始返回验证或错误的运行,是在告诉你当前的速率或 IP 级别已经不够了。将这视为退避的信号,而非可以忽略的噪音。

关于更广泛的策略,请参阅如何在不被封锁的情况下抓取网站。如果你宁愿通过轮换池路由自己的流量而不是使用托管 API,Smart AI Proxy 提供与之相同的住宅 IP 轮换作为即插即用的代理端点。如果你想抓取不同形态的 YouTube 数据,视频和搜索结果而非频道网格,请参阅我们关于如何抓取 YouTube 数据的指南。

抓取 YouTube 合法吗?

是否允许抓取 YouTube,取决于 YouTube 的服务条款、你所在的司法管辖区,以及你对数据的处理方式。YouTube 的条款限制了对网站的自动化访问,所以无论你的工具多么谨慎,抓取行为都可能违反这些条款。这里的任何代码都无法改变这一点;它只是让技术部分能够运行。请阅读 YouTube 的服务条款和 robots.txt,遵守其速率预期,并将两者都视为你收集内容的边界。

保持范围严格限定。只收集公开的、非个人数据:任何人无需登录即可在公开频道上看到的视频标题、播放量、上传日期、时长和观看链接。尽可能聚合数据(发布节奏、播放量分布、话题趋势),而不是为单个创作者建立档案。不要触碰任何登录后才有的内容、私人或未列出的视频、评论,或观看者和创作者的个人数据。当涉及任何个人数据时,GDPR 和 CCPA 适用:你需要合法依据来处理数据,并且必须遵守删除和访问请求。不要转发你获取链接的受版权保护的视频内容;指向公开观看页面的链接并不是托管媒体的许可证。

对于临时公开研究之外的任何需求,YouTube 提供了一条官方的、经过授权的路径:YouTube Data API。它以稳定、结构化的形式返回频道和视频元数据,有明确的配额和条款,当你需要大批量、可靠性或商业权利时,它是正确的工具。本指南刻意将范围限定在公开的频道列表页面,因为这是让工作合理可辩护的那条线。它不涵盖私人视频、评论、观看者数据、账号限定内容,或任何绕过身份验证的尝试。如果你的项目需要的超出了公开列表,YouTube Data API 或数据协议才是正确的途径,而不是一个更聪明的爬虫。

回顾

核心要点

  • YouTube 在客户端渲染网格。普通请求只返回空壳,所以你必须在解析"视频"页面之前先渲染它。
  • 你需要同时具备渲染和可信 IP。带 JavaScript 请求的 Crawling API 在一次调用中完成两者;ajax_waitpage_wait 控制它等待卡片的时间。
  • cheerio 完成提取工作。选择每张 ytd-rich-item-renderer 卡片,然后将标题、播放量、上传日期、时长和观看链接映射到当前选择器,并预期这些选择器会漂移。
  • 谨慎地导出和扩展。将 JSON 用于结构化数据,将 CSV 用于电子表格,通过更长时间的渲染或按视频抓取加上合理的节奏控制来深入挖掘频道。
  • 坚持使用公开数据。遵守 YouTube 的服务条款和 robots.txt,商业用途或大批量请求请优先使用官方 YouTube Data API,当涉及任何个人数据时遵守 GDPR 和 CCPA,且永远不要触碰登录、私人视频或评论。

常见问题

为什么普通请求从 YouTube 返回空的视频网格?

因为 YouTube 用 JavaScript 在浏览器中构建视频网格。初始 HTML 是一个薄壳,真正的卡片标记只有在页面脚本运行并加载观看数据之后才会注入,所以原始 HTTP 请求返回 200 状态码但网格为空。要获得填充了内容的页面,你必须先渲染它,这正是 Crawling API 的 JavaScript 请求为你处理的事情。

对于 YouTube,我需要普通请求还是 JavaScript 请求?

使用 JavaScript 请求。普通请求抓取静态 HTML,而在 YouTube 频道上,这只会返回没有任何卡片的空壳。JavaScript 请求在将 HTML 返回之前在真实浏览器中渲染页面,而 ajax_waitpage_wait 选项给网格足够的时间完成加载,使 cheerio 有卡片可以解析。

我的选择器返回 null。是什么改变了?

几乎可以确定是 YouTube 的标记发生了变化。它的渲染器标签和元素 id(ytd-rich-item-renderer#video-title#metadata-line 等)会在没有通知的情况下发生变化,而且"视频"标签、Shorts 和频道主页之间的标记各不相同,所以上个月还有效的选择器可能会失效。在浏览器开发者工具中重新检查实时渲染的页面并更新选择器。定期的选择器维护对任何生产爬虫来说都是正常的。

如何获取所有视频,而不只是最新的一批?

第一次渲染返回最新的上传,因为 YouTube 会随着滚动懒加载更多卡片。要往回追溯,可以通过 Crawling API 的滚动选项传入更长的 page_wait,使更多行在捕获前加载,然后对更大的网格运行相同的解析器。对于深度历史目录,官方 YouTube Data API 可以干净地分页获取频道上传,是更适合的工具。

我应该使用 YouTube Data API 还是抓取网站?

如果你需要大批量、有保障的结构或商业转用权,请使用官方 YouTube Data API。它正是为此而生的,并让你处于 YouTube 条款的正确一侧。本指南中的公开频道列表抓取方法适合较小的、以公开数据为目标的研究,只要你遵守服务条款、robots.txt 和速率限制即可。

我可以从频道抓取评论或观看者数据吗?

不可以,而且本指南不涵盖这部分。评论是用户撰写的个人内容,观看者数据不是公开的,所以两者都不在这里的公开、非个人范围之内。当涉及个人数据时,GDPR 和 CCPA 适用,你需要合法依据以及尊重删除请求的方式。对于这类数据的经授权访问,带有适当同意和条款的 YouTube Data API 才是正确的路径。

开始构建

大规模爬取任何站点,无需与基础设施对抗。

Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。

自助开通 · 无需销售通话 · 提供企业级爬取量