Нажатие кнопки согласия с кукловодом

Я использую "puppeteer": "^19.11.1",:

Я создал эту функцию, чтобы нажать кнопку согласия на этой странице:

Это моя функция:

async function handleConsent(page, logger) {
    const consentButtonSelector =
        '#uc-center-container > div.sc-eBMEME.ixkACg > div > div > div > button.sc-dcJsrY.bSKKNx';


    try {
        // Wait for the iframe to load
        await page.waitForSelector("iframe", { timeout: 3000 });

        // Identify the iframe that contains the consent button
        const iframeElement = await page.$(
            'iframe[name = "__tcfapiLocator"]'
        );
        if (iframeElement) {
            const iframeContent = await iframeElement.contentFrame();

            // Attempt to click the consent button within the iframe
            const consentButton = await iframeContent.$(consentButtonSelector);
            if (consentButton) {
                await iframeContent.click(consentButtonSelector);
                logger.info("Consent button clicked inside the iframe.");
            } else {
                logger.info("Consent button not found inside the iframe.");
            }
        } else {
            logger.info("Iframe with the consent message not found.");
        }

        await page.waitForTimeout(3000); // Wait for any potential redirects or updates after clicking
    } catch (error) {
        logger.error(`An error occurred while handling consent: ${error}`);
    }
}

Моя проблема в том, что селектор не найден, хотя я пытаюсь выбрать iframe.

Любое предложение о том, что я делаю неправильно?

Я ценю ваши ответы!

Чего вы в конечном итоге пытаетесь достичь на странице после нажатия кнопки согласия? Часто вам не нужно нажимать кнопки согласия, тем более что данные находятся прямо в статическом HTML. Таким образом, вам возможно, здесь даже не понадобится Puppeteer и это может быть xy-проблемой.

ggorlen 07.04.2024 16:27
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
1
135
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Кнопка согласия не помещается в iframe. Он находится внутри #shadow-root. Чтобы получить к нему доступ, вам нужно сначала получить его хост, а затем получить свойство shadowRoot, и только тогда вы сможете получить к нему доступ.

Селектор теневого хоста: #usercentrics-root

Однако согласие отображается асинхронно, поэтому после получения хоста кнопка «Принять» может еще не отображаться, поэтому вам нужно дождаться появления кнопки согласия, например, реализовав функцию waitFor внутри блока оценки.

После нажатия кнопки рекомендуется подождать, пока теневой хост согласия не будет скрыт.

Подробнее о shadowDOM

    const url =
      "https://www.immowelt.at/suche/wien/wohnungen/kaufen?d=true&pma=500000&sd=DESC&sf=TIMESTAMP&sp=1";
    await page.goto(url);
    const shadowHost = await page.waitForSelector('#usercentrics-root');
    await shadowHost.evaluate(async el => {
      let waitFor = async (action, timeoutMs = 10000, pollIntervalMs = 500) => {
        let isTimeout = false;
        const timeoutId = setTimeout(() => {
          isTimeout = true;
        }, timeoutMs);
        let result;
        while (!isTimeout && !result) {
          result = await action();
          await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
        }

        clearTimeout(timeoutId);
        if (isTimeout) {
          throw new Error('Timeout exceeded!');
        } else {
          return result;
        }
      };
      const accept = await waitFor(() => el.shadowRoot!.querySelector('[data-testid=uc-accept-all-button]'));
      accept.click();
    });
    await page.waitForSelector('#usercentrics-root', {hidden: true});
Ответ принят как подходящий

Этот ответ является правильным, поскольку указывает на то, что нужный вам элемент находится в теневом корне, но следует избегать решения, которое он предоставляет. Вы можете использовать >>>, чтобы легко проколоть теневые корни в Puppeteer:

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

const url = "<Your URL>";

let browser;
(async () => {
  browser = await puppeteer.launch({headless: false});
  const [page] = await browser.pages();
  await page.goto(url, {waitUntil: "domcontentloaded"});
  const acceptBtnSelector = ">>> [data-testid='uc-accept-all-button']";
  const accept = await page.waitForSelector(acceptBtnSelector);
  await accept.click();
  await page.waitForSelector(acceptBtnSelector, {hidden: true});
  await page.screenshot({path: "proof.png"})
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

И даже если вы не можете использовать >>> и waitForSelector здесь, page.waitForFunction предпочтительнее, чем переписывать опрос с нуля, который сложно поддерживать и который ненадежен.

Однако я уверен, что ваша фактическая цель на сайте — это не просто нажать кнопку «Принять» ради этого. Ваша реальная цель, скорее всего, состоит в сборе данных. Но большая часть критически важных данных на странице уже присутствует в статическом HTML, а не отображается асинхронно, поэтому вы сможете легко очистить их без JS или нажатия каких-либо кнопок:

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

const url = "<Your URL>";

let browser;
(async () => {
  browser = await puppeteer.launch();
  const [page] = await browser.pages();
  await page.setJavaScriptEnabled(false);
  await page.setRequestInterception(true);
  page.on("request", req =>
    req.url() === url ? req.continue() : req.abort()
  );
  await page.goto(url, {waitUntil: "domcontentloaded"});
  const data = await page.$$eval(
    '[class^ = "EstateItem"]',
    els =>
      els.map(el => {
        const text = s => el.querySelector(s).textContent.trim();
        return {
          title: text("h2"),
          price: text('[data-test = "price"]'),
          area: text('[data-test = "area"]'),
          rooms: text('[data-test = "rooms"]'),
          location: text('[class^ = "estateFacts"] span'),
          locationDetail: text(
            '[class^ = "estateFacts"] div:nth-of-type(2) span'
          ),
          provider: text('[class^ = "ProviderName"]'),
          picture: el
            .querySelector("img")
            .getAttribute("data-src"),
        };
      })
  );
  console.info(data);
  console.info(data.length);
})()
  .catch(err => console.error(err))
  .finally(() => browser?.close());

Выходной фрагмент:

[
  {
    title: 'Pärchentraum - Charmante 2 Zimmer Neubauwohnung in beliebter Wohngegend - Nahe Perchtoldsdorfer Heide!',
    price: '430.000 €',
    area: '59.79 m²',
    rooms: '2 Zi.',
    location: 'Wien,Liesing (Liesing)',
    locationDetail: 'Erstbezug, Neubau, Bad mit Wanne, ...',
    provider: 'Bero Immobilien GmbH',
    picture: 'https://ms.immowelt.org/7a82dc6d-4484-4b90-9377-9cd9d2f85c49/404cf877-f3bc-4912-ad33-1f55b1b771bd/328x224.jpg'
  },
  {
    title: 'Singlehit! Charmante 2 Zimmer-Neubauwohnung in beliebter Wohngegend Liesing`s',
    price: '299.000 €',
    area: '41.23 m²',
    rooms: '2 Zi.',
    location: 'Wien,Liesing (Liesing)',
    locationDetail: 'Erstbezug, Neubau, Loggia',
    provider: 'Bero Immobilien GmbH',
    picture: 'https://ms.immowelt.org/db352cce-9737-4e0d-85cf-9198af4f16aa/1e789a42-64a8-4500-8c50-6656f0dae60c/328x224.jpg'
  },
// ...
20

Это иллюстрирует распространенный антипаттерн в веб-скрапинге, который предполагает, что вам нужно вести себя так, как вел бы пользователь, с включенным JS и послушно нажимая кнопки. В большинстве случаев существует более прямой подход, который быстрее запускается и пишется и более надежен — в основном лучше по любым показателям.

На этом этапе вы можете даже полностью пропустить Puppeteer и использовать собственный fetch и облегченный парсер HTML, такой как Cheerio:

const cheerio = require("cheerio"); // ^1.0.0-rc.12

const url = "<Your URL>";

fetch(url)
  .then(res => {
    if (!res.ok) {
      throw Error(res.statusText);
    }

    return res.text();
  })
  .then(html => {
    const $ = cheerio.load(html);
    const data = [...$('[class^ = "EstateItem"]')].map(e => {
      const text = s => $(e).find(s).text().trim();
      return {
        title: text("h2"),
        price: text('[data-test = "price"]'),
        area: text('[data-test = "area"]'),
        rooms: text('[data-test = "rooms"]'),
        location: text('[class^ = "estateFacts"] span'),
        locationDetail: text(
          '[class^ = "estateFacts"] div:nth-of-type(2) span'
        ),
        provider: text('[class^ = "ProviderName"]'),
        picture: $(e).find("img").attr("data-src"),
      };
    });
    console.info(data);
  })
  .catch(err => console.error(err));

Вывод тот же, но Cheerio быстрее:

# optimized puppeteer:
real 0m2.015s
user 0m0.691s
sys  0m0.139s

# fetch/cheerio:
real 0m0.804s
user 0m0.282s
sys  0m0.044s

Если вы хотите очистить несколько страниц, просто добавьте цикл в нумерацию URL-адресов, а не взаимодействуйте с пользовательским интерфейсом:

const cheerio = require("cheerio");

const url = "<Your Base URL>&sp = "; // note the removed `sp=` page

const get = url =>
  fetch(url)
    .then(res => {
      if (!res.ok) {
        throw Error(res.statusText);
      }
  
      return res.text();
    });

(async () => {
  const data = [];

  for (let page = 1; page < 10 /* for testing */; page++) {
    const $ = cheerio.load(await get(url + page));
    const chunk = [...$('[class^ = "EstateItem"]')].map(e => {
      const text = s => $(e).find(s).text().trim();
      return {
        title: text("h2"),
        price: text('[data-test = "price"]'),
        area: text('[data-test = "area"]'),
        rooms: text('[data-test = "rooms"]'),
        location: text('[class^ = "estateFacts"] span'),
        locationDetail: text(
          '[class^ = "estateFacts"] div:nth-of-type(2) span'
        ),
        provider: text('[class^ = "ProviderName"]'),
        picture: $(e).find("img").attr("data-src"),
      };
    });

    if (!chunk.length) {
      break;
    }

    data.push(...chunk);
  }

  console.info(JSON.stringify(data, null, 2));
  console.info(data.length);
})()
  .catch(err => console.error(err));

См. Puppeteer не предоставляет точный HTML-код для страницы с теневыми корнями для подробного обзора теневых корней в Puppeteer.

Disclosure: I'm the author of the linked blog post.

Другие вопросы по теме