我正在用 puppeteer 抓取动态网站。我的目标是能够创建尽可能多的通用抓取逻辑,这也将删除大量样板代码。因此,出于这个原因,我创建了外部函数,在给定某些参数的情况下抓取数据。问题是当我尝试在 page.evaluate() puppeteer 方法中使用该函数时,我遇到了一个 ReferenceError ,即该函数未定义。
做了一些研究, page.exposeFunction() 和 page.addScriptTag() 作为可能的解决方案出现。然而,当我尝试在我的 scraper 中使用它们时,addScriptTag() 不起作用,并且 hideFunction() 没有让我能够访问公开函数中的 DOM 元素。我知道 hideFunction() 是在 Node.js 内部执行的,而 addScriptTag() 在浏览器中执行的,但我不知道如何进一步处理这些信息,也不知道它对我的情况是否有价值。
这是我的刮刀:
import { Browser } from "puppeteer";
import { dataMapper } from "../../utils/api/functions/data-mapper.js";
export const mainCategoryScraper = async (browser: Browser) => {
const [page] = await browser.pages();
await page.setUserAgent(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36"
);
await page.setRequestInterception(true);
page.on("request", (req) => {
if (
req.resourceType() === "stylesheet" ||
req.resourceType() === "font" ||
req.resourceType() === "image"
) {
req.abort();
} else {
req.continue();
}
});
await page.goto("https://www.ozone.bg/pazeli-2d-3d/nastolni-igri", {
waitUntil: "domcontentloaded",
});
/**
* Function will execute in Node.js
*/
// await page.exposeFunction('dataMapper', dataMapper);
/**
* The way of passing DOM elements to the function, because like that the function executes in the browser
*/
// await page.addScriptTag({ content: `${dataMapper}` });
const data = await page.evaluate(async () => {
const contentContainer = document.querySelector(".col-main") as HTMLDivElement;
const carousels = Array.from(
contentContainer.querySelectorAll(".owl-item") as NodeListOf<HTMLDivElement>
);
const carouselsData = await dataMapper<HTMLDivElement>(carousels, ".title", "img", "a");
return {
carouselsData,
};
});
await browser.close();
return data;
};
这是 dataMapper 函数:
import { PossibleTags } from "../typescript/types.js";
export const dataMapper = function <T extends HTMLDivElement>(items: Array<T>, ...selectors: string[]) {
let hasTitle = false;
for (const selector of selectors) {
if (selector === ".title" || selector === "h3") {
hasTitle = true;
break;
}
}
return items.map((item) => {
const data: PossibleTags = {};
return selectors.map((selector) => {
const dataProp = item.querySelector(selector);
switch (selector) {
case ".title": {
data["title"] = (dataProp as HTMLSpanElement)?.innerText;
break;
}
case "h3": {
data["title"] = (dataProp as HTMLHeadingElement)?.innerText;
break;
}
case "h6": {
data["subTitle"] = (dataProp as HTMLHeadingElement)?.innerText;
break;
}
case "img": {
if (!hasTitle) {
data["img"] = (dataProp as HTMLImageElement)?.getAttribute("src") ?? undefined;
break;
}
data["title"] = (dataProp as HTMLImageElement)?.getAttribute("alt") ?? undefined;
break;
}
case "a": {
data["url"] = (dataProp as HTMLAnchorElement)?.getAttribute("href") ?? undefined;
}
default: {
throw new Error("Such selector is not yet added to the possible selectors");
}
}
});
});
};
当我使用
page.exposeFunction('dataMapper', dataMapper);
时,它告诉我 item.querySelector 不是一个函数(在 dataMapper 内部)。对于 await page.addScriptTag({ content: `${dataMapper}` });
,它只是稍后在页面内抛出错误。评估,dataMapper 不是一个函数。
更新:在 addScriptTag 内指定路径时,它仍然给我:
Error [ReferenceError]: dataMapper is not defined
*
需要提一下的是 mainCategoryScraper * 稍后会在 scrapersHandler 函数中使用,该函数根据 URL 端点决定要执行的 scraper。
正如我在评论中讨论的那样,这里的方法似乎相当复杂。我警告不要过早抽象。 只需抓取数据即可:
const puppeteer = require("puppeteer"); // ^22.7.1
const url = "<Your URL>";
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
const ua =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36";
await page.setUserAgent(ua);
await page.setRequestInterception(true);
const blockedResources = [
"stylesheet",
"image",
"other",
"fetch",
"xhr",
"ping",
];
page.on("request", req => {
if (
!req.url().startsWith("https://www.ozone.bg") ||
blockedResources.includes(req.resourceType())
) {
req.abort();
} else {
req.continue();
}
});
await page.goto(url, {waitUntil: "domcontentloaded"});
await page.waitForSelector(".owl-item");
const data = await page.$$eval(".owl-item", els =>
els.map(el => ({
title: el.querySelector(".title")?.textContent,
img: el.querySelector("img").src,
url: el.querySelector("a").href,
oldPrice: el
.querySelector(".old-price .price")
?.textContent.trim(),
specialPrice: el
.querySelector(".special-price .price")
?.textContent.trim(),
}))
);
console.log(data);
})()
.catch(err => console.error(err))
.finally(() => browser?.close());
如果你想使其通用,可能有比
addScriptTag
更好的方法(
exposeFunction
通常对于提取数据没有用处,因为你无法从中访问 DOM)。首先,共享一个页面,该页面激发了 h6 和您需要处理的其他变体的存在,然后考虑提取适当的抽象,并可能在单个 $$eval
调用中完成所有工作。有一些技术可以做到这一点,但它们取决于用例。