Puppeteer:将循环结构转换为 JSON 您是否传递嵌套的 JSHandle?

2024-03-31

我正在尝试抓取一个一页网站。有多种选择组合会导致不同的搜索重定向。我在里面写了一个for循环page.evaluate的回调函数来单击不同的选择并在每个按钮中进行单击搜索。但是,我收到错误:将循环结构转换为 JSON 您是否传递嵌套的 JSHandle?

请帮忙!

我当前版本的代码如下所示:

const res = await page.evaluate(async (i, courseCountArr, page) => {
    for (let j = 1; j < courseCountArr[i]; j++) {
        await document.querySelectorAll('.btn-group > button, .bootstrap-select > button')['1'].click() // click on school drop down
        await document.querySelectorAll('div.bs-container > div.dropdown-menu > ul > li > a')[`${j}`].click() // click on each school option
        await document.querySelectorAll('.btn-group > button, .bootstrap-select > button')['2'].click() // click on subject drop down
        const subjectLen = document.querySelectorAll('div.bs-container > div.dropdown-menu > ul > li > a').length // length of the subject drop down
        for (let k = 1; k < subjectLen; k++) {
            await document.querySelectorAll('div.bs-container > div.dropdown-menu > ul > li > a')[`${k}`].click() // click on each subject option
            document.getElementById('buttonSearch').click() //click on search button
            page.waitForSelector('.strong, .section-body')
            return document.querySelectorAll('.strong, .section-body').length
        }
    }
}, i, courseCountArr, page);

为什么会发生错误

虽然您没有显示足够的代码来重现问题(是courseCountArrElementHandles 数组?通过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是纯粹同步的,所以没有意义awaiting 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' ]

(请注意,获取内部文本只是您可能拥有的任意复杂的浏览器代码的占位符)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Puppeteer:将循环结构转换为 JSON 您是否传递嵌套的 JSHandle? 的相关文章

随机推荐