是否可以使用 mutationObserver 检测 DOM 中的新子节点并将 elementHandle 对象返回给 puppeteer?

问题描述 投票:0回答:1

使用 mutantObserver 检测 DOM 中的新节点,并返回一个 elementHandle trough page.exposeFunction in puppetteer.

有谁知道是否可以使用 mutationObserver,这样当在 DOM 的一部分中创建一个新的孩子时,它会检测到它并发送一个 elementHandle 对象作为结果到 puppeteer exposeFunction 方法是处理? 我正在使用它来提取添加的每个节点的上下文,并继续对其进行抓取。 如果有人可以帮助我,我将不胜感激。现在我尝试了以下方法: (选择器是每张卡片的选择器)。

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 实现,因为我的代码与这些一起工作

node.js web-scraping dom puppeteer mutation-observers
1个回答
0
投票

您的目标并不完全清楚,更多的上下文将有助于避免 xy 问题。你问的是你尝试过的解决方案,但这可能是一种根本上很糟糕的方法。但是根据讨论,我有几点评论,可以尝试为您指出一种更可行的方法:

  • exposeFunction
    无法使用 DOM 节点。 DOM 节点不可序列化,因此当您尝试在 Node 中使用它们时,它们会反序列化为空对象。 99.9% 的时候,你不需要这个功能。如果您确实想使用它来将数据传递给 Node,请在浏览器中处理数据以使其可序列化。
  • Puppeteer 已经在
    MutationObserver
    之上提供了方便的包装器,例如
    waitForFunction
    waitForSelector
    ,所以 99.9% 的时间使用 Puppeteer 库而不是安装你自己的低级观察者。即使当您真的需要启动自己的侦听器时,您通常也可以使用
    requestAnimationFrame
    setTimeout
    作为轮询循环,这在语法上更简单,假设性能不是关键(您可以从 RAF 开始,然后一旦你掌握了基础知识,就升级为观察员)。
  • 尽量避免使用元素句柄,除非你别无选择或确实需要可信事件。如果您需要执行除单击或键入之外的任何操作,那么使用它们进行编码会很尴尬,并且可能导致竞争条件。
    $$eval
    $eval
    是更直接的提取数据的方法。

现在,YouTube 是出了名的难以抓取,并且在您显示的示例页面上有 5k 多条评论。我建议尽可能避免他们的 DOM,在这种情况下,这似乎是可能的。您可以监视网络请求并捕获向下滚动页面时返回的 JSON 负载。我在滚动时删除了 DOM 节点,以避免在评论列表变长时性能下降。

我在收到数据时写入数据,这样如果它失败了,我至少有部分数据。通常,我建议将所有数据写入磁盘,然后稍后离线处理,但我会进行一些预处理以展示您如何遍历结果结构以获得所需的任何数据。

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

let browser;
(async () => {
  browser = await puppeteer.launch({headless: false});
  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";
  await page.setRequestInterception(true);

  const results = [];
  let lastFound;
  page.on("response", async res => {
    if (
      !res
        .request()
        .url()
        .startsWith(
          "https://www.youtube.com/youtubei/v1/next?key="
        )
    ) {
      return;
    }
    lastFound = 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)
    );
    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,
    }));
    console.log(simpleResults.slice(-20), simpleResults.length);
    await fs.writeFile(
      "simple-comments.json",
      JSON.stringify(simpleResults, null, 2)
    );
  });

  const blockedResourceTypes = [
    "xhr",
    "image",
    "font",
    "media",
    "other",
  ];
  const blockedUrls = [
    "gstatic",
    "accounts",
    "googlevideo",
    "doubleclick",
    "syndication",
    "player",
  ];
  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: "networkidle2",
    timeout: 60_000,
  });

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

  lastFound = Date.now();

  while (
    results.length === 0 ||
    Date.now() - lastFound < 60_000
  ) {
    await scroll();
    await page.$$eval("ytd-comment-thread-renderer", els =>
      els.forEach(e => e.remove())
    );
  }
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

请注意,由于 issue #10033,我们现在需要使用已弃用的 Puppeteer 版本 (

^18.2.1
) 来自动化 YouTube。

© www.soinside.com 2019 - 2024. All rights reserved.