如何程序化点击元素并等待加载新元素,批量获取有效音频下载链接?
问题:如何程序化遍历点击元素列表,等待点击后加载预定义新元素?
场景示例:
我想从OTRR网站下载老广播节目《I Love a Mystery》,先通过以下代码构造了URL列表:
const urlStub = "https://otrr.org/OTRRLibrary/jukebox/"; copy(Array.from( document.getElementsByTagName('a')). map( a => `"${urlStub}${a.href.split('/').pop()}"` ). join(' ') )
但用下面的curl命令下载时,大量返回404 Not Found错误:
for url in <copied-list>; do curl -k -O $u; done
后来发现必须点击节目标题,触发播放器预加载音频文件后,curl才能成功下载。手动点击数百个文件太麻烦,尝试过批量点击的代码:
const t = Array.from(document.querySelectorAll('td.colTitle')); t.pop().click() ... t.pop().click()
但这个方案不够理想。我自己写了基于MutationObserver的递归解决方案,但不确定有没有更简单、不会因数千条内容崩溃的方法。
解决方案
方案1:async/await + 固定延迟(简单易实现)
适合页面加载逻辑稳定的场景,给每个点击操作预留足够的预加载时间,避免请求失败。
async function batchClickElements() { // 反转数组,按页面显示顺序点击(原pop()是倒序) const elements = Array.from(document.querySelectorAll('td.colTitle')).reverse(); for (const el of elements) { el.click(); // 等待音频预加载,时间根据页面实际速度调整,比如2秒 await new Promise(resolve => setTimeout(resolve, 2000)); // 可选:提取当前可用的音频下载链接 const validAudioUrl = document.querySelector('audio').src; console.log(validAudioUrl); // 可收集这些URL后批量下载 } } // 执行批量点击 batchClickElements();
- 优点:代码简洁,无需监听DOM变化,调试成本低
- 缺点:固定延迟不够灵活,页面加载慢时可能失败,加载快时浪费时间
方案2:改进版MutationObserver(稳定可靠)
针对页面加载速度不稳定的情况,通过监听DOM变化确认音频预加载完成后,再执行下一次点击,避免无效操作或内存泄漏。
function processNextElement(elements) { if (elements.length === 0) { console.log('所有元素处理完成'); return; } const currentEl = elements.shift(); currentEl.click(); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { // 监听音频元素的src属性变化,确认有效链接加载完成 if (mutation.type === 'attributes' && mutation.attributeName === 'src') { const audioSrc = mutation.target.src; if (audioSrc && !audioSrc.includes('placeholder')) { // 替换为页面实际的占位符特征 console.log('可用下载链接:', audioSrc); observer.disconnect(); // 停止当前监听,避免内存占用 // 留短缓冲时间,再处理下一个元素 setTimeout(() => processNextElement(elements), 500); } } // 如果音频元素还未生成,监听播放器容器的DOM新增 if (mutation.type === 'childList') { const audioEl = document.querySelector('audio'); if (audioEl) { observer.disconnect(); // 重新监听音频元素的属性变化 observer.observe(audioEl, { attributes: true }); } } } }); // 优先监听已存在的音频元素,否则监听播放器容器 const audioEl = document.querySelector('audio'); if (audioEl) { observer.observe(audioEl, { attributes: true }); } else { // 替换为页面实际的播放器容器选择器 const playerContainer = document.querySelector('#jukebox-player'); observer.observe(playerContainer, { childList: true, subtree: true }); } } // 启动批量处理 const targetElements = Array.from(document.querySelectorAll('td.colTitle')).reverse(); processNextElement(targetElements);
- 优点:基于实际DOM变化触发,可靠性高,无无效等待,适合大量元素处理
- 优化点:每次处理后断开观察者,避免内存泄漏;按顺序处理元素,逻辑更清晰;增加缓冲时间防止页面卡顿
方案3:直接调用接口获取真实URL(最优解)
通过浏览器开发者工具分析页面点击时的请求,找到获取音频URL的接口,直接批量请求接口获取真实链接,无需模拟点击。
async function fetchAllAudioUrls() { // 获取所有节目标题对应的标识(假设ID存储在data-episode-id属性) const titleElements = Array.from(document.querySelectorAll('td.colTitle')); const episodeIds = titleElements.map(el => el.dataset.episodeId); const audioUrls = []; for (const id of episodeIds) { // 替换为页面实际的音频接口地址 const response = await fetch(`https://otrr.org/api/episode/audio?id=${id}`, { headers: { // 复制浏览器请求头中的Cookie、Referer等信息,避免接口拦截 'Cookie': document.cookie, 'Referer': window.location.href } }); const data = await response.json(); audioUrls.push(data.audioUrl); console.log(data.audioUrl); } // 将所有URL复制到剪贴板,方便后续curl批量下载 copy(audioUrls.join('\n')); } fetchAllAudioUrls();
- 优点:无需模拟点击,效率最高,不会出现页面卡顿或崩溃问题
- 注意:需要匹配接口的请求头(如Cookie、Referer),部分网站可能有反爬机制,需根据实际情况调整
内容的提问来源于stack exchange,提问作者toraritte
相关产品推荐
相关产品推荐

