CoderCastrov logo
CoderCastrov
Node.js

Node.js: парсинг веб-страниц с использованием Puppeteer + обход reCaptcha

Node.js: парсинг веб-страниц с использованием Puppeteer + обход reCaptcha
просмотров
15 мин чтение
#Node.js

Puppeteer - это библиотека Node.js, которая предоставляет высокоуровневый API для программного управления Chrome или Chromium через протокол DevTools. Это управление может происходить в фоновом режиме (без открытия окна), или, если вы предпочитаете, вы можете наблюдать весь процесс визуально, запросив открытие графического интерфейса браузера.

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

Ресурсы Puppeteer

В основном, любые действия, выполняемые в Chrome или Chromium, могут быть автоматизированы с помощью Puppeteer. Вот несколько примеров:

  • Симуляция действий пользователей, таких как переход по страницам, нажатие на ссылки/кнопки, заполнение и отправка форм;
  • Создание скриншотов страниц;
  • Сохранение определенного контента в формате PDF;
  • Мониторинг определенного значения или состояния страницы для выполнения дополнительного действия;
  • Поиск данных на страницах;
  • и многое другое...

На самом деле, возможности бесконечны! Puppeteer может использоваться как для парсинга (web scraping), так и для парсинга сайтов (web crawling). Давайте кратко рассмотрим, что означает каждый термин:

  • Парсинг: это автоматическое загрузка данных с одной или нескольких страниц с целью извлечения очень конкретной информации; Автоматизация действий, выполняемых через интерфейс браузера, таких как заполнение и отправка форм или даже имитация навигации пользователя на "сайте".
  • Парсинг сайтов: это автоматическая загрузка данных с веб-страницы, извлечение содержащихся в ней гиперссылок и рекурсивное следование им. Грубо говоря, это один из методов, используемых поисковыми системами, такими как Google, Bing и другие.

📍Где использовать?

Трудно сказать, где не использовать. Но вот несколько возможных сценариев:

Тема 3 была той, которая побудила меня использовать Puppeteer. Он относительно просто "получает" веб-страницу программным способом, извлекает и отправляет данные. Однако это становится все более сложным, по мере того, как страницы начинают использовать JavaScript для рендеринга своих компонентов и реагирования на события, вызванные пользовательским взаимодействием.

Существует множество подходов, инструментов и библиотек, которые позволяют получать данные с веб-страниц, отправлять формы и так далее, но многие из них пытаются выполнять эту манипуляцию, рассматривая только структуру HTML страниц, то есть работают напрямую с DOM-деревом (Document Object Model). Работа с DOM является простой из-за его статического состояния, но все меняется, когда у нас есть страницы, которые используют JavaScript для выполнения рендеринга на стороне клиента (client side rendering). В таких случаях необходим движок, способный выполнять/интерпретировать JavaScript страницы, после чего DOM будет обновлен и его последнее состояние будет доступно для нашего парсинга или парсинга веб-страницы.

Puppeteer в действии

Ниже приведен список предварительных требований для создания решения для парсинга веб-страниц с использованием Puppeteer. Вот:

В этом материале я буду использовать Yarn в качестве менеджера зависимостей, но вы можете использовать NPM, если предпочитаете. В качестве редактора кода я буду использовать VSCode, но вы можете использовать любой другой редактор, который вам нравится. 😋

Установка

Для следующих шагов рекомендуется создать новый каталог, перейти в него и сосредоточить весь код внутри него. Let’s get started! 🚀

  • Чтобы создать новый автоматически сгенерированный проект, выполните следующую команду:
yarn init -y
  • init: указывает yarn создать файл package.json, который содержит начальные настройки проекта и список зависимостей;
  • -y: молчаливое подтверждение начальных настроек. Если вы удалите этот параметр, вы сможете указать имя проекта, описание, автора и т. д.

Теперь мы установим нашу единственную зависимость:

yarn add puppeteer

Согласно официальной документации, пакет puppeteer занимает следующее место на диске:

  • MacOS: ~170MB
  • Linux: ~282MB
  • Windows: ~280MB

При установке модуля puppeteer будет автоматически загружена автономная версия Chromium, поэтому указанные выше значения представляют собой объем места, занимаемого бинарным файлом. По желанию вы можете использовать пакет puppeteer-core, который не загружает бинарный файл, а использует уже установленную версию Chromium на вашем компьютере. Лично я предпочитаю устанавливать версию, которая уже содержит бинарный файл, чтобы сохранить вещи отдельно и избежать дополнительных проблем с отладкой.

Primeiro uso (exemplo 1)

Этот простой пример предназначен для демонстрации работы Puppeteer. Создайте файл example1.js в корне вашего проекта и скопируйте в него следующий код:

const puppeteer = require('puppeteer');(async () => {
  const browser = await puppeteer.launch();  const page = await browser.newPage();
  await page.goto('[http://books.toscrape.com/'](http://books.toscrape.com/'));
  await page.screenshot({path: 'example1.png'});  await browser.close();
})();

Теперь выполните в терминале:

node example1.js

Обратите внимание, что в вашей директории был создан файл изображения с названием example1.png, который представляет собой снимок экрана страницы http://books.toscrape.com. Пока что мы только начинаем, но делаем шаг за шагом!

Понимание кода

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

const puppeteer = require('puppeteer');

Импортируем модуль Puppeteer, это высокоуровневый API, о котором мы говорили в начале этого поста.

(async () => {
 // ... код здесь
})();

Асинхронная анонимная функция, это необходимо, потому что наш код будет получать данные, которые не доступны немедленно, то есть ресурсы, которые могут занять время для возврата. Чтобы узнать больше об этом поведении, прочитайте мою статью: Введение в Node.js (однопоточность, цикл событий и рынок).

const browser = await puppeteer.launch();

По умолчанию запускается самостоятельный экземпляр Chromium, но можно указать путь к другой установке Chromium или Chrome, а также указать высоту и ширину окна, даже когда запуск происходит в фоновом режиме.

const page = await browser.newPage();

Открывает новую вкладку в браузере.

await page.goto('http://books.toscrape.com/');

Открывает указанный URL. Выбранный для демонстрации сайт - это открытый проект, специально разработанный для тестирования парсинга (веб-скрапинга или даже веб-краулинга).

await page.screenshot({path: 'example1.png'});

Снимает скриншот и сохраняет изображение на диске.

await browser.close();

Закрывает экземпляр браузера.

Для получения дополнительной информации ознакомьтесь с официальной документацией: https://pptr.dev

Парсинг данных (Пример 2)

В первом примере мы получили доступ к веб-странице и сделали скриншот, наша цель была проста - понять основы работы с Puppeteer. Теперь мы узнаем, как извлекать данные с веб-страницы. Создайте файл с именем example2.js в корневой папке вашего проекта и добавьте в него следующий код:

const puppeteer = require('puppeteer');
let scrape = async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://books.toscrape.com/');
  const result = await page.evaluate(() => {
    const books = [];
    document.querySelectorAll('section > div > ol > li img')
            .forEach((book) => books.push(book.getAttribute('alt')));
    return books;
  });
  browser.close();
  return result;
};
scrape().then((value) => {
  console.log(value);
});

Теперь выполните в терминале:

node example2.js

В результате вы увидите примерно следующее:

Output da consulta na console do usuário.

Понимание кода

Я пропущу части, которые мы уже рассмотрели в примере 1. Так что давайте перейдем к новому:

const result = await page.evaluate(() => {
    const books = []
    document.querySelectorAll('section > div > ol > li img')
            .forEach(book => books.push(book.getAttribute('alt')))
    return books
})

Этот код работает, но мы можем переписать его более элегантно и прямолинейно, вот:

const result = await page.evaluate(_ => 
  Array.from(
    document.querySelectorAll('section > div > ol > li img'))
            .map(books => books.getAttribute('alt'))
)

evaluate используется для выполнения JavaScript-кода непосредственно в запущенном браузере, то есть то, что находится внутри этого метода, не будет выполняться непосредственно Node.js, а будет выполняться в браузере, запущенном с помощью Puppeteer.

Хотите лучше понять? Откройте свой любимый браузер, перейдите по адресу: http://books.toscrape.com, откройте Инструменты разработчика > Консоль, вставьте следующий код и нажмите ENTER:

document.querySelectorAll('section > div > ol > li img')
        .forEach(book => console.log(book.getAttribute('alt')))

Этот код является простым вариантом того, что мы выполнили ранее с использованием метода page.evaluate(), в основном вместо создания массива мы непосредственно выполняем console.log(). Точно так же мы можем использовать консоль инспектора кода для выполнения нашей "более элегантной" альтернативы:

Array.from(document.querySelectorAll('section > div > ol > li img'))
     .map(books => books.getAttribute('alt'))

Результат:

Выполнение скрипта непосредственно в консоли браузера.

Вот и все. Наш код прошел по странице, ища атрибут alt (в данном случае это атрибут, содержащий название книг) каждого изображения, находящегося непосредственно внутри section > div > alt > li img, и вывел результат в консоль.

Будьте осторожны с использованием console.log() внутри метода page.evaluate(), эта инструкция будет выполнена в консоли браузера, а не в консоли Node.js, поэтому вы не увидите никакого результата. Однако ничто не мешает вам вывести результат из метода и затем использовать console.log().

Я получил эту структуру элементов (section > div > alt > li img) благодаря предварительному анализу кода. Здесь нет никаких вариантов, нужно открыть инспектор кода и проанализировать, как добраться до нужных данных. Например:

Обход дерева элементов HTML.

Мы могли бы использовать более специфичный селектор, например section > div > ol > li > article > div > a > img? Конечно, да, однако в данном случае я не видел необходимости, так как на этой странице нет других аналогичных структур, которые могли бы вернуть нежелательные данные. Помните, не усложняйте вещи, если можно упростить.

Рефакторинг с использованием page.$$eval

page.$$eval является альтернативой page.evaluate(), основное отличие заключается в том, что он неявно выполняет Array.from(document.querySelectorAll(selector)). Вот как выглядит код после рефакторинга:

const puppeteer = require('puppeteer')let scrape = async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('[http://books.toscrape.com/'](http://books.toscrape.com/'))  const result = await page.$$eval('li img', titles =>
    titles.map(titles => titles.getAttribute('alt'))
  )  browser.close()
  return result
};scrape().then((value) => {
  console.log(value)
})

Здесь мы получаем точно такой же результат, но с меньшим количеством кода. Обратите внимание, что я также уменьшил использование селекторов (li img и alt), однако это сокращение селекторов также можно было применить к page.evaluate() без проблем.

Взаимодействие с кликами (пример 3)

Пришло время сделать еще один шаг, теперь мы научимся кликать по кнопкам, ссылкам и любым другим элементам, ждать загрузки следующей страницы и получать информацию с этой страницы. Создайте файл с названием example3.js в корневой папке проекта и добавьте в него следующий код:

const puppeteer = require('puppeteer')
let scrape = async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('http://books.toscrape.com/')
  await page.click('h3 > a')
  await page.waitForNavigation()
  await page.screenshot({ path: 'example3.png' })
  const result = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('div.product_main')).reduce(
      (result, book) => {
        return {
          title: book.getElementsByTagName('h1')[0].innerText,
          price: book.getElementsByClassName('price_color')[0].innerText,
        }
      }, {})
  })
  browser.close()
  return result
}

scrape().then((value) => {
  console.log(value)
})

Теперь выполните в терминале:

node example3.js

В результате вы увидите примерно следующее изображение:

Output de objeto na console.

Помимо вывода выше, помните, что мы добавили в код команду для создания скриншота, то есть в корневой папке проекта будет сохранен файл example3.png с снимком экрана страницы.

Понимание кода

Перейдем к непроанализированным участкам кода:

page.click("h3 > a");

Этот код указывает, какой элемент должен быть нажат. В данном случае, указано, что должна быть нажата первая ссылка внутри элемента h3. Селектор может состоять из тегов, идентификаторов и классов. Важно помнить, что действие будет выполнено для всех элементов, соответствующих указанному селектору.

await page.waitForNavigation();

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

await page.waitForTimeout(1000);

Этот метод может принимать целое число, представляющее время в миллисекундах, строку, представляющую селектор HTML, или даже функцию. Условие будет истинным, когда время истечет, когда селектор HTML будет найден на странице или когда внутренняя функция вернет true. То есть поведение определяется типом входного параметра.

Помимо вышеуказанного метода, который принимает функцию в качестве параметра, у нас есть page.waitForFunction(). Вот пример использования:

await page.waitForFunction(
  'document.querySelector("body").innerText.includes("Загружено!")'
);

Инструкция waitForFunction ждет, пока внутренний код не вернет true. В данном примере внутренний код функции проверяет, содержит ли тело страницы слово "Загружено!". Если это не так, выполнение будет ожидать, пока оно не появится.

Существуют и другие варианты waitFor*, ознакомьтесь с документацией, чтобы узнать больше.

Рефакторинг с использованием page.$eval

Метод page.$eval() (не путать с page.$$eval()) может использоваться для выбора определенного селектора и является альтернативой page.evaluate(). Неявно этот метод выполняет document.querySelector. Вот как выглядит код после рефакторинга:

const puppeteer = require('puppeteer');
let scrape = async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://books.toscrape.com/');
  await page.click('h3 > a');
  await page.waitForNavigation();
  await page.screenshot({path: 'example3.png'});
  const title = await page.$eval(
    'div.product_main h1', divs => divs.innerText
  );
  const price = await page.$eval(
    'div.product_main .price_color', divs => divs.innerText
  );
  browser.close();
  return {title, price};
};

scrape().then((value) => {
  console.log(value);
});

Переведем page.$eval на русский язык, в основном он делает следующее:

document.querySelector('div.product_main h1').innerText

При выполнении этой инструкции непосредственно в консоли браузера будет выведен заголовок книги, но помните, что для этого необходимо перейти на http://books.toscrape.com и щелкнуть по книге, ведь именно это делает пример 3.

Советы

В этом разделе я предоставлю несколько полезных советов и/или рекомендаций. Для более подробной информации обратитесь к официальной документации.

const browser = await puppeteer.launch({headless: true})

Метод launch() в puppeteer по умолчанию принимает значение headless: true. Это означает, что выполнение происходит в фоновом режиме. Однако, если вы измените значение на false, вы сможете наблюдать выполнение, поскольку будет запущен и управляем программно графический экземпляр браузера.

Если вы сталкиваетесь с системой, которая требует ввода Captcha, у вас есть два варианта: использовать отдельный механизм/алгоритм для разгадывания Captcha или запустить графический экземпляр браузера с помощью await page.waitForTimeout(10000);, чтобы у вас было достаточно времени для разгадывания Captcha. Это особенно полезно, когда у вас есть процесс, который работает в цикле и редко требует повторного прохождения этапа с Captcha.

await page.type('#idCampo', 'значение');

Это один из способов вставить значение в поле. Обратите внимание, что #idCampo - это селектор, который я использовал, вы можете использовать другие типы селекторов, помимо идентификаторов.

await page.keyboard.press('Enter');

Этот метод имеет множество полезных функций. page.keyboard.press() может нажать практически любую клавишу или комбинацию клавиш. Одно из возможных применений: вы заполняете форму входа, после завершения вы можете вызвать метод, который нажимает кнопку входа, указав ID кнопки, или просто использовать этот метод, если форма принимает нажатие клавиши Enter в качестве команды отправки, что является стандартным поведением для большинства сайтов.

const result = await page.$x("//td[contains(text(), 'Текст здесь')]");
if (result.length > 0) {
  await result[0].click();
}

У меня были случаи, когда кнопка, на которую я хотел нажать, не имела уникального идентификатора и не имела фиксированного положения в дереве элементов страницы. Чтобы обойти эту проблему, я использовал page.$x(). В основном мы указываем тег, который обрамляет элемент (в данном случае это был td), проверяем, содержит ли он искомое значение в своем тексте и сохраняем ссылку на элемент.

Затем мы проверяем, существует ли элемент > 0, и если он существует, вызываем метод click для элемента. Грубо говоря, это все 😉

Парсинг данных с CEI (Портал инвестора) + разгадывание reCaptcha

Для выполнения приведенного ниже кода рекомендуется склонировать общедоступный репозиторий на GitHub: https://github.com/fabiojaniolima/cei-web-scraping и следовать инструкциям в файле README.

Для входа в CEI нам нужно пройти через систему reCaptcha, поэтому нам потребуется работа с "взломом" капчи. Чтобы упростить нашу жизнь, я решил использовать 2captcha, платный, но очень эффективный сервис с хорошим соотношением цены и качества. У нас есть варианты пакетов с разгадкой 1000 капч за 0,75 доллара.

Если этот материал был полезен для вас и вы планируете использовать 2captcha, пожалуйста, рассмотрите возможность использования моей партнерской ссылки https://2captcha.com?from=11308844, это будет моим стимулом создавать больше материалов по этой теме.

Кроме того, нам нужно установить следующие дополнительные библиотеки:

yarn add puppeteer-extra puppeteer-extra-plugin-recaptcha

Пакет puppeteer-extra - это простая и легкая оболочка вокруг puppeteer, его основная функция - предоставить удобный интерфейс для использования плагинов. Важно: вам все равно нужно установить пакет puppeteer или puppeteer-core, но вам нужно только импортировать puppeteer-extra, и он позаботится об остальном.

Вот упрощенная версия кода:

require('dotenv/config')
const puppeteer = require('puppeteer-extra')
const RecaptchaPlugin = require('puppeteer-extra-plugin-recaptcha')const {
  CEI_USERNAME,
  CEI_PASSWORD,
  CEI_MAIN_URL,
  BACKGROUND_NAVIGATION,
  CAPTCHA_TOKEN,
} = process.envpuppeteer.use(
  RecaptchaPlugin({
    provider: {
      id: '2captcha',
      token: CAPTCHA_TOKEN,
    },
    visualFeedback: true, // colorize reCAPTCHAs (violet = detected, green = solved)
  })
)const main = async () => {
  const browser = await puppeteer.launch({ headless: BACKGROUND_NAVIGATION })
  const page = await browser.newPage()
  await page.goto(`${CEI_MAIN_URL}/login.aspx`)await page.waitForFunction(
    'document.querySelector("body").innerText.includes("CPF/CNPJ")'
  )await page.type('#ctl00_ContentPlaceHolder1_txtLogin', CEI_USERNAME)
  await page.type('#ctl00_ContentPlaceHolder1_txtSenha', CEI_PASSWORD)await page.solveRecaptchas()await Promise.all([
    page.waitForNavigation(),
    page.click('#ctl00_ContentPlaceHolder1_btnLogar'),
  ])await page.goto(`${CEI_MAIN_URL}/ConsultarCarteiraAtivos.aspx`)await page.click('#ctl00_ContentPlaceHolder1_btnConsultar')await Promise.all([
    page.waitForResponse(`${CEI_MAIN_URL}/ConsultarCarteiraAtivos.aspx`),
    page.click('#ctl00_ContentPlaceHolder1_btnConsultar'),
  ])await page.waitForTimeout(1000)const result = await page.evaluate(() => {
    const columnsTitle = []
    const stocks = []document
      .querySelector(
        'table[id^=ctl00_ContentPlaceHolder1_rptAgenteContaMercado_ctl0] thead > tr'
      )
      .querySelectorAll('th')
      .forEach(column => {
        columnsTitle.push(column.innerText)
      })document
      .querySelectorAll(
        'table[id^=ctl00_ContentPlaceHolder1_rptAgenteContaMercado_ctl0] tbody tr'
      )
      .forEach(stock => {
        let newObject = {}stock.querySelectorAll('td').forEach((value, index) => {
          newObject[columnsTitle[index]] = value.innerText
        })stocks.push(newObject)
      })return stocks
  })console.log(result)await browser.close()
}main()

Пример вывода:

Результат после захвата и разбора данных на портале CEI.

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

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

Обратите внимание, что в течение кода я использовал три разных метода для ожидания "некоторого времени" перед продолжением выполнения скрипта:

waitForFunction()
waitForNavigation()
waitForTimeout()

Я использовал waitForFunction() для ожидания загрузки метки "CPF/CNPJ" на экране, иначе следующий метод мог бы попытаться заполнить поле ввода до его загрузки.

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

Наконец, evaluate() будет получать данные, выполнять разбор и возвращать результат. Помните, что все, что находится внутри этого метода, выполняется в экземпляре браузера, запущенном Puppeteer, поэтому, если вы выполните console.log(), вы не увидите вывода, потому что вывод будет происходить в экземпляре браузера, а не в терминале, где вы запустили скрипт.

Не создавайте свою логику захвата и разбора данных непосредственно внутри скрипта, перейдите на страницу, на которой будет выполняться разбор, откройте консоль вашего браузера и создайте ее там. Когда все будет готово, перенесите код внутрь evaluate. Этот подход даст вам большую гибкость и оперативность во время разработки, а также возможность тестировать код в режиме реального времени.

Рекомендации по чтению

Этот пост был написан с учетом парсинга веба, который в основном сводится к извлечению данных и взаимодействию с веб-страницами. Если вашей целью является рекурсивное отслеживание и парсинг контента, используйте инструменты веб-краулера, такие как: https://sdk.apify.com

Заключение

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

Наконец, но не менее важно, стоит отметить, что даже с простыми примерами мы использовали очень интересные возможности, такие как цикл forEach, map, reduce и другие.

Итак, я заканчиваю здесь, надеюсь, вам понравилось 😉