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

Jac*_*Zzz 5 javascript web-scraping puppeteer

我正在尝试抓取一页网站。有多种选择组合会导致不同的搜索重定向。我在page.evaluate回调函数中编写了一个 for 循环来单击不同的选择并在每个按钮中进行单击搜索。但是,我收到错误:将循环结构转换为 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);
Run Code Online (Sandbox Code Playgroud)

ggo*_*len 12

为什么会发生错误

虽然您没有显示足够的代码来重现问题(是courseCountArrElementHandles 数组吗?传递pageevaluate也不起作用,这是一个 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())
;
Run Code Online (Sandbox Code Playgroud)

输出是

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>)
Run Code Online (Sandbox Code Playgroud)

为什么会发生这种情况?回调page.evaluate(以及系列:evaluateHandle$eval$$eval)内的所有代码均由 Puppeteer 在浏览器控制台内以编程方式执行。浏览器控制台是一个与 Node 截然不同的环境,Puppeteer 和 ElementHandles 都位于其中。为了弥合进程间的差距,回调evaluate、参数和返回值被序列化和反序列化。

这样做的结果是您无法像在page.waitForSelector('.strong, .section-body')浏览器内部尝试那样访问任何节点状态。page与浏览器处于完全不同的过程。(顺便说一句,document.querySelectorAll它是纯粹同步的,所以没有意义await。)

Puppeteer ElementHandles是用于挂钩页面 DOM 的复杂结构,无法像您尝试的那样序列化并传递到页面。Puppeteer 必须在后台执行翻译。任何传递给evaluate(或.evaluate()调用它们)的 ElementHandles 都会跟随它们所代表的浏览器中的 DOM 节点,并且该 DOM 节点就是evaluate调用您的回调的对象。截至撰写本文时,Puppeteer 无法使用嵌套的 ElementHandles 来执行此操作。

可能的修复

在上面的代码中,如果更改.$$.$,则将仅检索第一个<li>。这个单一的、非嵌套的 ElementHandle 可以转换为一个元素:

// ...
  const handle = await page.$("li");
  const val = await page.evaluate(el => el.innerText, handle);
  console.log(val); // => a
// ...
Run Code Online (Sandbox Code Playgroud)

或者:

const handle = await page.$("li");
const val = await handle.evaluate(el => el.innerText);
console.log(val); // => a
Run Code Online (Sandbox Code Playgroud)

要在您的示例中实现此功能,只需交换循环和调用evaluate以便courseCountArr[i]在 Puppeteer 中访问,将嵌套的 ElementHandles 解压到单独的参数中evaluate或者将大部分控制台浏览器调用移回 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
}
Run Code Online (Sandbox Code Playgroud)

要获得一系列结果,您可以执行以下操作:

const nestedHandles = await page.$$("li");
const vals = await Promise.all(
  nestedHandles.map(el => el.evaluate(el => el.innerText))
);
console.log(vals); // [ 'a', 'b', 'c' ]
Run Code Online (Sandbox Code Playgroud)

您还可以将 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' ]
Run Code Online (Sandbox Code Playgroud)

如果除了句柄之外还有其他参数,您可以执行以下操作:

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' ]
Run Code Online (Sandbox Code Playgroud)

或者:

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' ]
Run Code Online (Sandbox Code Playgroud)

另一种选择可能是使用$$eval,它选择多个句柄,然后在浏览器上下文中运行回调,并将所选元素的数组作为其参数:

const vals = await page.$$eval("li", els => 
  els.map(e => e.innerText)
);
console.log(vals); // => [ 'a', 'b', 'c' ]
Run Code Online (Sandbox Code Playgroud)

如果您没有对 Node.js 中的句柄执行任何其他操作,这可能是最干净的。

同样,您可以完全绕过 Puppeteer 并在浏览器上下文中完成整个选择和操作:

const vals = await page.evaluate(() =>
  [...document.querySelectorAll("li")].map(e => e.innerText)
);
console.log(vals); // => [ 'a', 'b', 'c' ]
Run Code Online (Sandbox Code Playgroud)

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