的后代节点。但原逻辑却在 item(即 .css-1tkalz1 元素)内部查找 .css-177ui4i,自然返回 null。
✅ 正确做法:以最外层统一容器为作用域
应将每个
const storesData = await page.$$eval('.css-4od5c4', buttons =>
buttons.map(button => {
const getText = selector =>
button.querySelector(selector)?.textContent?.trim() ?? 'no value';
return {
address: getText('.css-iqfm9l:nth-of-type(1)'), // 第一个 .css-iqfm9l(地址)
city: getText('.css-1cwtvfm'), // 城市
amount: getText('.css-177ui4i .css-iqfm9l'), // 第二个 div 下的同名 class
};
})
);? 关键改进点:
- 使用 page.$$eval() 直接遍历所有 .css-4od5c4 按钮,避免手动 Array.from(document.querySelectorAll(...));
- 所有 querySelector 均以 button 为上下文,确保能跨子 div 查找;
- 封装 getText() 工具函数提升可读性与容错性(自动处理 null 并提供默认值)。
? 替代方案:基于顺序的稳健提取(适用于 class 不稳定场景)
若 CSS 类名动态生成或存在重复干扰(如多个 .css-iqfm9l 出现在同一按钮中),可改用 DOM 结构顺序定位:
const storesData = await page.$$eval('.css-4od5c4', buttons =>
buttons.map(button => {
const paragraphs = [...button.querySelectorAll('p')].map(p => p.textContent.trim());
return {
address: paragraphs[0] || 'no value', // Sisjön / random address...
city: paragraphs[1] || 'no value', // Askim / some city...
amount: paragraphs[3] || 'no value', // "3 st" — 注意:跳过 <div class="css-7omsg3"> 中的 "Välj butik"
};
})
);✅ 该方式不依赖 class 名,仅依赖
标签在按钮内的固定顺序,适合反爬强度较高、class 频繁变更的站点。
⚠️ 注意事项与最佳实践
- 避免 setTimeout 等待硬编码延时:await new Promise(setTimeout(...)) 不可靠且低效。应改用 page.waitForSelector('.css-177ui4i .css-iqfm9l') 显式等待目标元素出现;
- 警惕 class 名重复性:.css-iqfm9l 同时用于地址和库存数量,直接全局匹配会取到第一个(地址)。务必结合父容器限定作用域;
- 启用严格选择器校验:在开发阶段添加断言,例如:
console.assert(button.querySelector('.css-177ui4i .css-iqfm9l'),
'⚠️ Amount element missing in button:', button.outerHTML.slice(0, 120)); - 考虑使用 data-testid 或语义化属性:若可影响前端,建议推动添加 data-store-address、data-store-stock 等属性,大幅提升抓取稳定性。
通过以上调整,您将获得预期的结构化结果:
[
{ "address": "Sisjön", "city": "Askim", "amount": "3 st" },
{ "address": "random address...", "city": "some city...", "amount": "3 st" }
]掌握“以记录容器为锚点 + 相对路径定位”的思维模式,是写出健壮 Puppeteer 抓取逻辑的核心能力。
今天关于《Puppeteer嵌套元素CSS选择器写法》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于的内容请关注golang学习网公众号!
您即将跳转至第三方网站,请注意保护好个人信息和财产安全!
继续访问