Продвинутая работа с DOM в Puppeteer: отключение JavaScript, загрузка HTML без захода на сайт, действие при ошибках, ожидание и прокрутка страницы


Оглавление

1. Как пользоваться Puppeteer: установка и быстрый старт

2. Работа с DOM в Puppeteer: как получить HTML код и извлечь различные теги (текст, картинки, ссылки и прочее)

3. Продвинутая работа с DOM в Puppeteer: отключение JavaScript, загрузка HTML без захода на сайт, действие при ошибках, ожидание и прокрутка страницы

3.1 Какое именно содержимое получает Puppeteer — исходный HTML или DOM после завершения работы JavaScript?

3.2 Как в Puppeteer выключить (остановить) все скрипты JavaScript

3.3 Как в Puppeteer получить исходный HTML вместо DOM после создания страницы с помощью JavaScript

3.4 Как в Puppeteer передать HTML страницы из строки (из базы данных)

3.5 Как в Puppeteer сохранить текущее состояние веб-страницы при возникновении ошибки

3.6 Как в Puppeteer приостановить выполнение скрипта и действий на некоторое время. Задержка работы скриптов Puppeteer

3.7 Как в Puppeteer сделать скриншот страницы с картинками если сайт использует lazy download

3.8 Как скроллировать страницу в Puppeteer

3.9 Как делать постепенный скроллинг страницы в Puppeteer

4.

5.

6.


Прежде чем перейти к следующим практическим примерам использования Puppeteer, давайте чуть углубимся в теоретические моменты работы Puppeteer и рассмотрим опции и действия, которые могут влиять на генерацию DOM веб-страницы.

Эти подсказки помогут вам лучше понимать работу Puppeteer. Также вы улучшите уже имеющиеся навыки по работе с Puppeteer, например, вы сможете:

  1. Отключать скрипты JavaScript;
  2. Выбирать, с чём вы хотите работать, с исходным HTML или текущим DOM;
  3. Улучшить внешний вид скриншотов, если на страницы включён lazy download для изображений;
  4. Загружать HTML код из базы данных или других источников, а не с сайта;
  5. Сохранить текущий статус DOM и сделать скриншот при возникновении ошибки;
  6. Приостанавливать работу JavaScript и Puppeteer на определённое время;
  7. Перемещаться вниз страницы или делать постепенный скроллинг страницы.

3.1 Какое именно содержимое получает Puppeteer — исходный HTML или DOM после завершения работы JavaScript?

Содержимое веб-страницы состоит из HTML кода, полученного от веб-сервера. Но современные технологии (HTML 5 и JavaScript) позволяют менять содержимое веб-страницы на лету: JavaScript может добавить новые элементы, изменить свойства имеющихся элементов, удалить элементы и выполнять самые разнообразные действия с ними. JavaScript может просто очистить весь полученный от веб-сервера HTML код и создать совершенно иную страницу.

HTML DOM (Document Object Model) это содержимое веб-страницы в текущий момент, после того, как JavaScript добавил, удалил или изменил элементы.

То есть при загрузки веб-страницы её DOM соответствует HTML коду полученному от сервера. В дальнейшем DOM может не меняться (для статических страниц), либо очень сильно меняться под воздействием JavaScript.


Возможно, вы уже обращали внимание, что для некоторых сайтов сохранённая веб-страница выглядит совершенно иначе, чем то, что вы видели у себя на экране. Это происходит по той причине, что выполняя операцию «Сохранить как» вы сохраняете HTML, который прислал сервер, но не сохраняете текущий DOM.

В вещественном смысле DOM представляет собой HTML, который формирует текущую версию веб-страницы.

Смотрите также: Как сохранить весь HTML DOM в его текущем состоянии

Если получить HTML код страницы, например, с помощью утилиты cURL (а также многих других инструментов, предназначенных для скачивания отдельных веб-страниц или создания локальный копий веб-сайтов), то будет скачен именно исходный HTML код.

Что именно выводит Puppeteer, когда мы выводили HTML или сохраняли веб-страницу? Puppeteer выводит HTML DOM как он выглядит после завершения работы JavaScript. То есть результат сохранения одной и той же страницы с помощью cURL и Puppeteer могут различаться (в первую очередь для сайтов, которые динамически создают веб-бстраницу).

Итак, если ваша цель получить HTML DOM сайта после завершения работы JavaScript, то Puppeteer выполнит эту задачу по умолчанию.

3.2 Как в Puppeteer выключить (остановить) все скрипты JavaScript

Чтобы выключить JavaScript в Puppeteer добавьте следующую опцию:

await page.setJavaScriptEnabled(false)

Эту опцию нужно добавлять до перехода на веб-страницу, поскольку если это сделать после, то опция не будет иметь никакого видимого эффекта, ведь JavaScript уже отработал.

Ниже будет дан пример, как это сделать.

3.3 Как в Puppeteer получить исходный HTML вместо DOM после создания страницы с помощью JavaScript

Чтобы получить исходный код веб-страницы (HTML) вместо DOM, отключите выполнение JavaScript следующим образом:

await page.setJavaScriptEnabled(false)

Пример запуска Puppeteer с отключённым JavaScript:

const puppeteer = require('puppeteer');
 
async function run() {
	const url = process.argv[2];
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
   
	await page.setUserAgent(customUserAgent);
	await page.setJavaScriptEnabled(false)
	await page.goto('https://hackware.ru/?p=19226');
	const html = await page.content();
	console.log(html);
	   
	browser.close();
}
 
run();

Давайте сравним вывод для одной и той же страницы с включённым JavaScript (по умолчанию) и с отключённым JavaScript.

Поиск указанных HTML тегов на странице с включённым JavaScript:

node js-en.js | grep -i -E --color '<pre|<code'

Мы не будем вдаваться в полученный результат, просто обратите внимание, что в полученном HTML коде найдены только теги CODE.


А теперь выполним поиск тех же самых HTML тегов на той же самой странице, но с выключенным JavaScript:

node js-dis.js | grep -i -E --color '<pre|<code'

Как можно увидеть, найдены только теги PRE.

Из полученных результатов можно сделать вывод: по умолчанию в HTML разметке используется тег PRE, но при формировании DOM страницы JavaScript значительно меняет HTML разметку, в том числе заменяет теги PRE на CODE.

3.4 Как в Puppeteer передать HTML страницы из строки (из базы данных)

Как в Puppeteer загрузить страницу не с сайта, а из базы данных или строки и для чего это нужно?

В Puppeteer вы можете передать содержимое веб-страницы в качестве HTML кода, без открытия URL адреса. Это может пригодиться, например, в следующих ситуациях:

  • вы хотите сделать скриншот для веб-страницы, которая будет сформирована HTML кодом (заменяет следующую последовательность действий: сохранить HTML в файл → открыть файл в веб-браузере → сделать скриншот)
  • вы хотите получить DOM страницы из HTML после завершения работы JavaScript

Для этого вы можете использовать метод Page.setContent(): https://pptr.dev/api/puppeteer.page.setcontent

Следующий скрипт передаёт HTML код в Puppeteer, а затем выводит получившийся DOM (который очень сильно отличается от исходного HTML — вы можете убедиться в этом, отключив JavaScript):

const puppeteer = require('puppeteer');
 
async function run() {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
	await page.setUserAgent(customUserAgent);
	//await page.setJavaScriptEnabled(false)    
	page.setContent(`<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="UTF-8" />
    <title>Working with elements</title>
  </head>
  <body>
    <div id="div1">The text above has been created dynamically.</div>
  </body>
</html>


<script>
source_html = document.documentElement.outerHTML

function addElement(value, content) {
	const newDiv = document.createElement(value);
	const newContent = document.createTextNode(content);
	newDiv.appendChild(newContent);
	const currentDiv = document.getElementById("div1");
	document.body.insertBefore(newDiv, currentDiv);
}

addElement('div', 'Hi there and greetings!');
addElement('h2', 'Original HTML code:');
addElement('pre', source_html);
addElement('ul', '');

for (let i = 0; i < 10; i++) {
	addElement('li', i);
}

dom = document.documentElement.outerHTML
addElement('h2', 'New HTML DOM:');
addElement('pre', dom);
</script>`)
	
	const html = await page.content();
	console.log(html);   
	browser.close();
}
 
run();

Данная страница не несёт особого смысла, главная её цель — показать, как сильно может различаться исходный HTML (слева) и конечный DOM (справа) для одной и той же страницы — на следующем скриншоте DOM не поместился в окно консоли (вы видите меньше половины):

3.5 Как в Puppeteer сохранить текущее состояние веб-страницы при возникновении ошибки

Очень скоро мы перейдём к взаимодействию с элементами веб-страницы из Puppeteer (говоря простым языком, мы будем нажимать кнопки и вводить данные в текстовые поля, а потом опять нажимать кнопки). Кстати, первую кнопку мы будем нажимать из Puppeteer уже в следующем примере.

Так вот, при возникновении ошибок, Puppeteer будет аварийно завершать работу и будет непонятно, что именно произошло. Как уже было показано выше, анализ HTML кода может не дать результатов, поскольку Puppeteer работает с временно сгенерированным DOM, который после завершения работы Puppeteer исчезает.

Как сделать так, чтобы при возникновении сбоя из-за ошибки Puppeteer сохраняла бы текущее состояние DOM и делала скриншот?


Для этого мы можем использовать конструкцию try-catch:

try {
} 
catch (error) {
}

Это конструкция языка JavaScript, а не библиотеки Puppeteer.


Пример фрагмента кода, который пытается выполнить действие, а если возникает ошибка, то выводит текст ошибки, делает скриншот веб-страницы и сохраняет текущее содержимое DOM в файл:

try {
// ACTIONS
} 
catch (error) {
	console.error(error);
	await page.screenshot({ path: 'fail.png', fullPage: true });
	const html = await page.content();
	const fs = require('fs');
	fs.writeFile("failed.htm", html, function(err) {
	if(err) {
		return console.log(err);
	}
	console.log("The failed file was saved!");
	});
}

Рассмотрим полный пример кода, который вызовет ошибку — мы пытаемся нажать не существующую на странице кнопку.

const puppeteer = require('puppeteer');

function delay(time) {
	return new Promise(function(resolve) { 
		setTimeout(resolve, time)
	});
} 
async function run() {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
   
	await page.setViewport({width: 1440, height: 3440});
	await page.setUserAgent(customUserAgent); 
	await page.goto('https://hackware.ru/?p=19226');
	
	for (let i = 0; i < 1; i++) { 
		await page.evaluate(() => {
			window.scrollTo(0, document.body.scrollHeight);
		});
		
		try {
			const button = await page.$('div > div.c-loadMoreButton.u-grid-columns > a');
			await button.click();
			await delay(4000);
		}
		catch (error) {
			console.error(error);
			await page.screenshot({ path: 'fail.png', fullPage: true });
			const html = await page.content();
			const fs = require('fs');
			fs.writeFile("failed.htm", html, function(err) {
				if(err) {
					return console.log(err);
				}
				//console.log("The failed file was saved!");
			});
		}		
	}
	browser.close();
}
 
run();

Поскольку кнопка, которую пытается нажать Puppeteer, отсутствует, то это вызывает ошибку. Сообщение ошибки выводится в консоль:

TypeError: Cannot read properties of null (reading 'click')
    at run (/home/mial/bin/tests/puppeteer/fail.js:28:17)

Также будет создано два файла: fail.png и failed.htm, содержащие скриншот и DOM страницы на момент возникновения ошибок.

3.6 Как в Puppeteer приостановить выполнение скрипта и действий на некоторое время. Задержка работы скриптов Puppeteer

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

То есть если вы хотите приостановить работу Puppeteer для завершения выполнения JavaScript, то высока вероятность того, что на самом деле вам это не нужно — вполне возможно, что Puppeteer по умолчанию корректно выполнит вашу задачу.

Тем не менее, я сталкивался с ситуациями, что при взаимодействии с веб-страницами в Puppeteer действительно нужно ожидать завершения операций (например, дожидаться, когда будет подгружена следующая часть страницы).

В библиотеке Puppeteer имелись средства для приостановки работы Puppeteer, но в настоящее время все эти функции и методы признаны устаревшими и удалены из библиотеки Puppeteer.

Тем не менее, вы можете использовать следующую конструкцию для приостановки Puppeteer перед тем, как веб-браузер приступит к следующему запрограммированному действию:

function delay(time) {
	return new Promise(function(resolve) { 
		setTimeout(resolve, time)
	});
} 

// some actions
// await button.click();
await delay(4000);

Практический пример использования задержки — если из скрипта эту задержку убрать, то результат будет не такой, как ожидается:

const puppeteer = require('puppeteer');

function delay(time) {
	return new Promise(function(resolve) { 
		setTimeout(resolve, time)
	});
} 
async function run() {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
   
	await page.setViewport({width: 1440, height: 3440});
	await page.setUserAgent(customUserAgent); 
	await page.goto('https://www.zdnet.com/topic/artificial-intelligence/');
	
	for (let i = 0; i < 5; i++) { 
		await page.evaluate(() => {
			window.scrollTo(0, document.body.scrollHeight);
		});
		
		try {
			const button = await page.$('div > div.c-loadMoreButton.u-grid-columns > a');
			await button.click();
			await delay(4000);
		}
		catch (error) {
			console.error(error);
			await page.screenshot({ path: 'fail.png', fullPage: true });
			const html = await page.content();
			const fs = require('fs');
			fs.writeFile("failed.htm", html, function(err) {
				if(err) {
					return console.log(err);
				}
			});
		}		
	}
	
	await page.screenshot({ path: 'delay.jpg', fullPage: true });
	browser.close();
}
 
run();

Результат выполнения скрипта (вы можете закомментировать строку «await delay(4000);» и убедиться, что действительно требуется дождаться завершения подгрузки страницы:

3.7 Как в Puppeteer сделать скриншот страницы с картинками если сайт использует lazy download

Ленивая загрузка (lazy download) – это метод ожидания загрузки определённых частей веб-страницы – особенно изображений – до тех пор, пока они не понадобятся. Вместо того, чтобы загружать всё сразу, что называется «жадной» загрузкой, браузер не запрашивает определённые ресурсы, пока пользователь не взаимодействует таким образом, что эти ресурсы понадобятся.

Чтобы было понятнее, на практике это проявляется, например, в том, что картинки загружаются только после того, как вы проскроллили до них. Обратите внимание на следующий скриншот — в верхней части экрана картинки загрузились, а в нижней части экрана картинки не загружены. И бесполезно ставить задержку и ждать загрузки картинок — они не будут загружен до тех пор, пока пользователь не проскроллит до них.

Но что делать в этом случае при использовании Puppeteer? Вариантов как минимум два:

  1. Проскроллить до картинок для их загрузки — именно так, как это и предполагается логикой сайта.
  2. Увеличить размер окна веб-браузера так, чтобы картинки попали на первый экран без скроллинга.

Мы рассмотрим оба варианта, давайте начнём со второго, поскольку он совсем простой.

В предыдущих частях мы уже познакомились с методом setViewport(), который позволяет устанавливать размер окна виртуального веб-браузера. Сейчас мы опять будем использовать его, но установим значение высоты значительно больше, например:

const puppeteer = require('puppeteer');

async function run() {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
   
	await page.setViewport({width: 1440, height: 3440});
	await page.setUserAgent(customUserAgent); 
	await page.goto('https://www.zdnet.com/topic/artificial-intelligence/');
	
	await page.screenshot({ path: 'lazy-download.jpg', fullPage: true });
	browser.close();
}
 
run();

Как можно убедиться, теперь все картинки загрузились, поскольку веб-сайт считает, что всё содержимое страницы находится на первом экране.

Показанные в этом примере размеры экрана не является пределом, например, следующие размеры экрана также не вызывают ошибки:

await page.setViewport({width: 14400, height: 14400});

То есть вы можете экспериментировать с размером.

3.8 Как скроллировать страницу в Puppeteer

Для скроллинга страницы вы можете использовать следующий код:

await page.evaluate(() => {
	window.scrollTo(0, document.body.scrollHeight);
});

Обратите внимание, что скорее всего нужно будет установить задержку перед следующим действием, чтобы веб-страница могла подгрузить необходимые данные.

Пример:

const puppeteer = require('puppeteer');

function delay(time) {
	return new Promise(function(resolve) {
		setTimeout(resolve, time)
	});
}

async function run() {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
   
	await page.setViewport({width: 1440, height: 1440});
	await page.setUserAgent(customUserAgent); 
	await page.goto('https://www.zdnet.com/topic/artificial-intelligence/');
	await page.evaluate(() => {
		window.scrollTo(0, document.body.scrollHeight);
	});
	
	await delay(4000);
	
	await page.screenshot({ path: 'scrolled.jpg', fullPage: true });
	browser.close();
}
 
run();

Можно убедиться, что и теперь все картинки загружены (обратите внимание, что скриншот не идеален — рекламный блок, который должен быть прилеплен к низу экрана, находится в неположенном месте).

3.9 Как делать постепенный скроллинг страницы в Puppeteer

Установление большой высоты окна веб-браузера, либо прокрутка в самый низ веб-браузера обычно хорошо работают для дозагрузки lazy download контента. Кроме этих вариантов, можно также использовать постепенную прокрутку страницы — ниже показан пример кода:

const puppeteer = require('puppeteer');

function delay(time) {
	return new Promise(function(resolve) { 
		setTimeout(resolve, time)
	});
}
async function run() {
	const browser = await puppeteer.launch();
	const page = await browser.newPage();
	const customUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
   
	await page.setViewport({width: 1440, height: 800});
	await page.setUserAgent(customUserAgent); 
	await page.goto('https://www.zdnet.com/topic/artificial-intelligence/');

	const height = await page.evaluate(() => {
		return document.body.scrollHeight;
	});

	window_h = page.viewport().height
	step = height / window_h + 1
	for (let i = 0; i < step; i++) {
		current_height = window_h * i
		await page.evaluate((current_height) => {
			window.scrollTo(0, current_height);
		}, current_height);
		await delay(300);
	}
	await page.screenshot({ path: 'smooth-scroll.jpg', fullPage: true });

	browser.close();
}

run();

Это скриншот после плавной прокрутки — как можно убедиться, теперь все картинки прогрузились и в целом со страницей не возникло проблемы, которая имеется в предыдущем примере при простом перемещении вниз:


Рекомендуется Вам:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *