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 数组吗?传递page给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())
;
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)
(请注意,获取内部文本只是您可能拥有的任意复杂的浏览器代码的占位符)