为什么会发生错误
虽然您没有显示足够的代码来重现问题(是courseCountArr
ElementHandles 数组?通过page
to evaluate
也不起作用,这是一个 Node 对象),这是显示可能模式的最小再现:
const puppeteer = require("puppeteer");
let browser;
(async () => {
const html = `<ul><li>a</li><li>b</li><li>c</li></ul>`;
browser = await puppeteer.launch();
const [page] = await browser.pages();
await page.setContent(html);
// ...
const nestedHandle = await page.$$("li"); // $$ selects all matches
await page.evaluate(els => {}, nestedHandle); // throws
// ...
})()
.catch(err => console.error(err))
.finally(() => browser?.close())
;
输出是
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'BrowserContext'
| property '_browser' -> object with constructor 'Browser'
--- property '_defaultContext' closes the circle Are you passing a nested JSHandle?
at JSON.stringify (<anonymous>)
为什么会发生这种情况?回调中的所有代码page.evaluate https://pptr.dev/api/puppeteer.page.evaluate(和家人:evaluateHandle https://pptr.dev/api/puppeteer.page.evaluatehandle, $eval https://pptr.dev/api/puppeteer.page._eval, $$eval https://pptr.dev/api/puppeteer.page.__eval) 由 Puppeteer 在浏览器控制台内以编程方式执行。浏览器控制台是一个与 Node 截然不同的环境,Puppeteer 和 ElementHandles 都位于其中。为了弥补进程间的差距,回调evaluate
,参数和返回值被序列化和反序列化。
这样做的结果是您无法像尝试那样访问任何 Node 状态page.waitForSelector('.strong, .section-body')
在浏览器内。page
与浏览器处于完全不同的过程。 (作为旁白,document.querySelectorAll https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll是纯粹同步的,所以没有意义await
ing it.)
傀儡师元素句柄 https://pptr.dev/api/puppeteer.elementhandle是用于挂钩页面 DOM 的复杂结构,无法像您尝试那样序列化并传递到页面。 Puppeteer 必须在后台执行翻译。任何 ElementHandles 传递给evaluate
(或有.evaluate()
调用它们)跟随它们所代表的浏览器中的 DOM 节点,并且该 DOM 节点就是您的evaluate
的回调被调用。截至撰写本文时,Puppeteer 无法使用嵌套的 ElementHandles 来执行此操作。
可能的修复
在上面的代码中,如果你改变.$$ https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pageselector-1 to .$ https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pageselector,您将仅检索第一个<li>
。这个单一的、非嵌套的 ElementHandle 可以转换为一个元素:
// ...
const handle = await page.$("li");
const val = await page.evaluate(el => el.innerText, handle);
console.log(val); // => a
// ...
Or:
const handle = await page.$("li");
const val = await handle.evaluate(el => el.innerText);
console.log(val); // => a
要在您的示例中实现此功能,只需交换循环和evaluate
致电以便您访问courseCountArr[i]
在 Puppeteer 领域,将嵌套的 ElementHandles 解包到单独的参数中evaluate
, or将大部分控制台浏览器调用移回 Puppeteer(取决于您的用例和代码目标)。
您可以应用evaluate
调用每个 ElementHandle:
const nestedHandles = await page.$$("li");
for (const handle of nestedHandles) {
const val = await handle.evaluate(el => el.innerText);
console.log(val); // a b c
}
要获得一系列结果,您可以执行以下操作:
const nestedHandles = await page.$$("li");
const vals = await Promise.all(
nestedHandles.map(el => el.evaluate(el => el.innerText))
);
console.log(vals); // [ 'a', 'b', 'c' ]
您还可以将 ElementHandles 解压缩为参数evaluate
并使用(...els)
回调中的参数列表:
const nestedHandles = await page.$$("li");
const vals = await page.evaluate((...els) =>
els.map(e => e.innerText),
...nestedHandles
);
console.log(vals); // => [ 'a', 'b', 'c' ]
如果除了句柄之外还有其他参数,您可以执行以下操作:
const nestedHandle = await page.$$("li");
const vals = await page.evaluate((foo, bar, ...els) =>
els.map(e => e.innerText + foo + bar)
, 1, 2, ...nestedHandle);
console.log(vals); // => [ 'a12', 'b12', 'c12' ]
or:
const nestedHandle = await page.$$("li");
const vals = await page.evaluate(({foo, bar}, ...els) =>
els.map(e => e.innerText + foo + bar)
, {foo: 1, bar: 2}, ...nestedHandle);
console.log(vals); // => [ 'a12', 'b12', 'c12' ]
另一种选择可能是使用$$eval
,它选择多个句柄,然后在浏览器上下文中运行回调,并将所选元素的数组作为其参数:
const vals = await page.$$eval("li", els =>
els.map(e => e.innerText)
);
console.log(vals); // => [ 'a', 'b', 'c' ]
如果您没有对 Node.js 中的句柄执行任何其他操作,这可能是最干净的。
同样,您可以完全绕过 Puppeteer 并在浏览器上下文中完成整个选择和操作:
const vals = await page.evaluate(() =>
[...document.querySelectorAll("li")].map(e => e.innerText)
);
console.log(vals); // => [ 'a', 'b', 'c' ]
(请注意,获取内部文本只是您可能拥有的任意复杂的浏览器代码的占位符)