首页
/ Puppeteer 的 page.evaluate 和 evaluateHandle 有什么区别,该怎么取回页面数据

Puppeteer 的 page.evaluate 和 evaluateHandle 有什么区别,该怎么取回页面数据

2026-09-08 16:22:55作者:龚格成

用 Puppeteer 驱动页面后,最常见的一个任务是"把页面上的某个值取回自己的脚本里"。此时会遇到两个方法:page.evaluatepage.evaluateHandle,签名几乎一样,但返回的东西完全不同。更常见的情况是:evaluatereturn document.body 之后拿到的是 {},于是不知道该换哪个方法。

本文基于 Puppeteer 的 JavaScript execution 指南 和 API 文档(Page.evaluatePage.evaluateHandleJSHandleElementHandle),讲清楚两者的区别,以及取回数据时该走哪条路径。

先建立上下文:在页面里执行 JS 的最小路径

以下代码取自 JavaScript execution 指南,其中的 'YOUR_SITE' 是文档原有占位符,替换为你要访问的实际地址即可:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('YOUR_SITE');

const three = await page.evaluate(() => {
  return 1 + 2;
});
console.log(three);

await browser.close();

有一个必须记住的执行机制:函数体是在你的脚本里定义的,但会被 Puppeteer 转成字符串发送到目标页面里执行。因此它无法访问你脚本作用域里的变量,也不能调用脚本里定义的其他函数——整个逻辑必须写在函数体内。文档也支持直接传字符串(如 page.evaluate('1 + 2')),但文档明确建议优先用函数形式:字符串形式下,被求值函数能看到的类型和全局变量无法确定,TypeScript 里尤其要注意被引用对象的类型是否正确。

两者的核心区别:取回的是"值"还是"页面内对象的引用"

API 文档对两者差异的原文定义只有一句话(见 Page.evaluateHandle):

The only difference between page.evaluate and page.evaluateHandle is that evaluateHandle will return the value wrapped in an in-page object.

也就是说:

返回什么 适用情况
page.evaluate Promise<Awaited<ReturnType<Func>>>,页面函数执行后的原始值 只要一个可以跨上下文传回的最终值
page.evaluateHandle Promise<HandleFor<...>>,即 JSHandleElementHandle页面内对象的引用 需要在页面里继续持有这个对象并做后续操作

两者对 Promise 的行为一致:如果页面里的函数返回 Promise,Puppeteer 会等它 resolve 后再继续,所以 page.evaluate(() => new Promise(...)) 会阻塞到页面内 Promise 完成。

evaluate:取回纯数据的正确姿势

指南evaluate 返回值的说明是:

  • 返回基本类型时,自动转换成脚本上下文中的基本类型;
  • 返回对象时,Puppeteer 会把它序列化成 JSON 再在脚本端重建——"This process might not always yield correct results"。

所以只要目标数据是字符串、数字、布尔值,或者你手动在页面里拼好的普通对象/数组,用 evaluate 一步到位。

从元素上提取单个值,优先用 page.$evalPage.$eval:找到第一个匹配元素,把元素作为 pageFunction 的第一个参数)。文档给出的示例:

const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', el => el.outerHTML);

TypeScript 下默认参数类型是 Element,访问 valuehref 这类子类型字段时需要显式标注:

// if you don't provide HTMLInputElement here, TS will error
// as `value` is not on `Element`
const searchValue = await page.$eval(
  '#search',
  (el: HTMLInputElement) => el.value,
);

两个行为差异要注意:$eval 找不到匹配元素会抛错,而 [Page.](https://gitcode.com/GitHubTrending/puppeteer1/puppeteer/blob/3f273e8ed5ee26b17cddc6dceeb59d7ca660d322/docs/api/puppeteer.page..md?utmsource=gitcoderepofiles)找不到时返回null。写断言式提取用](https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer/blob/3f273e8ed5ee26b17cddc6dceeb59d7ca660d322/docs/api/puppeteer.page._.md?utm_source=gitcode_repo_files) 找不到时返回 `null`。写断言式提取用 `eval`,需要容错判断"元素是否存在"时用 `page.$` 然后自己判 `null`。

page.evaluate 本身也可以接收参数(Page.evaluate):

const three = await page.evaluate(
  (a, b) => {
    return a + b; // 1 + 2
  },
  1,
  2,
);

参数可以是基本值,也可以是 JSHandle / ElementHandle 实例,比如先 page.$('body') 拿 handle,再传给 evaluateinnerHTML

返回 DOM 节点得到 {} 时:改用 evaluateHandle

指南给出的典型翻车示例:

const body = await page.evaluate(() => {
  return document.body;
});
console.log(body); // {}, unexpected!

原因上面已经说了:对象要走 JSON 序列化重建,DOM 节点重建出来是空对象。指南给出的解法就是"按引用返回":

const body = await page.evaluateHandle(() => {
  return document.body;
});
console.log(body instanceof ElementHandle); // true

Page.evaluateHandle 文档的说明是:大多数时候返回 JSHandle;如果页面函数返回的是元素引用,你拿到的会是 ElementHandle(它继承自 JSHandle)。

拿元素 handle 之后可以直接执行元素级操作,这是 evaluate 给不了的:

const button = await page.evaluateHandle(() =>
  document.querySelector('button'),
);
// can call `click` because `button` is an `ElementHandle`
await button.click();

文档还提示:TypeScript 默认假设 evaluateHandle 返回 JSHandle,如果你确定它返回 ElementHandle,用泛型显式标注以获得完整类型:

const button = await page.evaluateHandle<ElementHandle>(...);

从 handle 里取回数据:jsonValue、getProperty、evaluate

evaluateHandle 把对象留在了页面里,数据要取回脚本侧,JSHandle 提供了这几条路:

1. jsonValue() —— "A vanilla object representing the serializable portions of the referenced object",即拿到引用对象中可序列化部分的普通 JS 对象:

const aHandle = await page.evaluateHandle(() => document.body);
const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle);
console.log(await resultHandle.jsonValue());
await resultHandle.dispose();

两个限制必须知道(见 JSHandle.jsonValue):

  • 对象存在循环引用时会抛错;
  • 即使对象上有 toJSON 方法,jsonValue() 不会调用它。

2. getProperty(name) —— 从引用对象上取单个属性,拿到的是新 handle,可以再 jsonValue()

const documentHandle = await page.evaluateHandle('document'); // 文档示例
const titleHandle = await documentHandle.getProperty('title');
console.log(await titleHandle.jsonValue());

3. handle.evaluate() —— 以当前 handle 作为第一个参数在页面里继续求值,取回的是原始值:

const url = await windowHandle.evaluate((w) => w.location.href);

文档示例 page.evaluateHandle('document') 展示的就是字符串形式拿 document 的 handle。

不要忘记释放 handle

JSHandle 的机制说明:handle 会阻止引用的对象被垃圾回收,除非你主动 dispose();当关联 frame 发生导航或父 context 销毁时,handle 会自动释放。跨导航场景不用手动管,但在同一页面里创建了大量 handle(例如循环里反复 evaluateHandle 提取数据)时,用完后调用 dispose() 释放

const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
await bodyHandle.dispose();

这是 Page.evaluate 文档 Example 3 的完整用法。

取数路径怎么选,以及边界

结合上面的机制,取回页面数据可以按这个决策走:

  1. 目标是单个可序列化的值(字符串、数字、布尔、你在页面里拼好的纯数据对象/数组):page.$eval(selector, el => el.xxx)(针对单个元素)或 page.evaluate(() => ...)(针对整个页面)一步取回。
  2. 需要持有页面内对象继续操作(点击、滚动、继续取属性):page.evaluateHandleJSHandle/ElementHandle,再用 jsonValue() / getProperty() / handle.evaluate() 取回具体数据,用完后 dispose()
  3. 直接 return DOM 节点这类不可 JSON 化的对象给 evaluate,只会得到 {}——这是文档明确展示的失败模式,不要靠它取元素。

边界与限制(均来自上述文档):

  • evaluate 返回对象走 JSON 序列化重建,"might not always yield correct results";
  • 页面函数被字符串化到页面执行,不能引用脚本作用域的变量;
  • jsonValue() 遇循环引用抛错,且不调用 toJSON
  • $eval 找不到元素抛错,page.$ 找不到元素返回 null

如果要继续深入,JSHandle 还有 getProperties()(拿到引用对象所有属性的 handle map)、asElement()(判断 handle 是否为 ElementHandle)等方法;ElementHandle 则提供 $$$screenshotboundingBox 等元素级操作,都是拿 handle 之后的下一组动作。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391