先说结论:在 Puppeteer 中遍历 DOM 子元素时遇到 undefined,绝大多数情况是因为将 DOM 节点直接传递给 evaluate() 返回。这类错误非常常见,下面详细讲解如何避开这个陷阱,以及如何写出更稳定、更简洁的代码。

陷入这个问题的典型操作是:在浏览器上下文中使用 evaluate() 获取某些节点(例如 x.children),然后试图在 Node.js 环境中直接读取其 innerText。但问题在于,DOM 元素本质上是“不可序列化的复杂对象”——内部包含循环引用、方法和事件监听器等,Puppeteer 在跨进程传输时无法传递。你拿到的往往不是节点本身,而是 null 或 undefined。
因此,正确的思路非常直接:所有 DOM 操作必须在浏览器内部完成,最后只将字符串、数字、数组这类可序列化的数据传递出来。
推荐的做法是使用 page.$$eval(selector, pageFn)。这个 API 直接在页面内执行函数,自动传入所有匹配元素,并返回 JSON 可序列化的结果。例如下面的示例:
const selector = "#slenkanti_dalis tr"; // 尽量使用简洁的选择器,避免过度依赖嵌套结构
await page.waitForSelector(selector); // 关键步骤:确保元素已完全加载
const tableData = await page.$$eval(selector, rows =>
rows.map(row => {
return Array.from(row.querySelectorAll("td"), cell =>
cell.textContent.trim()
);
})
);
console.table(tableData);
// 输出结构大致如下:
// ┌─────────┬────────────┬───────────┐
// │ (index) │ 0 │ 1 │
// ├─────────┼────────────┼───────────┤
// │ 0 │ '2024-03-15' │ '125.99 €' │
// │ 1 │ '2024-02-28' │ '89.50 €' │
// └─────────┴────────────┴───────────┘
这里有几个值得注意的优化点:
- 选择器避免过于“绝对”:像
#slenkanti_dalis > table > tbody > tr这种写法,一旦页面结构微调就会失效。改用#slenkanti_dalis tr这种更宽松的选择器,容错性更强。 - 等待元素就绪再抓取:如果表格是动态加载的,没有
waitForSelector()很可能返回空数组。 - 使用
textContent而非innerText:textContent是标准化属性,性能也更优;innerText更接近“渲染后”的文本,但在某些旧环境兼容性稍差。日常抓取数据直接使用textContent最稳妥。 - 变量声明不要省略:原代码中
for (atsiskaitymas1 of ...)缺少声明符,容易造成全局变量污染。改写为for (const row of rows)更干净,ESLint 也会给出提示。
另外还有两点额外提醒:
- 不要混用
page.$$(...)和element.evaluate(...)处理列表——这会增加 ElementHandle 的管理负担,容易出错且性能较差。批量读取文本时,$$eval是最合适的工具。 - 如果需要点击元素或触发事件,确实要使用
element.click()等方法;但如果只是读取文字,$$eval可以一站式搞定,最简洁可靠。
掌握这个模式后,不仅 undefined 的问题迎刃而解,写出的脚本也更容易维护和调试。核心原则就是一句话:在浏览器内部完成所有操作,只把数据带出来。
