我正在和 puppeteer 师一起抓取动态网站.我的目标是能够创建尽可能多的通用抓取逻辑,这也将删除大量模板代码.因此,我创建了外部函数,该函数在给定某些参数的情况下抓取数据.问题was是,当我try 在page. evergue()puppeteer方法中使用该函数时,我遇到了Reference错误,表明该函数未定义.

进行了一些研究,page. exposeValue然而,当我try 在我的scraper中使用它们时,addWritttag()不起作用,而且exposeStep()也没有为我提供访问公开函数内的多姆元素的能力.我知道exposeCopy()正在Node.js中执行,而addWritttag()则在浏览器中执行,但我不知道如何进一步处理该信息,也不知道它对我的情况是否有价值.

这是我的铲子:

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);时,它告诉我title. queryspel不是一个函数(在dataMapper内部).对于await page.addScriptTag({ content: `${dataMapper}` });,它稍后会在page. evalve中抛出错误,即dataMapper不是一个函数.

更新:当指定addWritttag内的路径时,它仍然给我:Error [ReferenceError]: dataMapper is not defined * 只想说mainCategoryScraper * is later on used in scrapersHandler function, which decides what scraper to be executed, based on URL endpoint.

推荐答案

作为discussed in my comment,这里的方法似乎相当复杂.我警告不要过早抽象.

一般来说,一旦您需要添加以前没有的多个条件(switchif),您可能会走上错误的道路.这些都会增加代码的认知复杂性.如果函数降低了调用者的复杂性,那么它的复杂性是可以接受的,但如果函数的契约不清楚,那么抽象可能会将leak个问题反馈给调用者.

将所有逻辑打包到您的dataMapper函数中会违反single responsibility principle并使其无法维护,因为您需要通过其他类型的 struct 进一步增加它的负担.该功能内的控制流程已经很难掌握,并且无法以任何合理的方式扩展.调用者应该负责对要抓取的 struct 进行显式编码,而不是try 编写无法为这些 struct 编写的一体化函数.

另一个经验法则:如果因子分解很困难,那么就保持重复.或者退后一步,try 编写不同的抽象,无论是比第一次try 更高还是更低的级别.

在这种情况下,您可能会编写几个更高级的抽象$$evalMap$text,这可以让您更干净地编写数据映射器.这些抽象只是清除了一些语法,但并不试图用条件概括抓取不同的 struct .

const puppeteer = require("puppeteer"); // ^22.7.1

const url = "https://www.ozone.bg/pazeli-2d-3d/nastolni-igri";

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 = [
    "image",
    "fetch",
    "other",
    "ping",
    "stylesheet",
    "xhr",
  ];
  page.on("request", req => {
    if (
      !req.url().startsWith("https://www.ozone.bg") ||
      blockedResources.includes(req.resourceType())
    ) {
      req.abort();
    } else {
      req.continue();
    }
  });
  await page.evaluateOnNewDocument(
    "window.$text = (el, s) => el.querySelector(s)?.textContent.trim();"
  );
  await page.goto(url, {waitUntil: "domcontentloaded"});

  const $$evalMap = async (sel, mapFn) => {
    await page.waitForSelector(sel);
    return page.$$eval(
      sel,
      (els, mapFn) => els.map(new Function(`return ${mapFn}`)()),
      mapFn.toString()
    );
  };

  const carouselData = await $$evalMap(".owl-item", el => ({
    title: $text(el, ".title"),
    img: el.querySelector("img").src,
    url: el.querySelector("a").href,
  }));

  const widgetData = await $$evalMap(".widget-box", el => ({
    title: el.querySelector("img").alt,
    img: el.querySelector("img").src,
    url: el.querySelector("a").href,
  }));

  const sliderData = await $$evalMap(
    ".item.slick-slide",
    el => ({
      title: $text(el, "h3"),
      subTitle: $text(el, "h6"),
      img: el.querySelector("img").src,
      url: el.querySelector("a").href,
    })
  );

  console.log(carouselData);
  console.log(widgetData);
  console.log(sliderData);
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

如果TypScript类型妨碍了,请考虑将querySelector移出到与$text类似的帮助器函数中.$$evalMap个调用还可以移出到每个部分(scrapeCarouselDatascrapeWidgetDatascrapeSliderData等)的各个功能.这样做需要$$evalMap接受page参数,但如果您要分解函数,那么无论如何它可能都没有必要,因为复杂性是隐藏的--普通的$$eval似乎也完全可以接受的,尤其是如果只有三个参数.

将这main个IIFE分解为子功能很简单:

const puppeteer = require("puppeteer");

const $$evalMap = async (page, sel, mapFn) => {
  await page.waitForSelector(sel);
  return page.$$eval(
    sel,
    (els, mapFn) => els.map(new Function(`return ${mapFn}`)()),
    mapFn.toString()
  );
};

const scrapeCarouselData = page =>
  $$evalMap(page, ".owl-item", el => ({
    title: $text(el, ".title"),
    img: el.querySelector("img").src,
    url: el.querySelector("a").href,
  }));

const scrapeWidgetData = page =>
  $$evalMap(page, ".widget-box", el => ({
    title: el.querySelector("img").alt,
    img: el.querySelector("img").src,
    url: el.querySelector("a").href,
  }));

const scrapeSliderData = page =>
  $$evalMap(page, ".item.slick-slide", el => ({
    title: $text(el, "h3"),
    subTitle: $text(el, "h6"),
    img: el.querySelector("img").src,
    url: el.querySelector("a").href,
  }));

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  // ... same optimization code,  could be moved out to a set up func
  const url = "https://www.ozone.bg/pazeli-2d-3d/nastolni-igri";
  await page.evaluateOnNewDocument(
    "window.$text = (el, s) => el.querySelector(s)?.textContent.trim();"
  );
  await page.goto(url, {waitUntil: "domcontentloaded"});
  console.log(await scrapeCarouselData(page));
  console.log(await scrapeWidgetData(page));
  console.log(await scrapeSliderData(page));
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

$$evalMap有点难看,但可以移出到位于另一个文件中的导入实用函数.抓取器依赖于现有的$text个,因此一些共享/全局设置可能是它存在的地方,可能还有第一个片段中的优化代码.

现在主代码相当干净,每个抓取功能都很容易维护.

编写类似的抓取函数将遵循既定模式.如果某个特定部分不能很好地遵守$$evalMap范式,那没问题--它应该使用自己的定制逻辑,而不是试图将其塞进带有条件的现有功能之一.

总结和进一步 comments :

  • 避免premature abstractions.
  • 当因子分解时,如果您引入了以前没有的多个条件,请停止.switch/case/break特别令人讨厌.如果您的抽象比原始重复的代码更冗长、更难以理解,请不要执行它们,或者try 找到不同的抽象.
  • 分解时,首先以冗长的方式编写,然后try 抽象出相似之处(但稍微重复是可以接受的--诚实地说什么更容易阅读和维护).
  • TS中不鼓励as.尽可能少地使用它,以支持变量类型.
  • 一直使用$$eval.它是Puppeteer中最常用的抓取函数,可以避免丑陋的Array.from(document.querySelectorAll)或元素手柄.如果Puppeteer的定位器API将来成熟,$$eval可能会被取代,但目前这是一条路要走.
  • ?? undefined没有必要.如果左手连锁运算符?.失败,则该表达无论如何都会判断为undefined,因此undefined ?? undefined毫无意义.
  • 一般来说,你不需要addScriptTagexposeFunction.如果您经常编写类似$text的抽象,您可以使用jQuery或类似的东西来简化查询.
  • 在网络抓取中,有no silver bullets for selection个,因此在试图概括时要非常谨慎--这并非不可能,但需要大量的谨慎和针对具体 case 的规划.

Javascript相关问答推荐

在nodejs中使用快速路由的API路径

在JS中转换mysql UTC时间字段?

如何从JavaScript中的公共方法调用私有方法?

如何在加载的元数据上使用juserc和await中获得同步负载?

Angular:ng-contract未显示

获取加载失败:获取[.]添加时try 将文档添加到Firerestore,Nuxt 3

如何最好地从TypScript中的enum获取值

Cypress -使用commands.js将数据测试id串在一起失败,但在将它们串在一起时不使用命令有效

Cookie中未保存会话数据

将状态向下传递给映射的子元素

Next.js(react)使用moment或不使用日期和时间格式

如何将Openjphjs与next.js一起使用?

单个HTML中的多个HTML文件

使用getBorbingClientRect()更改绝对元素位置

如何使用JS创建一个明暗功能按钮?

在JS中动态创建对象,并将其追加到HTML表中

如何在Press上重新启动EXPO-AV视频?

删除元素属性或样式属性的首选方法

图表4-堆叠线和条形图之间的填充区域

如果对象中的字段等于某个值,则从数组列表中删除对象