YouTube KOL 信息爬取:输入搜索关键词 → 输出创作者列表(频道名称、URL、视频数量、最相关视频的播放量、联系邮箱/社交链接) 触发场景: - “按关键词搜索 YouTube 以查找创作者信息” - “收集 YouTube 创作者联系信息” - “提取频道数据和邮箱”
YouTube — KOL InfoScraping. 输入搜索关键词 — 从搜索结果输入每个创建者的频道页面 — 提取频道名称、 URL、 视频计数、 大多数是一项面向实际任务的技能。它将相关步骤、工具调用和结果整理方式集中到统一流程中,帮助使用者更快完成目标并减少重复操作。
实际使用前应先确认任务范围、数据来源、运行环境、必要权限和关键参数,再依据技能说明逐步执行;若输入条件不完整,应先补齐信息或采用保守配置,避免因错误假设导致结果偏离需求。
执行过程中需要关注工具调用是否成功、接口或依赖是否可用、输出格式是否符合预期,并对异常提示、缺失字段和边界情况进行处理;涉及批量任务时,还应保存进度,避免中断后重复操作。
输入搜索关键词 → 从搜索结果中逐个进入创作者频道页 → 提取频道名称、URL、视频总数、最相关视频的播放量、联系邮箱(优先从频道简介中正则匹配 → 其次尝试从个人网站提取 → 最后回退至社交平台链接)
按关键词在 YouTube 上执行搜索,收集搜索结果中每位创作者的频道基础信息及联系方式。
https://www.youtube.com前置条件描述的是任务启动前网站必须所处的状态。它不指定 browser-act 如何连接浏览器——调用方应根据自身运行环境选择连接方式。
若 browser-act 已完成更新、API Key 已验证、且当前会话中已加载用量,则跳过此步骤。
否则请按顺序执行以下操作;任一环节失败即中止:
uv tool upgrade browser-act-cli || uv tool install browser-act-cli --python 3.12browser-act browser list 验证 API Key 是否可用。若返回缺失 Key 错误,请向用户完整展示错误信息(含注册链接),待其完成注册后再重试browser-act 加载 browser-act 用量所有在探索过程中发现的原子能力均列于下方;执行时按需组合使用。
导航至搜索页后,从页面内嵌的 ytInitialData 中提取结果:
browser-act navigate 跳转至搜索页:
https://www.youtube.com/results?search_query={keyword}
页面加载完成后,执行 browser-act eval(运行时数据提取):
// keyword={搜索关键词}
const d = ytInitialData;
const contents = d?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents;
const videos = [];
contents.forEach(s => {
(s?.itemSectionRenderer?.contents || []).forEach(i => {
if (i.videoRenderer) {
const v = i.videoRenderer;
videos.push({
title: v.title?.runs?.[0]?.text,
videoId: v.videoId,
views: v.viewCountText?.simpleText || v.viewCountText?.runs?.map(r => r.text).join(''),
channel: v.ownerText?.runs?.[0]?.text,
channelUrl: v.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl,
channelId: v.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId
});
}
});
});
// 提取分页 continuation token
const lastItem = contents[contents.length - 1];
const continuationToken = lastItem?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
JSON.stringify({ count: videos.length, videos, continuationToken: continuationToken || null })
响应结构:{ "count": N, "videos": [...], "continuationToken": "..." }
browser-act eval(请求在浏览器内发起,会话凭据自动注入):
// continuationToken={上一页返回的 continuationToken}
const apiKey = ytcfg?.data_?.INNERTUBE_API_KEY;
const ctx = ytcfg?.data_?.INNERTUBE_CONTEXT;
const res = await fetch('https://www.youtube.com/youtubei/v1/search?prettyPrint=false&key=' + apiKey, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context: ctx, continuation: '{continuationToken}' })
});
const data = await res.json();
const actions = data?.onResponseReceivedCommands?.[0]?.appendContinuationItemsAction?.continuationItems || [];
const videos = [];
actions.forEach(i => {
if (i?.itemSectionRenderer?.contents) {
i.itemSectionRenderer.contents.forEach(c => {
if (c.videoRenderer) {
const v = c.videoRenderer;
videos.push({
title: v.title?.runs?.[0]?.text,
videoId: v.videoId,
views: v.viewCountText?.simpleText || v.viewCountText?.runs?.map(r => r.text).join(''),
channel: v.ownerText?.runs?.[0]?.text,
channelUrl: v.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl,
channelId: v.ownerText?.runs?.[0]?.navigationEndpoint?.browseEndpoint?.browseId
});
}
});
}
});
const nextToken = actions[actions.length - 1]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
JSON.stringify({ count: videos.length, videos, continuationToken: nextToken || null })
响应结构与第一页一致。continuationToken 为 null 表示无更多结果。
导航至频道页后,从 ytInitialData 中提取基础信息:
browser-act navigate 跳转至频道页:
https://www.youtube.com{channelUrl}
channelUrl 格式为 /@ChannelHandle(源自搜索结果)。
页面加载完成后,执行 browser-act eval:
const d = ytInitialData;
const meta = d?.metadata?.channelMetadataRenderer;
const h = d?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel;
const rows = h?.metadata?.contentMetadataViewModel?.metadataRows;
const desc = meta?.description;
const emailMatch = desc?.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}/g);
JSON.stringify({
channelName: meta?.title,
channelUrl: meta?.vanityChannelUrl,
channelId: meta?.externalId,
description: desc,
emailInDesc: emailMatch,
handle: rows?.[0]?.metadataParts?.[0]?.text?.content,
subscribers: rows?.[1]?.metadataParts?.[0]?.text?.content,
videoCount: rows?.[1]?.metadataParts?.[1]?.text?.content
})
响应结构:{ "channelName": "...", "channelUrl": "...", "channelId": "UC...", "description": "...", "emailInDesc": ["email@example.com"] | null, "handle": "@...", "subscribers": "100K subscribers", "videoCount": "546 videos" }
通过频道页上的 browse API 获取 About 数据(社交链接、国家、加入日期等):
browser-act eval(请求在浏览器内发起,无需登录):
(async () => {
const d = ytInitialData;
const desc = d?.header?.pageHeaderRenderer?.content?.pageHeaderViewModel?.description?.descriptionPreviewViewModel;
const ep = desc?.rendererContext?.commandContext?.onTap?.innertubeCommand?.showEngagementPanelEndpoint;
const cont = ep?.engagementPanel?.engagementPanelSectionListRenderer?.content?.sectionListRenderer?.contents?.[0]?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint;
const token = cont?.continuationCommand?.token;
if (!token) return JSON.stringify({ error: 'no continuation token' });
const apiKey = ytcfg?.data_?.INNERTUBE_API_KEY;
const ctx = ytcfg?.data_?.INNERTUBE_CONTEXT;
const res = await fetch('https://www.youtube.com/youtubei/v1/browse?prettyPrint=false&key=' + apiKey, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context: ctx, continuation: token })
});
const data = await res.json();
const items = data?.onResponseReceivedEndpoints?.[0]?.appendContinuationItemsAction?.continuationItems;
const aboutRenderer = items?.find(i => i?.aboutChannelRenderer);
const vm = aboutRenderer?.aboutChannelRenderer?.metadata?.aboutChannelViewModel;
if (!vm) return JSON.stringify({ error: 'no aboutChannelViewModel' });
return JSON.stringify({
country: vm.country,
joinedDate: vm.joinedDateText?.content,
subscribers: vm.subscriberCountText,
videoCount: vm.videoCountText,
viewCount: vm.viewCountText,
links: (vm.links || []).map(l => ({
title: l?.channelExternalLinkViewModel?.title?.content,
link: l?.channelExternalLinkViewModel?.link?.content
}))
});
})()
响应结构:{ "country": "...", "joinedDate": "...", "links": [{"title": "Instagram", "link": "instagram.com/xxx"}, ...] }
从 About 数据返回的 links 中,剔除需登录才能访问的社交平台,仅保留公开可访问的个人/公司官网,并逐一访问以提取邮箱。
过滤规则 — 排除以下域名(需登录或通常不含邮箱):
instagram.com、tiktok.com、linkedin.com、x.com、twitter.com、facebook.com、discord.gg、discord.com、skool.com、apps.apple.com、play.google.com、github.com、buymeacoffee.com、youtube.com
browser-act eval(过滤逻辑,可在浏览器或本地执行):
// links={About 数据返回的 links 数组}
const socialDomains = ['instagram.com','tiktok.com','linkedin.com','x.com','twitter.com','facebook.com','discord.gg','discord.com','skool.com','apps.apple.com','play.google.com','github.com','buymeacoffee.com','youtube.com'];
const websiteLinks = links.filter(l => {
const domain = l.link?.toLowerCase() || '';
return !socialDomains.some(sd => domain.includes(sd));
});
JSON.stringify({ websiteLinks })
对 websiteLinks 中每个链接执行如下流程:
browser-act navigate 访问目标站点 → browser-act wait stable 等待页面稳定 → browser-act eval:
// 双路匹配邮箱(正文正则 + mailto 链接)
(() => {
const text = document.body.innerText;
const emailsInText = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}/g) || [];
const mailtoLinks = Array.from(document.querySelectorAll('a[href^="mailto:"]')).map(a => a.href.replace('mailto:', '').split('?')[0]);
const allEmails = [...new Set([...emailsInText, ...mailtoLinks])];
return JSON.stringify({ emails: allEmails });
})()
若首页未提取到邮箱,则查找 Contact / About 页面链接并最多下探一级:
(() => {
const contactLinks = Array.from(document.querySelectorAll('a')).filter(a => {
const t = (a.textContent + ' ' + a.href).toLowerCase();
return t.includes('contact') || t.includes('about');
}).map(a => a.href).filter(h => h.startsWith('http'));
return JSON.stringify({ contactLinks: [...new Set(contactLinks)].slice(0, 3) });
})()
访问 Contact 页面并重复邮箱提取流程;一旦找到邮箱即终止;最多访问两级(首页 + Contact 页面)。
无枚举参数。搜索关键词由用户指定,不存在预定义枚举值。
API 分页:使用 continuationToken,类型为游标(cursor),起始值:从 ytInitialData 中最后一个 continuationItemRenderer 获取;下一页值:从每次 API 响应中最后一个 item 的 continuationItemRenderer.continuationEndpoint.continuationCommand.token 提取;终止条件:continuationToken 为 null,或响应中未包含新的 videoRenderer。
videos.length ≥ 1channelName 和 channelId 均非空links 数组成功获取(允许为空)channelName、channelUrl、videoCount 每个频道的非空率达 100%ytInitialData 和 ytcfg 是 YouTube 页面内嵌的运行时变量;页面结构变更可能影响路径访问路径:{working_dir}/browser-act-skill-forge-memories/youtube-kol-scraper-youtube-kol-scraper.memory.md
执行前:若该文件存在,请先读取——其中记录了历史执行中遇到的异常事件(例如端点变更),据此调整本次执行策略。
执行后:若发生异常事件(端点失效、页面重构、反爬升级等),追加一行记录:
{YYYY-MM-DD}: {发生了什么} → {结论}
正常执行无需写入。
文件格式示例:
# youtube-kol-scraper-youtube-kol-scraper 经验记录 2026-04-16: YouTube reveal_business_email API 无论使用 Stealth Chrome 还是 Real Chrome 均返回 400(BotGuard 编码不兼容) → 放弃 reCAPTCHA 路径 2026-04-16: 社交平台简介(Instagram/X/TikTok)极少包含邮箱 → 不值得访问 2026-04-16: 个人/公司官网是最佳邮箱来源(命中率约 3/10),通过 mailto 链接 + 正则提取 → 作为主邮箱提取路径