如何正确使用Puppeteer中的.$eval()与.$$eval()?

来源:程序开发作者:下班再修头衔:程序员
导读:本期聚焦于下班再修创作的《如何正确使用Puppeteer中的.$eval()与.$$eval()?》,敬请观看详情。Puppeteer 的页面求值方法中,$eval 与 $$eval 是使用频率很高但容易混淆的两个接口。$eval 针对匹配选择器的第一个元素执行函数,并把该元素作为参数传入;$$eval 则匹配所有元素,把元素数组作为参数传入。理解它们的底层执行机制、参数传递方式和返回值序列化规则,是避免运行时错误的关键。本文将深入对比两者的差异,通过完整代码示例演示如何从 DOM 中提取文本、属性以及执行复杂数据收集,并指出常见误区,比如选择器返回 null 时的处理、函数参数顺序、以及浏览器上下文与 Node 上下文的边界。掌握这些细节后,你可以更稳定地编写 Puppeteer 自动化脚本。

Puppeteer 提供了一组在页面上下文中执行 JavaScript 的方法,其中 .$eval() 和 .$$eval() 是处理 DOM 元素最常用的两个接口。它们都允许你把一个函数注入到浏览器页面中运行,但一个针对单个元素,另一个针对元素集合。很多脚本错误都源于对这两个方法参数结构的误解,所以把它们的机制彻底理清对编写可靠爬虫或自动化测试很有帮助。

如何正确使用Puppeteer中的.$eval()与.$$eval()?

从本质上说,.$eval(selector, pageFunction, ...args) 会先在页面中查找第一个匹配 selector 的元素,然后把这个元素作为第一个参数传给 pageFunction。如果页面中没有匹配元素,pageFunction 的第一个参数会是 null,而不是直接抛出异常。这一点非常关键,因为很多开发者会假设元素一定存在,结果在函数内部访问属性时得到 TypeError。相比之下,.$$eval(selector, pageFunction, ...args) 会查找所有匹配的元素,并把一个元素数组作为第一个参数传给 pageFunction。这个数组始终存在,即使没有元素匹配,也是一个空数组。

另一个重要的细节是,这两个方法中的 pageFunction 是在浏览器上下文里执行的,而不是在 Node.js 进程中执行。因此,函数内部可以访问 window、document 等浏览器 API,但不能直接引用 Node.js 环境中的变量,除非你通过 ...args 把值传递进去。传参时,参数会被序列化后传递给页面函数,基础类型和可 JSON 序列化的对象都可以正常使用。

$eval 与 $$eval 的核心区别

.$eval() 的设计目标是从单个元素中提取数据。调用它时,Puppeteer 内部会先执行 document.querySelector(selector),把得到的元素引用交给页面函数。例如,你想获取页面中第一个段落元素的文本内容,可以这样写:

const text = await page.$eval('p', el => el.textContent);
console.log(text);

这里 el 就是第一个 <p> 元素。如果页面上没有 <p> 元素,el 会是 null,此时访问 el.textContent 会抛出错误。更稳妥的做法是在函数内部先做判空处理:

const text = await page.$eval('p', el => el ? el.textContent : '');

而 .$$eval() 内部执行的是 document.querySelectorAll(selector),它把整个 NodeList 转换成数组后传给页面函数。因此你可以直接使用数组方法,比如 map、filter、forEach。例如,提取所有商品标题:

const titles = await page.$$eval('.product-title', items =>
  items.map(item => item.textContent.trim())
);
console.log(titles);

从返回结果来看,.$eval() 返回的是页面函数返回的值,这个值会被序列化后传回 Node.js 上下文。如果页面函数返回的是 DOM 元素或无法序列化的对象,Puppeteer 会尝试处理,但通常你应当返回基础类型、普通对象或数组。两者在序列化规则上没有区别,区别只在于传入页面函数的第一个参数类型不同。

$eval 的详细用法与参数传递

.$eval() 的完整签名是 page.$eval(selector, pageFunction, ...args)。selector 是 CSS 选择器字符串,pageFunction 是要在页面中执行的函数,后面的 ...args 会作为额外参数按顺序传给 pageFunction。注意,第一个参数固定是元素,后续参数才对应 ...args。比如你想从 Node.js 传递一个属性名给页面函数,用来获取元素对应的属性值:

const attributeName = 'data-id';
const value = await page.$eval(
  '.card',
  (el, attr) => el.getAttribute(attr),
  attributeName
);
console.log(value);

在上面的代码中,el 是第一个匹配 .card 的元素,attr 接收 attributeName 的值。这种传参方式可以避免在页面函数中硬编码数据,提高了代码复用性。需要留意的是,...args 会被 JSON 序列化后传给页面函数,所以不能传函数、DOM 节点或包含循环引用的对象。

另一个常见场景是通过 .$eval() 获取表单输入框的值。例如,读取搜索框中的关键词:

const keyword = await page.$eval('#search-input', input => input.value);
console.log(keyword); // 输出输入框当前的值

如果输入框不存在,input 为 null,访问 input.value 会报错。这时可以在函数内部判断,或者确保选择器可靠。有些开发者习惯使用可选链操作符 input?.value,但需要注意浏览器版本是否支持。稳妥写法仍然是显式判空。

$$eval 处理多个元素与复杂数据收集

.$$eval() 更适合批量数据处理。当你需要从一组元素中提取结构化信息时,页面函数通常会返回一个对象数组。例如,从新闻列表中提取标题和链接:

const news = await page.$$eval('.news-item', items =>
  items.map(item => ({
    title: item.querySelector('h3')?.textContent.trim(),
    link: item.querySelector('a')?.href,
    time: item.querySelector('.time')?.textContent.trim()
  }))
);
console.log(JSON.stringify(news, null, 2));

这里 items 是所有 .news-item 元素的数组,map 会遍历每个元素并返回普通对象。由于返回的是可序列化的对象数组,Puppeteer 会把它完整地传回 Node.js 上下文。需要提醒的是,页面函数中不能使用 console.log 把数据输出到 Node 终端,因为 console.log 在页面上下文中对应的是浏览器的控制台。如果你在页面函数里写了 console.log,需要开启 page.on('console') 才能看到输出。

有时候你只需要统计元素数量,不必把所有元素传给页面函数。虽然可以调用 page.$$eval('.item', items => items.length),但 Puppeteer 还提供了更直接的 page.$$eval 的兄弟方法 page.$$(selector) 用于获取元素句柄数组,不过 .$$eval 适合需要在页面中直接对元素做计算的情况。例如,计算所有商品价格的总和:

const total = await page.$$eval('.price', prices =>
  prices.reduce((sum, priceEl) => {
    const value = parseFloat(priceEl.textContent.replace(/[^0-9.]/g, ''));
    return sum + (isNaN(value) ? 0 : value);
  }, 0)
);
console.log(total);

这个例子展示了如何利用数组中可用的方法在页面上下文中完成数据清洗和计算,减少数据在 Node 和浏览器之间的传输量。对于大型页面,尽量在页面函数内完成过滤和聚合,只返回最终结果,能显著提升脚本性能。

常见错误与调试建议

第一个常见错误是混淆参数顺序。误以为 .$eval(selector, ...args, pageFunction) 的形式也能工作,实际上 pageFunction 必须是第二个参数,...args 从第三个参数开始。如果写错顺序,可能会收到 “pageFunction is not a function” 之类的错误。第二个常见错误是在 pageFunction 中直接引用外部变量。比如在 Node.js 中定义了一个变量 const prefix = 'item-',然后在页面函数里直接使用 prefix 会得到 ReferenceError,因为页面函数运行在浏览器全局作用域,看不到 Node 变量。正确做法是把变量作为参数传入。

另一个值得注意的坑是选择器匹配不到元素时 .$eval() 传入 null,而很多开发者会在页面函数中直接读取属性导致脚本中断。调试时可以在 Node 侧先用 page.$(selector) 检查元素是否存在,或者使用 page.waitForSelector 等待元素出现后再调用 .$eval()。如果元素是动态加载的,这一点尤其重要。

还有一点是关于返回值序列化。页面函数如果返回 undefined,Puppeteer 会把它转换成 undefined 传回 Node 侧,这没问题。但如果返回了 Map、Set 或自定义类实例,则可能无法原样保留,因为序列化会调用 JSON.stringify。建议始终返回 JSON 兼容的数据结构。最后,当你在 .$eval() 或 .$$eval() 中执行异步操作时,页面函数可以是 async 函数,Puppeteer 会等待 Promise 完成后再返回结果。不过要注意浏览器上下文中可用的异步 API 与 Node 不同,不要混用。

Puppeteer$eval$$eval修改时间:2026-09-19 13:03:15

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0919/59249.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。