使用HtmlAgilityPack抓取YouTube视频信息时节点返回null问题
问题根因
你之前推测的「HtmlAgilityPack无法读取ytd-page-manager这类自定义标签」是错误的:只要节点存在于加载到的HTML源码中,HtmlAgilityPack可以正常解析任意名称的自定义标签,和标签名格式无关。
返回null的核心原因有两个:
- HtmlAgilityPack本身没有JavaScript执行能力,直接调用
Load()拉取到的只是YouTube服务端返回的初始骨架HTML,所有视频数据、ytd开头的自定义组件都是页面加载后靠JS动态渲染插入到DOM里的,你写的XPath对应的节点根本不存在于初始源码中,自然匹配不到结果。 - 你写的全路径带索引的XPath鲁棒性极差,YouTube前端会频繁调整DOM层级、板块顺序,哪怕你拿到了渲染后的完整DOM,只要节点索引、层级有一点变动,这个XPath就会直接失效。
可行实现方案
不要硬爬动态渲染后的DOM节点,YouTube频道页的所有视频数据在初始返回的HTML里就已经内嵌在ytInitialData这个全局JS变量中,直接提取这段JSON数据解析即可,不需要JS渲染环境,稳定性远高于XPath匹配DOM的方案。
实现步骤
- 拉取页面时配置常规浏览器UserAgent,避免被YouTube反爬拦截
- 遍历页面script标签,提取
ytInitialData变量对应的JSON内容 - 反序列化JSON后按固定结构遍历提取视频标题、描述、链接、播放量等需要的字段
参考代码
using HtmlAgilityPack; using System.Text.Json; string url = "https://www.youtube.com/c/charlieputh/featured"; HtmlWeb webClient = new HtmlWeb(); // 模拟浏览器请求头 webClient.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"; HtmlDocument doc = webClient.Load(url); // 提取内嵌的ytInitialData数据 string ytInitialDataRaw = string.Empty; HtmlNodeCollection scriptNodes = doc.DocumentNode.SelectNodes("//script"); foreach (HtmlNode script in scriptNodes) { string scriptText = script.InnerText; if (scriptText.StartsWith("var ytInitialData = ")) { int startPos = "var ytInitialData = ".Length; int endPos = scriptText.LastIndexOf(';'); ytInitialDataRaw = scriptText.Substring(startPos, endPos - startPos); break; } } // 解析JSON提取视频信息 using JsonDocument jsonDoc = JsonDocument.Parse(ytInitialDataRaw); var selectedTab = jsonDoc.RootElement .GetProperty("contents") .GetProperty("twoColumnBrowseResultsRenderer") .GetProperty("tabs") .EnumerateArray() .First(tab => tab.GetProperty("tabRenderer").GetProperty("selected").GetBoolean()) .GetProperty("tabRenderer"); var videoList = selectedTab .GetProperty("content") .GetProperty("sectionListRenderer") .GetProperty("contents") .EnumerateArray() .SelectMany(section => section.GetProperty("itemSectionRenderer").GetProperty("contents").EnumerateArray()) .Where(content => content.TryGetProperty("shelfRenderer", out _)) .SelectMany(shelf => shelf.GetProperty("shelfRenderer").GetProperty("content").GetProperty("horizontalListRenderer").GetProperty("items").EnumerateArray()) .Select(item => item.GetProperty("gridVideoRenderer")); foreach (var videoItem in videoList) { // 提取视频标题 string videoTitle = videoItem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString(); // 提取视频ID string videoId = videoItem.GetProperty("videoId").GetString(); // 其他字段比如观看量、发布时间、短描述都可以在gridVideoRenderer节点下找到对应属性 Console.WriteLine($"视频标题:{videoTitle},视频地址:https://www.youtube.com/watch?v={videoId}"); } Console.ReadLine();
注意事项
- 这个方案不需要依赖Selenium、Playwright这类重的浏览器自动化工具,请求和解析速度快,资源占用低
- 如果后续YouTube调整了内嵌JSON的结构,只需要对照实际返回的JSON调整属性访问路径即可,维护成本远低于超长XPath
- 如果请求返回异常,可以适当调整请求头、增加请求间隔,避免触发YouTube的反爬机制
内容的提问来源于stack exchange,提问作者tyler
相关产品推荐
相关产品推荐

