如何在Puppeteer中确定性使用CSS选择器获取SoundCloud歌曲时长
问题解决方案
核心原因
- SoundCloud是客户端渲染的单页应用,默认的
page.goto完成后页面动态内容还没渲染完成,直接执行选择器自然返回空 page.waitForNavigation()放在page.goto()之后调用会报错,因为该API的作用是等待「后续触发的页面跳转动作」完成,此时导航已经结束,自然会超时抛出异常
具体优化步骤
1. 给页面跳转增加完整加载配置
修改page.goto参数,配置等待网络空闲,延长超时时间适配资源加载:
await page.goto("https://soundcloud.com/octasine/octasine-audio-example-1", { // 等待2秒内只有不到2个网络请求,认为动态内容加载完成 waitUntil: 'networkidle2', // 超时时间调整为60秒,避免网络慢导致加载失败 timeout: 60000 });
直接删除你注释的await page.waitForNavigation(),不需要再加这个语句。
2. 执行选择器前先等待目标元素加载完成
在调用work函数之前,先等待你要找的span.sc-visuallyhidden元素渲染完成,避免空数组:
// 等待目标元素出现,最长等10秒 await page.waitForSelector('span.sc-visuallyhidden', { timeout: 10000 }); await work();
同理,点击播放按钮前也可以加元素等待,避免点击失效:
await page.waitForSelector('.snippetUXPlayButton', { timeout: 10000 }); await page.evaluate(() => { let elements = document.getElementsByClassName('snippetUXPlayButton'); for (let element of elements) element.click(); });
3. 优化时长提取逻辑,增加容错
你当前的mins和secs没有声明作用域,且用split提取容易受文本格式影响,建议调整:
首先在test函数顶部声明变量:
let mins = 0, secs = 0;
然后修改work函数里的提取逻辑,用正则匹配更稳定:
if (inputValue.includes('Duration')){ console.log("DURATION"); const match = inputValue.match(/Duration (\d+) minutes? (\d+) seconds?/); if (match) { mins = Number(match[1]); secs = Number(match[2]); } console.log(mins); console.log(secs); console.log(inputValue); }
优化后完整可运行代码
// example.js -- node version v14.17.2 -- dependency installed with npm i puppeteer-stream const { launch, getStream } = require("puppeteer-stream"); const fs = require("fs"); const file = fs.createWriteStream(__dirname + "/test.webm"); async function test() { let mins = 0, secs = 0; const browser = await launch(); const page = await browser.newPage(); // 跳转时增加加载配置 await page.goto("https://soundcloud.com/octasine/octasine-audio-example-1", { waitUntil: 'networkidle2', timeout: 60000 }); let html_var = await page.content(); // 写入HTML文件 fs.writeFile("example.html", html_var, function (err) { if (err) return console.log(err); }); console.log("Wrote html to example.html"); // 等待播放按钮加载完成再点击 await page.waitForSelector('.snippetUXPlayButton', { timeout: 10000 }); await page.evaluate(() => { let elements = document.getElementsByClassName('snippetUXPlayButton'); for (let element of elements) element.click(); }); const work = async () => { const inputsValues = []; const inputElements = await page.$$('span.sc-visuallyhidden'); for (const element of inputElements) { let inputValue; inputValue = await element.getProperty('innerText'); inputValue = await inputValue.jsonValue(); // 时长提取增加容错 if (inputValue.includes('Duration')){ console.log("DURATION"); const match = inputValue.match(/Duration (\d+) minutes? (\d+) seconds?/); if (match) { mins = Number(match[1]); secs = Number(match[2]); } console.log(mins); console.log(secs); console.log(inputValue); } inputsValues.push(inputValue); } console.log(inputsValues) } // 等待目标元素加载完成再执行提取逻辑 await page.waitForSelector('span.sc-visuallyhidden', { timeout: 10000 }); await work(); let page_url = await page.url(); console.log(page_url) await page.evaluate(() => { let elements = document.getElementsByClassName('sc-visuallyhidden'); for (let element of elements) console.log(element.innerHTML); }); const stream = await getStream(page, { audio: true, video: true }); console.log("recording"); stream.pipe(file); setTimeout(async () => { await stream.destroy(); file.close(); console.log("finished"); browser.close(); }, 1000 * 5 + mins * 60000 + secs * 1000); } test();
内容的提问来源于stack exchange,提问作者swartchris8
相关产品推荐
相关产品推荐

