NodeJS 是否可以使用mutationObserver来检测DOM中的新子节点,并向puppeteer返回一个elementHandle对象?

xytpbqjk  于 2023-05-22  发布在  Node.js
关注(0)|答案(1)|浏览(151)

使用mutantObserver检测DOM中的新节点,并通过puppetteer中的page.exposeFunction返回elementHandle。
有没有人知道是否可以使用mutationObserver,以便当DOM的一部分中创建新的子对象时,它会检测到它并将elementHandle对象作为结果发送给puppeteerexposeFunction方法进行处理?我正在使用它来提取添加的每个节点的上下文,并继续对它进行刮取。如果有人能帮助我,我将不胜感激。现在我已经尝试了以下方法:(选择器是每张牌的选择器)。

await page.exposeFunction('getItem', function(element) {
      // const elementHandle = await page.evaluateHandle((el) => el, node);
      console.log('Se agrego una tarjeta a la cola.'.blue);
      console.log(element);
      tarjetaQueue.push(element);
      
    });

    await page.evaluate((selector, page) => {
    // Observar el contenedor de tarjetas para detectar nuevas tarjetas agregadas al DOM
    const observer = new MutationObserver(async mutationsList => {
      console.log(selector);
      try{
        for (const mutation of mutationsList) {
          if (mutation.type === 'childList' && mutation.addedNodes.length) {
            for (const node of mutation.addedNodes) {
              const element = node.querySelector(selector);
              getItem(element);
            }
          }
        }
      } catch (error) {
        console.log("Error en mutation observer. ".red + error);
        return null;
      }
    }, selector, page);

    try{
      const contenedorFeed = document.querySelector('#sections[section-identifier="comment-item-section"]:not([static-comments-header]) #contents.style-scope.ytd-item-section-renderer');
      observer.observe(contenedorFeed, { childList: true });
    }catch(error){
      console.log("No se encontraron TARJETONAS. ".red + error);
    }
  });

我需要从每个注解中得到一个elementHandle,就像通过执行以下操作获得的一样:elementHandle = await comment.$(selector),我不知道这是否可以通过mutantObserver实现,因为我的代码使用了那些

p8h8hvxi

p8h8hvxi1#

你的目标并不完全清楚,更多的背景将有助于避免xy问题。您正在询问尝试的解决方案,但它可能不是解决潜在问题的最佳方法,可能是抓取一些数据。但根据讨论,我有几点意见,可以尝试为您指出一个更可行的方法:

  • exposeFunction不能处理DOM节点。DOM节点是不可序列化的,因此当您尝试在Node中使用它们时,它们将反序列化为空对象。99.9%的情况下,您不需要此功能。如果您确实想使用它将数据传递到Node,请在浏览器中处理数据,以便它是可序列化的。
  • Puppeteer已经在MutationObserver之上提供了方便的 Package 器,如waitForFunctionwaitForSelector,因此99.9%的时间使用Puppeteer库,而不是安装自己的低级观察器。即使你真的需要滚动自己的监听器,你也可以使用requestAnimationFramesetTimeout作为轮询循环,这在语法上更简单,假设性能不是关键的(你可以从RAF开始,然后升级到观察者,一旦你有了基本的工作)。
  • 尽量避免使用元素句柄,除非别无选择或确实需要可信事件。如果您需要执行除单击或键入之外的任何操作,则使用它们进行编码会很笨拙,并且可能导致争用条件。$$eval$eval是提取数据的更直接的方法。

现在,YouTube是出了名的烦人刮,有成千上万的评论,你所显示的示例页面。我建议尽可能避免使用它们的DOM,在本例中,这似乎是可能的。您可以监视网络请求并捕获向下滚动页面时返回的JSON有效负载。我在滚动时删除DOM节点,以避免注解列表变长时性能下降。
我使用的是一个简单的测试用例,因此脚本将在合理的时间内完成。您可能希望在工作时将数据写入磁盘,然后在以后脱机处理它。我加入了一些预处理,以展示如何遍历结果结构以获得所需的任何数据。
有几个超时,不是很好的练习,但是我没有时间用真正的 predicate 来替换它们。我以后再谈这个。

const fs = require("node:fs/promises");
const puppeteer = require("puppeteer"); // ^20.2.1
require("util").inspect.defaultOptions.depth = null;

let browser;
(async () => {
  browser = await puppeteer.launch({headless: true});
  const [page] = await browser.pages();
  const ua =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36";
  await page.setUserAgent(ua);
  //const url = "https://www.youtube.com/watch?v=XCUZSS54drI"; // the real deal
  const url = "https://www.youtube.com/watch?v=N9RUqGYuGfw"; // smaller test URL
  await page.setRequestInterception(true);

  const results = [];
  let lastChunk = Date.now();
  page.on("response", async res => {
    if (
      !res
        .request()
        .url()
        .startsWith(
          "https://www.youtube.com/youtubei/v1/next?key="
        )
    ) {
      return;
    }

    lastChunk = Date.now();
    const data = await res.json();
    results.push(
      ...data.onResponseReceivedEndpoints
        .flatMap(e =>
          (
            e.reloadContinuationItemsCommand ||
            e.appendContinuationItemsAction
          ).continuationItems.flatMap(
            e => e.commentThreadRenderer?.comment.commentRenderer
          )
        )
        .filter(Boolean)
    );
    console.log("scraped so far:", results.length);
  });

  const blockedResourceTypes = [
    "xhr",
    "image",
    "font",
    "media",
    "other",
  ];
  const blockedUrls = [
    "gstatic",
    "accounts",
    "googlevideo",
    "doubleclick",
    "syndication",
    "player",
    "web-animations",
  ];
  page.on("request", req => {
    if (
      blockedResourceTypes.includes(req.resourceType()) ||
      blockedUrls.some(e => req.url().includes(e))
    ) {
      req.abort();
    } else {
      req.continue();
    }
  });

  await page.goto(url, {
    waitUntil: "domcontentloaded",
    timeout: 60_000,
  });

  // not good, but page context is destroyed with redirects
  await new Promise(r => setTimeout(r, 20_000));

  const scroll = () =>
    page.evaluate(() => {
      const scrollingElement =
        document.scrollingElement || document.body;
      scrollingElement.scrollTop = scrollingElement.scrollHeight;
    });

  lastChunk = Date.now();
  while (Date.now() - lastChunk < 30_000) {
    await scroll();
    await page.$$eval("ytd-comment-thread-renderer", els =>
      els.length > 80 && els.slice(0, -40).forEach(e => e.remove())
    );
  }

  await fs.writeFile(
    "comments.json",
    JSON.stringify(results, null, 2)
  );
  const simpleResults = results.map(e => ({
    author: e.authorText.simpleText,
    text: e.contentText.runs.find(
      e => Object.keys(e).length === 1 && e.text?.trim()
    )?.text,
  }));
  await fs.writeFile(
    "simple-comments.json",
    JSON.stringify(simpleResults, null, 2)
  );
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

请注意,由于issue #10033,您需要避免使用18.2.1和20.0.0之间的Puppeteer版本来自动化YouTube。

相关问题