本文介绍一种基于 MutationObserver 的可靠方案,用于在网页中逐个点击列表项,等待异步加载的音频元素出现后再继续操作,从而避免因资源未就绪而导致的 404 错误。
爬取那些依赖前端交互来触发资源加载的网页(例如 OTRR 老广播剧库),经常会遇到一个棘手的问题:直接提取 标签中的 URL 却返回 404 Not Found。原因很简单——目标音频文件并非静态存在,而是需要先点击某个标题(比如 td.colTitle),页面才会动态插入一个
手动点击数百项?不现实。简单粗暴的递归 click + setTimeout 容易导致顺序错乱、超时或主线程阻塞。更可靠的做法是结合 DOM 变化监听与任务队列控制:每次点击后精准等待目标元素(例如 audio)出现,再执行下一步。下面这个方案已经过生产环境验证,可直接使用:
async function clickAndAwaitElements(
clickTargets, // 待点击的 HTMLElement[](如 td.colTitle)
targetSelector, // 需等待出现的元素选择器(如 "audio")
timeoutMs = 10000 // 单次等待超时时间(毫秒)
) {
if (clickTargets.length === 0) return;
const currentTarget = clickTargets.pop();
currentTarget.click();
// 使用 Promise.race 确保不无限挂起
await Promise.race([
new Promise((resolve) => {
const observer = new MutationObserver(() => {
if (document.querySelector(targetSelector)) {
observer.disconnect();
resolve();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 超时兜底
setTimeout(() => {
observer.disconnect();
console.warn(`Timeout waiting for ${targetSelector} after clicking`);
resolve(); // 继续下一循环,避免卡死
}, timeoutMs);
}),
new Promise(resolve => setTimeout(resolve, timeoutMs)) // 双保险
]);
// 递归处理剩余项(使用 async/await 保证顺序)
await clickAndAwaitElements(clickTargets, targetSelector, timeoutMs);
}
// 使用示例:
const titles = Array.from(document.querySelectorAll('td.colTitle'));
clickAndAwaitElements(titles, 'audio', 8000)
.then(() => console.log('✅ All clicks completed and audio elements loaded'))
.catch(err => console.error('❌ Execution failed:', err));
这个方案有几个值得一提的优势:
- 非阻塞 & 可控:通过 async/await 代替深层递归,避免调用栈溢出;Promise.race 防止 MutationObserver 永久挂起。
- 容错设计:超时后自动跳过当前项继续,确保整体流程不因某个卡点而中断。
- 精准监听:只关心 targetSelector 是否出现,而不是盲目轮询或监听所有变化,性能更优。
- 语义清晰:函数职责单一——点击、等待、下一轮——便于复用和调试。
使用时也要注意几个细节:
- 确保页面启用了 CORS 或允许跨域请求(curl -k 只能绕过证书验证,无法解决跨域限制)。
- 如果目标元素被插入到 Shadow DOM 内,需要将
observer.observe()改为监听对应的 shadowRoot。 - 大量操作时建议添加节流(比如
await new Promise(r => setTimeout(r, 500))),避免触发网站的反爬机制。 - 最终下载时,应从已加载的
这套方案兼顾了可靠性、可维护性与浏览器兼容性(所有现代浏览器均支持),是处理“点击触发动态加载”类场景的推荐实践。
