Я использую "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.
Любое предложение о том, что я делаю неправильно?
Я ценю ваши ответы!



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


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