Puppeteer 的 page.evaluate 和 evaluateHandle 有什么区别,该怎么取回页面数据
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
用 Puppeteer 驱动页面后,最常见的一个任务是"把页面上的某个值取回自己的脚本里"。此时会遇到两个方法:page.evaluate和page.evaluateHandle,签名几乎一样,但返回的东西完全不同。更常见的情况是:evaluate里return document.body之后拿到的是{},于是不知道该换哪个方法。
本文基于 Puppeteer 的 JavaScript execution 指南 和 API 文档(Page.evaluate、Page.evaluateHandle、JSHandle、ElementHandle),讲清楚两者的区别,以及取回数据时该走哪条路径。
先建立上下文:在页面里执行 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<...>>,即JSHandle或ElementHandle,页面内对象的引用 | 需要在页面里继续持有这个对象并做后续操作 |
两者对 Promise 的行为一致:如果页面里的函数返回 Promise,Puppeteer 会等它 resolve 后再继续,所以page.evaluate(() => new Promise(...))会阻塞到页面内 Promise 完成。
evaluate:取回纯数据的正确姿势
指南对evaluate返回值的说明是:
- 返回基本类型时,自动转换成脚本上下文中的基本类型;
- 返回对象时,Puppeteer 会把它序列化成 JSON 再在脚本端重建——"This process might not always yield correct results"。
所以只要目标数据是字符串、数字、布尔值,或者你手动在页面里拼好的普通对象/数组,用evaluate一步到位。
从元素上提取单个值,优先用page.$eval(Page.$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,访问value、href这类子类型字段时需要显式标注:
// 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.$ 找不到时返回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,再传给evaluate读innerHTML。
返回 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); // truePage.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 的完整用法。
取数路径怎么选,以及边界
结合上面的机制,取回页面数据可以按这个决策走:
- 目标是单个可序列化的值(字符串、数字、布尔、你在页面里拼好的纯数据对象/数组):
page.$eval(selector, el => el.xxx)(针对单个元素)或page.evaluate(() => ...)(针对整个页面)一步取回。 - 需要持有页面内对象继续操作(点击、滚动、继续取属性):
page.evaluateHandle拿JSHandle/ElementHandle,再用jsonValue()/getProperty()/handle.evaluate()取回具体数据,用完后dispose()。 - 直接
returnDOM 节点这类不可 JSON 化的对象给evaluate,只会得到{}——这是文档明确展示的失败模式,不要靠它取元素。
边界与限制(均来自上述文档):
evaluate返回对象走 JSON 序列化重建,"might not always yield correct results";- 页面函数被字符串化到页面执行,不能引用脚本作用域的变量;
jsonValue()遇循环引用抛错,且不调用toJSON;$eval找不到元素抛错,page.$找不到元素返回null。
如果要继续深入,JSHandle 还有getProperties()(拿到引用对象所有属性的 handle map)、asElement()(判断 handle 是否为ElementHandle)等方法;ElementHandle 则提供$、$$、screenshot、boundingBox等元素级操作,都是拿 handle 之后的下一组动作。
【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考