• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья 🎭 Парсинг и не только с Puppeteer проще, чем можно подумать

BlameUself

RAM
Пользователь
Регистрация
12.04.2022
Сообщения
121
Реакции
161
Автор: BlameUself
Источник: xss.pro

JavaScript — поистине прекрасный язык программирования. Его миры безграничны и позволяют решать многочисленные задачи. Среди множества существует такая библиотека, как Puppeteer, которая позволяет автоматизировать массу задач. В первую очередь, она невероятно удобна для написания парсинга и авторегистраций. Но её потенциал позволяет реализовать любую задачу, которую вы способны сделать руками в рамках браузера.

Эта статья построена на нескольких примерах, приближенных к реальным задачам. Я склонен довериться тому, что разработку стоит осваивать, отталкиваясь именно от 'бизнес-требований', после прочтения минимальной информации о необходимых для решения инструментах. Это наиболее эффективный способ взаимодействия. Касательно того, насколько сложно разобраться с Puppeteer? При должной усердности освоить библиотеку можно с нуля за несколько недель, даже если вы достаточно поверхностно знаете язык.

Однажды я публиковал дорожную карту FullStack разработки на основе JavaScript, она все еще актуальна - ссылка на дорожную карту. Вам не требуется пройти её полностью для чтения этой статьи, совсем нет, но если вы захотите узнать больше о возможностях языка и список наиболее актуальных технологий, то можете продолжить с её помощью. В любом случае, я бы хотел, чтобы каждый после прочтения смог работать с библиотекой, и в то же время я не стану останавливаться на каждой строчке работы JavaScript. Было бы здорово предварительно ознакомиться с курсом
А также с https://learn.javascript.ru/. Эти два источника будут достаточны для свободного продолжения.

Перед началом работы с Puppeteer вам необходимо убедиться в том, что у вас установлен рантайм для JavaScript - Node.js. Его можно скачать с https://nodejs.org/en, и вместе с ним будет установлен пакетный менеджер NPM. Для проверки откройте терминал и введите команду 'node --version'. В случае успешного выполнения вы увидите версию Node.js. В качестве среды разработки (IDE) я буду использовать модный и бесплатный VS Code, но вы можете использовать любую по своему выбору.

Небольшое теоретическое введение:

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

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

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

Начну я с тех случаев, когда использование Puppeteer будет излишним решением.
Предположим, перед нами стоит задача получения данных о курсе криптовалют.
Итак, в сети существует множество API (Application Programming Interface), которые имеют удобные интерфейсы для предоставления данных. Способ получения данных с их помощью будет примерно следующим:
Вводим поисковой запрос в Google "free api crypto prices" и выбираем любое на свой вкус. Я выберу CoinGecko.
Untitled (20).png

Как правило, для работы с API необходим ключ, который служит методом аутентификации, а также ограничителем использования. Да, можно сказать, что минусом такого способа получения данных будет ограничение количества запросов.
Также мы видим ссылку на документацию. Плюсом такой работы является то, что у API имеется описание.
Untitled.png

Хорошо, как мы видим, от нас требуют ключ для работы. Регистрируемся и получаем ключ.

Кстати говоря, для достаточно популярных API вы легко сможете найти ключ без регистрации и SMS, используя дорки на GitHub. В данном случае это x_cg_pro_api_key. Иногда люди делятся .env файлами, но иногда они просто хардкодят. Несколько ключей оказались недействительными, но я все же достаточно быстро нашел один:
Untitled (1).png

Итак, допустим, мы хотим узнать актуальную цену биткойна. Внимательно изучаем эндпоинты и находим нужный.
Untitled (2).png

Прекрасная документация. Она сразу предоставляет необходимый код для запроса, отдаёт данные, дает возможность тестировать параметры - очеравательно описанное API.
Копируем код, который нам предоставили, и идем в VS Code. Вам необходимо изучить полученные данные, поскольку я вижу этот эндпоинт впервые, и я не знал, что он делает. Оказалось, он отдает данные сразу по множеству бирж, так даже круче.
Перепишем код исходя из полученного объекта, пройдемся по каждому tickers методом forEach и выведем название маркета + цену:
JavaScript:
const url = 'https://pro-api.coingecko.com/api/v3/coins/bitcoin'
const options = {
  method: 'GET',
  headers: { 'x-cg-pro-api-key': 'CG-QyGfobho7fpz7W33LMCU9dnX' },
}

fetch(url, options)
  .then((res) => res.json())
  .then((json) => {
    json.tickers.forEach((ticker) => {
      console.log(ticker.market.name + ' ' + ticker.last)
    })
  })
  .catch((err) => console.error('error:' + err))
Untitled (3).png

Вместо написания десятков веб-скраперов, мы использовали API. Итого, если требуется только выполнение простых HTTP-запросов и обработка ответов без необходимости автоматизации действий в браузере, старый добрый Fetch - наш выбор, и никаких папетиров).

И все же, даже если вы не нашли публичное API, возможно вам все еще не нужен Puppeteer. Предположим, наша задача - получить данные с сайта https://nftpricefloor.com/, а он (предположим, сайт я взял для примера) не предоставляет публичного API. В таком случае, мы можем самостоятельно изучить запросы, которые наш браузер отправляет к серверу. Это более времязатратный способ, поскольку никакой документации вы не найдете. Тем не менее, открываем инструменты разработчика (в хроме это F12). Панель следует открыть до момента перехода на сайт. Переходим во вкладку Network и видим список запросов. Предположим, что вся интересующая нас информация находится именно в этом запросе (замечаем в нем API, впрочем, это не всегда может быть так). Если вам сложно понять, за что отвечает тот или иной запрос, пересмотрите все во вкладке Response.
Untitled (4).png

Переходим во вкладку Response и видим нужную нам информацию:
Untitled (5).png

Пишем код для получения данных, в данном случае имени и адреса контракта.
JavaScript:
const url = 'https://api-bff.nftpricefloor.com/search'

const options = {
  method: 'GET',
}
fetch(url, options)
  .then((res) => res.json())
  .then((json) => {
    json.forEach((item) => {
      console.log(item.name + ' ' + item.contractAddress)
    })
  })
  .catch((err) => console.error('error:' + err))
Untitled (6).png

Проверка запросов может здорово сократить время на написание парсера.

Ну что ж, переходим к Puppeteer.
Предположим, мы решили открыть онлайн-кинотеатр мультфильмов, а список решили взять с сайта https://rezka.io/multfilmy/.
Открываем редактор кода в папке с будущим проектом, устанавливаем библиотеку командой ‘npm i puppeteer’. После установки у вас появятся файлы package.json, package-lock.json и папка node_modules.
Собственно, сам код. Рассмотрим боллерплейт:
JavaScript:
const puppeteer = require('puppeteer')
async function main() {
  let browser = await puppeteer.launch()
  try {
  } catch (e) {
    console.error(e)
  } finally {
    await browser?.close()
  }
}
main()
Подключаем модуль Puppeteer. Объявляем асинхронную функцию main(). Запускаем экземпляр браузера с помощью метода puppeteer.launch(). Далее объявляем блок try/catch, в котором в блоке try содержится основной код будущих скриптов, а блок catch создан для отлова ошибок. В блоке finally, независимо от того, возникло исключение или нет, вызывается метод browser.close(), чтобы закрыть браузер.

Логика работы кода заключается в том, что мы получаем HTML-документ и затем ищем нужную информацию, используя HTML-теги. Следовательно, нам необходимо найти соответствующий тег. Для этого открываем уже знакомую панель разработчика. Во вкладке "Elements" находится весь полученный HTML. В левом верхнем углу будет иконка, на которую можно нажать, чтобы быстро найти место в коде, где находится нужная информация. Вы также можете открыть код страницы, щелкнув правой кнопкой мыши и выбрав "View page source", затем использовать комбинацию клавиш Ctrl + F, чтобы найти нужный текст.
Untitled (7).png

Находим нужный HTML-тег, сохраняем его и переходим в IDE.
JavaScript:
const puppeteer = require('puppeteer')

async function main() {
  let browser = await puppeteer.launch({ headless: false })
  try {
    const page = await browser.newPage()
    await page.goto('https://rezka.io/multfilmy/')

    const data = await page.evaluate(() => {
      const h2Elements = document.querySelectorAll('h2.postTitle')
      const extractedData = Array.from(h2Elements).map((element) => {
        return {
          title: element.querySelector('a').getAttribute('title'),
          href: element.querySelector('a').getAttribute('href'),
        }
      })
      return extractedData
    })

    console.log(data)

    await page.screenshot({ path: 'screenshot.png' })
  } catch (e) {
    console.error(e)
  } finally {
    await browser?.close()
  }
}

main()

Untitled (8).png

Если вы хотите видеть, что происходит в браузере, используйте параметр headless: false. Итак, сперва мы создаем новую вкладку (page) в браузере с помощью метода browser.newPage(). Затем происходит переход на страницу с помощью метода page.goto(). На данный момент меня интересуют ссылки на страницы и название мультфильма. Для этого используем метод page.evaluate(), чтобы выполнить JavaScript в контексте страницы. Этот код извлекает данные из элементов <h2 class="postTitle">, а именно атрибуты title и href всех элементов <a>, внутри этих заголовков, и возвращает массив объектов с этими данными. Далее данные отображаются в консоли. Кроме того, мы делаем скриншот страницы с помощью метода page.screenshot() и сохраняем его в файл screenshot.png. Это может быть полезно для отладки.
Всего на сайте 139 страниц с мультиками. Давайте перепишем код, чтобы получить данные с второй по пятую страницы. В URL отличается лишь цифра, так что код будет следующим:
JavaScript:
const puppeteer = require('puppeteer')
const fs = require('fs')

async function main() {
  const browser = await puppeteer.launch({ headless: false })
  try {
    const allData = []

    for (let pageNumber = 2; pageNumber <= 5; pageNumber++) {
      const page = await browser.newPage()
      await page.goto(`https://rezka.io/multfilmy/page/${pageNumber}/`)

      const data = await page.evaluate(() => {
        const h2Elements = document.querySelectorAll('h2.postTitle')
        const extractedData = Array.from(h2Elements).map((element) => {
          return {
            title: element.querySelector('a').getAttribute('title'),
            href: element.querySelector('a').getAttribute('href'),
          }
        })
        return extractedData
      })

      allData.push(...data)

      await page.close()

      await new Promise((resolve) => setTimeout(resolve, 2000))
    }

    fs.writeFileSync('data.json', JSON.stringify(allData, null, 2))
    console.log('Data saved to data.json')
  } catch (e) {
    console.error(e)
  } finally {
    await browser?.close()
  }
}

main()
Очень круто выводить данные в консоль, но ещё более невероятно их куда-то сохранять. Предлагаю делать это в JSON формате, для чего подключаем модуль fs (file system). Создаём цикл и переменную allData, в которую добавляем данные на каждой итерации. Добавим небольшую задержку в 2 секунды между переходами по страницам, используя промис. В конце преобразуем объект в JSON и сохраняем его в папке проекта.

Допустим, мы хотим получить все изображения, чтобы добавить их в свой онлайн-кинотеатр. Мы находим в коде тег, который отвечает за фото, и возвращаемся в VS Code.
JavaScript:
const puppeteer = require('puppeteer')
const fs = require('fs')

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

async function main() {
  let browser = await puppeteer.launch({ headless: false })
  try {
    const page = await browser.newPage()

    const jsonData = require('./data-data.json')

    for (let i = 0; i < jsonData.length; i++) {
      const { title, href } = jsonData[i]

      await page.goto(href)

      await page.waitForSelector('.postImg.postItem-cover img')

      const imgUrl = await page.$eval('.postImg.postItem-cover img', (img) =>
        img.getAttribute('src')
      )

      const response = await fetch(`https://rezka.io${imgUrl}`)
      const buffer = await response.arrayBuffer()

      const filename = `${href.replace(/[^a-zA-Z0-9]/g, '_')}.webp`

      fs.writeFileSync(filename, Buffer.from(buffer))

      console.log(`Изображение "${title}" сохранено как ${filename}`)

      await delay(2000)
    }
  } catch (e) {
    console.error(e)
  } finally {
    await browser.close()
  }
}

main()
Немного оптимизируем код. Во-первых, вынесем промис в отдельную функцию delay. Во-вторых, вынесем переменную page за пределы цикла. Загружается JSON-файл с данными из './data-data.json' (который создали ранее). С помощью метода page.goto() происходит переход на страницу, указанную в поле 'href'. Ждем, пока на странице не появится селектор '.postImg.postItem-cover img' с помощью метода page.waitForSelector(). С помощью метода page.$eval() извлекается атрибут 'src' изображения. Далее выполняется запрос к серверу с помощью fetch() для получения содержимого изображения по полученному URL. Полученное содержимое сохраняется в буфер с использованием метода arrayBuffer(). Формируется имя файла на основе ссылки, заменяя все символы, не являющиеся буквами и цифрами, на символ подчеркивания. Содержимое буфера записывается в файл с помощью fs.writeFileSync().
Итого, мы прошлись по ссылкам и выкачали все изображения мультфильмов из онлайн-кинотеатра.

Мы также можем реализовывать различные другие взаимодействия с браузером, используя Puppeteer. Нашей следующей задачей будет написать авторегистратор. В качестве примера возьмем сайт prizerebel.com
Untitled (9).png

Видим два поля и кнопку, все как обычно. Находим нужные поля в коде страницы. Такие поля, где вводят текст, обычно обозначаются тегом input. Нам нужно вставить в них свои значения. Далее нам нужно нажать кнопку "Sign Up" и убедиться, что код отработал правильно.
Перенесемся в среду разработки:
JavaScript:
const puppeteer = require('puppeteer')

async function main() {
  let browser = await puppeteer.launch({ headless: false })
  try {
    const page = await browser.newPage()
    await page.setViewport({
      width: 1920,
      height: 1080,
    })
    await page.goto('https://www.prizerebel.com/')

    await page.type('#signupFormEmail', 'temp-mail@com.com')
    await page.type('.signupDiv input[name="password"]', 'temptemptemp')

    await page.click('#signupSubmit')
    await page.waitForNavigation({ waitUntil: 'networkidle0' })

    console.log('Done!')

    await page.screenshot({ path: 'final_screenshot.png' })
    console.log('Screenshot saved as final_screenshot.png')
  } catch (e) {
    console.error(e)
  } finally {
    await browser?.close()
  }
}

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

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

Во-первых, для большинства сайтов вы выглядите как бот. Тем не менее, на сайтах, которые я использовал для примера, это не помешало, но так бывает не всегда. Я оставлю это вам в качестве домашнего задания. Перейдите на сайт https://bot.sannysoft.com используя Puppeteer и сделайте скриншот, вы увидите, что он прекрасно определит, что перед ним бот.
Решением этого может быть плагин puppeteer-extra-plugin-stealth. В продолжение задания используйте плагин и увидите соверешенно другую картину.
В целом, плагин может изменить user-agent браузера, чтобы сделать его более похожим на то, как выглядит браузер при использовании обычного пользователя, но это не все его возможности.
Помимо этого, вы можете использовать прокси для скрытия IP-адреса. Для этого добавьте прокси во время инициализации браузера:
JavaScript:
const browser = await puppeteer.launch({
    args: [
      '--proxy-server=YOUR_PROXY_SERVER_IP:PORT',
    ]
  });
Также вы можете запустить код прямо от имени своего браузера Chrome:
JavaScript:
const browser = await puppeteer.launch({
    executablePath: '/путь/к/вашему/браузеру/chrome.exe',
  });
Это действительно хорошие способы, но поистине крутым будет приспособить Puppeteer к антидетект браузеру.

Ещё одной проблемой в этой задаче может стать капча. В целом, наиболее удобным решением будет использование плагина https://www.npmjs.com/package/puppeteer-extra-plugin-recaptcha.
Логика заключается в том, что мы перенаправляем капчу на сервис, где её разгадывают (насколько я понимаю, это до сих пор происходит вручную), и получаем решение. Минус очевиден в скорости работы такой системы и дополнительных затратах.
Могу ошибиться, но, по-видимому, авторами этого плагина являются сами 2captcha. В документации есть хороший пример, и также на их сайте есть отличная статья про решение капчи с помощью Puppeteer. Мне кажется, она достаточно понятна, тем не менее, я думаю, что вы можете смело обратиться в поддержку, и они окажут вам помощь. В целом, я считаю, что любой сервис должен помочь в интеграции своей системы с Puppeteer. Ссылка на статью от 2captcha.

Как и при копировании сайтов, парсинге данных или автоматизации — это довольно простые задачи, и вы легко сможете их освоить, если вам будет интересно это сделать. Удачи!
 
Привет, спасибо за информативную статью. Извини за банальный вопрос, который возможно загуглить, скажи с высоты своего опыта, чем Puppeteer лучше / хуже того же селениума?
 
Привет, спасибо за информативную статью. Извини за банальный вопрос, который возможно загуглить, скажи с высоты своего опыта, чем Puppeteer лучше / хуже того же селениума?
Привет,
У меня совсем не много опыта.
По крайней мере, до этого вопроса я относил Selenium к миру Python, и особо не обращал на него внимания. Когда я начал отвечать тебе, я понял, что это не совсем так. Узнав немного больше об этом, я пришел к выводу, что у Selenium даже более интересный спектр возможностей в плане разнообразия браузеров, и да, у него также есть поддержка JavaScript. Хотя есть некоторые различия, все же они очень схожи. Сравнив синтаксис, мне больше нравится Puppeteer, и он решает те задачи, о которых я говорил. Однако, возможно, имеет смысл попробовать обе библиотеки/фреймворка и выбрать то, что больше по душе.
Я не знаю, почему я думал, что Selenium принадлежит миру Python) Спасибо за вопрос.
Ссылка на сравнение - https://oxylabs.io/blog/puppeteer-vs-selenium
 
Что касается Selenium для Python, есть интересный проект SeleniumBase (ссылку не могу опубликовать, но найти на гитхабе не проблема).
Много классных надстроек над классическим Selenium, которые позволяют обходить блокировки cloudflare и прочие + поддерживает многопоточность из коробки.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх